Skip to content

Instantly share code, notes, and snippets.

@manavortex
Last active January 30, 2023 07:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save manavortex/eb9a8692988e862e3304fea0fb492d03 to your computer and use it in GitHub Desktop.
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
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)
@manavortex
Copy link
Author

update: it can now process armatures, meshes (multiple ones, too) or everything in your blend file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment