Last active
January 30, 2023 07:26
-
-
Save manavortex/eb9a8692988e862e3304fea0fb492d03 to your computer and use it in GitHub Desktop.
A Blender script to automatically apply shape keys to NoraLee's glb template for your NPV
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import bpy | |
import bmesh | |
import re | |
import os | |
# For use with the meshes from NoraLee's tutorial | |
# https://docs.google.com/document/d/1clFJhpi7H5jk73vUQPnjIwjkuQV6VnYkKMoXt1eYMb0/view | |
# video documentation on how to use this (thanks Vesna!): https://youtu.be/Zt9Shl8d5rM | |
# TL;DW: | |
# 1. Switch to Blender "Script" tab | |
# 2. Create a new text file, paste the contents of this script into the file | |
# 3. Select the mesh or armature on which you want to apply the shape keys and hit the "play" button | |
# CAUTION: If you select nothing, then all meshes in your file will be processed! | |
# 4. Profit | |
# _ _ _ _ | |
# | | | | | | (_) | |
# ___| |__ __ _ _ __ __ _ ___ | |_| |__ _ ___ | |
# / __| '_ \ / _` | '_ \ / _` |/ _ \ | __| '_ \| / __| | |
# | (__| | | | (_| | | | | (_| | __/ | |_| | | | \__ \ | |
# \___|_| |_|\__,_|_| |_|\__, |\___| \__|_| |_|_|___/ | |
# __/ | | |
# |___/ | |
# Skip hidden objects? | |
ignoreHidden = True | |
# alter the numbers in the column on the right EXACTLY as shown in character creator. | |
meshVariants = { | |
'eyes': '01', # 01 = default, do not change | |
'nose': '01', | |
'mouth': '01', | |
'ears': '01', | |
'jaw': '01', | |
} | |
# Apply custom shapekeys? (Set to True if you want to apply shapekeys directly, | |
# rather than determining them from the character creator) | |
# ATTENTION: See the first comment under "applying shape keys" for how the names match the character creator! | |
applyCustomShapekeys = False | |
# Will only apply if you set the boolean above to "True". | |
# Use this to mix your custom shapekeys. If the shapekey starts with the key (the value on the left), | |
# then its value will be set to the one on the left. | |
# IMPORTANT: This will first apply the settings above, then overwrite them. That is, if you've set | |
# 'eyes': '02' and 'h011': 0.5, the value of the first eyes will be 0.5 and not 1. | |
customShapekeys = { | |
'h011': 0.5, | |
'h021': 0.5, | |
} | |
# | |
# Auto export settings | |
# | |
# set to True if you want to auto-export. | |
# Each armature will be written to a file with its name into the same folder as your blend file. | |
autoExport = False | |
# Only active if autoExport = True: export armatures for Noesis import (FBX)? | |
isNoesisExport = False | |
# _ _ _ _ | |
# | | (_) | | | | | |
# __ _ _ __ _ __ | |_ _ _ _ __ __ _ ___| |__ __ _ _ __ ___ | | _____ _ _ ___ | |
# / _` | '_ \| '_ \| | | | | | '_ \ / _` | / __| '_ \ / _` | '_ \ / _ \ | |/ / _ \ | | / __| | |
# | (_| | |_) | |_) | | |_| | | | | | (_| | \__ \ | | | (_| | |_) | __/ | < __/ |_| \__ \ | |
# \__,_| .__/| .__/|_|\__, |_|_| |_|\__, | |___/_| |_|\__,_| .__/ \___| |_|\_\___|\__, |___/ | |
# | | | | __/ | __/ | | | __/ | | |
# |_| |_| |___/ |___/ |_| |___/ | |
# Number of the component is indicated by the first two digits. SUBTRACT 1 from character creator! | |
# Target body part for the shapekey is indicated by the third digit. | |
# EXAMPLE: | |
# h011 => eyes 2 | |
# h204 => jaw 20 | |
numberToBodyPart = { | |
'1': 'eyes', | |
'2': 'nose', | |
'3': 'mouth', | |
'4': 'jaw', | |
'5': 'ears', | |
} | |
def getTargetComponentName(shapekeyName): | |
try: | |
partIndex = shapekeyName[3] # determined by third number in string, e.g. 'h011' | |
return numberToBodyPart[partIndex] | |
except: | |
return 0 | |
def deleteShapekeyByName(oObject, shapekeyName): | |
# setting the active shapekey | |
iIndex = oObject.data.shape_keys.key_blocks.keys().index(shapekeyName) | |
oObject.active_shape_key_index = iIndex | |
# delete it | |
bpy.ops.object.shape_key_remove() | |
def applyShapekeys(obj): | |
if not obj or not obj.type == 'MESH': | |
print('aborting for ' + obj.type) | |
return | |
if not obj.data.shape_keys: | |
print('mesh ' + obj.name + ' has no shape keys') | |
return | |
print('processing {}'.format(obj.name)) | |
# select it | |
objectToSelect = bpy.data.objects[obj.name] | |
objectToSelect.select_set(True) | |
bpy.context.view_layer.objects.active = objectToSelect | |
# Hold all shapekey names in array, so we can delete them later | |
shapekeyNames = [] | |
for shapekey in obj.data.shape_keys.key_blocks: | |
shapekeyNames.append(shapekey.name) | |
targetComponentName = getTargetComponentName(shapekey.name) | |
# don't set shapekey value for anything that's not "h000" (e.g. Basic) | |
if (not targetComponentName): | |
continue | |
# Get numeric index from meshVariants array | |
shapekeyPartVariant = str(int(meshVariants[targetComponentName] or 0) -1).zfill(2) | |
if (shapekey.name.startswith('h'+ shapekeyPartVariant)): | |
print('applying shape key {} ({}: {})'.format( | |
shapekey.name, | |
targetComponentName, | |
shapekeyPartVariant | |
)) | |
shapekey.value = 1.0; | |
# only apply custom shapekeys if the flag is toggled | |
if applyCustomShapekeys: | |
for customKeyName in filter(lambda keyName: shapekey.name.startswith(keyName), customShapekeys): | |
keyValue = customShapekeys[customKeyName]; | |
print('{}: applying custom value {}'.format(shapekey.name, keyValue)) | |
shapekey.value = keyValue | |
obj.shape_key_add(from_mix=True) | |
for shapekeyName in reversed(shapekeyNames): | |
deleteShapekeyByName(obj, shapekeyName) | |
# now delete all the other keys. Should be just one of them. | |
for shapekey in obj.data.shape_keys.key_blocks: | |
deleteShapekeyByName(obj, shapekey.name) | |
# _ _ _ _ _ | |
# | | | | | (_) | | | |
# ___ ___ | | | ___ ___| |_ _ _ __ __ _ _ __ ___ ___ ___| |__ ___ ___ | |
# / __/ _ \| | |/ _ \/ __| __| | '_ \ / _` | | '_ ` _ \ / _ \/ __| '_ \ / _ \/ __| | |
# | (_| (_) | | | __/ (__| |_| | | | | (_| | | | | | | | __/\__ \ | | | __/\__ \ | |
# \___\___/|_|_|\___|\___|\__|_|_| |_|\__, | |_| |_| |_|\___||___/_| |_|\___||___/ | |
# __/ | | |
# |___/ | |
dialogWasConfirmed = False | |
def setConfirmationState(): | |
dialogWasConfirmed = True | |
# if armatures are selected: get objects inside armature | |
def addSubmeshesToList(obj, list): | |
if obj.type == 'MESH' and obj.data.shape_keys is not None: | |
list.append(obj) | |
return | |
if not obj.type == 'ARMATURE': | |
return | |
for mesh in filter(lambda o: o.type == 'MESH' and o.parent == obj, bpy.data.objects): | |
list.append(mesh) | |
def collectMeshesWithShapekeys(): | |
meshesToProcess = [] | |
# if nothing is selected: apply to all meshes | |
if (not bpy.context.selected_objects == []): | |
for object in bpy.context.selected_objects: | |
if ignoreHidden and not object.visible_get(): | |
continue; | |
addSubmeshesToList(object, meshesToProcess) | |
return meshesToProcess | |
for mesh in filter(lambda obj: obj.type == 'MESH' and obj.data.shape_keys is not None, bpy.data.objects): | |
if ignoreHidden and not mesh.visible_get(): | |
continue; | |
meshesToProcess.append(mesh) | |
return meshesToProcess | |
# _ | |
# | | | |
# _____ ___ __ ___ _ __| |_ | |
# / _ \ \/ / '_ \ / _ \| '__| __| | |
# | __/> <| |_) | (_) | | | |_ | |
# \___/_/\_\ .__/ \___/|_| \__| | |
# | | | |
# |_| | |
meshesByArmatureName = {} | |
def positionForExportAndHide(armature): | |
# rotate it | |
rotation = armature.rotation_quaternion | |
if (isNoesisExport): | |
armature.rotation_quaternion.w = 0.0 | |
armature.rotation_quaternion.z = -1.0 | |
else: | |
armature.rotation_quaternion.w = 1.0 | |
armature.rotation_quaternion.z = 0.0 | |
# collect meshes | |
list = [] | |
addSubmeshesToList(armature, list) | |
meshesByArmatureName[armature.name] = list | |
for mesh in filter(lambda obj: re.match("^submesh_0|^submesh", obj.name), list): | |
mesh.hide_viewport = False | |
meshNumberMatch = re.sub('^submesh_0|^submesh', '', mesh.name) | |
meshNumberMatch = re.sub('(?<=\d)_.+$', '', meshNumberMatch) | |
if not meshNumberMatch and '0' != meshNumberMatch: | |
continue | |
if (isNoesisExport): | |
mesh.name = "submesh{}".format(meshNumberMatch) | |
else: | |
mesh.name = "submesh_{}_LOD_1".format(meshNumberMatch.zfill(2)) # prefix with leading zero | |
filepath = bpy.data.filepath | |
directory = os.path.dirname(filepath) | |
targetPath = '' | |
def export(armature): | |
bpy.ops.object.select_all(action='DESELECT') | |
armature.select_set(True) | |
numMeshes = 0 | |
armatureMeshes = meshesByArmatureName[armature.name] or [] | |
for mesh in armatureMeshes: | |
numMeshes = numMeshes + 1 | |
mesh.select_set(True) | |
extension = 'fbx' if isNoesisExport else 'glb' | |
targetPath = os.path.join(directory , armature.name + "." + extension) | |
print('exporting {} with {} meshes to {}'.format(armature.name, numMeshes, targetPath)) | |
if isNoesisExport: | |
bpy.ops.export_scene.fbx( | |
filepath=targetPath, | |
check_existing=False, | |
filter_glob='*.fbx', | |
use_selection=True, | |
use_visible=False | |
) | |
else: | |
bpy.ops.export_scene.gltf( | |
filepath=targetPath, | |
check_existing=False, | |
export_format='GLB', | |
export_texcoords=True, | |
export_normals=True, | |
export_tangents=True, | |
use_selection=True, | |
use_visible=False | |
) | |
# _ | |
# | | | |
# _____ _____ ___ _ _| |_ ___ | |
# / _ \ \/ / _ \/ __| | | | __/ _ \ | |
# | __/> < __/ (__| |_| | || __/ | |
# \___/_/\_\___|\___|\__,_|\__\___| | |
# switch to object mode | |
try: | |
bpy.ops.object.mode_set(mode='OBJECT') | |
except: | |
print('couldn\'t switch viewport to object mode') | |
# iterate and apply shapekeys. filter out meshes that don't have any | |
for mesh in collectMeshesWithShapekeys(): | |
applyShapekeys(mesh) | |
if autoExport: | |
if None == bpy.data.filepath or '' == bpy.data.filepath: | |
raise Exception("You need to call the export script from a blend file inside the export folder!") | |
for armature in filter(lambda obj: obj.type == 'ARMATURE', bpy.data.objects): | |
if ignoreHidden and not armature.visible_get(): | |
continue; | |
print("positioning {}".format(armature.name)) | |
positionForExportAndHide(armature) | |
for armature in filter(lambda obj: obj.type == 'ARMATURE', bpy.data.objects): | |
if ignoreHidden and not armature.visible_get(): | |
continue; | |
print("exporting {}".format(armature.name)) | |
export(armature) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
update: it can now process armatures, meshes (multiple ones, too) or everything in your blend file