Banner image of the blog
0 0 分钟

基于Metahuman提取Apple ARKit 52Blendshape

2025-08-14 Maya 编辑
基于Metahuman提取Apple ARKit 52Blendshape

核心实现思路

  1. 先保存模型的基础表情(无任何动作)作为基准。
  2. 对于每个 ARKit 表情(如 “eyeBlinkLeft”),调整对应的控制器(如左眼皮控制器上移)。
  3. 复制此时的模型状态作为 “眨眼表情” 的目标形状。
  4. 将所有表情目标整合到一个混合形状节点中,使模型可以通过调整属性(如eyeBlinkLeft值为 1)触发对应表情。

Python代码

# -*- coding: utf-8 -*-

import maya.cmds as cmds
import traceback

# --- 全局常量 ---
UI_ID = "ARKitCreatorUI_Optimized"
ARkit_List = [
    "eyeBlinkLeft", "eyeLookDownLeft", "eyeLookInLeft", "eyeLookOutLeft", "eyeLookUpLeft",
    "eyeSquintLeft", "eyeWideLeft", "eyeBlinkRight", "eyeLookDownRight", "eyeLookInRight",
    "eyeLookOutRight", "eyeLookUpRight", "eyeSquintRight", "eyeWideRight", "jawForward",
    "jawLeft", "jawRight", "jawOpen", "mouthClose", "mouthFunnel", "mouthPucker",
    "mouthLeft", "mouthRight", "mouthSmileLeft", "mouthSmileRight", "mouthFrownLeft",
    "mouthFrownRight", "mouthDimpleLeft", "mouthDimpleRight", "mouthStretchLeft",
    "mouthStretchRight", "mouthRollLower", "mouthRollUpper", "mouthShrugLower",
    "mouthShrugUpper", "mouthPressLeft", "mouthPressRight", "mouthLowerDownLeft",
    "mouthLowerDownRight", "mouthUpperUpLeft", "mouthUpperUpRight", "browDownLeft",
    "browDownRight", "browInnerUp", "browOuterUpLeft", "browOuterUpRight", "cheekPuff",
    "cheekSquintLeft", "cheekSquintRight", "noseSneerLeft", "noseSneerRight", "tongueOut"
]

# --- 优化点 1: 将姿势定义为数据结构 ---
POSE_DEFINITIONS = [
    [("CTRL_L_eye_blink.translateY", 1)],                                    # 0: eyeBlinkLeft
    [("CTRL_L_eye.translateY", -1)],                                         # 1: eyeLookDownLeft
    [("CTRL_L_eye.translateX", -1)],                                         # 2: eyeLookInLeft
    [("CTRL_L_eye.translateX", 1)],                                          # 3: eyeLookOutLeft
    [("CTRL_L_eye.translateY", 1)],                                          # 4: eyeLookUpLeft
    [("CTRL_L_eye_squintInner.translateY", 1)],                              # 5: eyeSquintLeft
    [("CTRL_L_eye_blink.translateY", -1)],                                   # 6: eyeWideLeft
    [("CTRL_R_eye_blink.translateY", 1)],                                    # 7: eyeBlinkRight
    [("CTRL_R_eye.translateY", -1)],                                         # 8: eyeLookDownRight
    [("CTRL_R_eye.translateX", 1)],                                          # 9: eyeLookInRight
    [("CTRL_R_eye.translateX", -1)],                                         # 10: eyeLookOutRight
    [("CTRL_R_eye.translateY", 1)],                                          # 11: eyeLookUpRight
    [("CTRL_R_eye_squintInner.translateY", 1)],                              # 12: eyeSquintRight
    [("CTRL_R_eye_blink.translateY", -1)],                                   # 13: eyeWideRight
    [("CTRL_C_jaw_fwdBack.translateY", -1)],                                 # 14: jawForward
    [("CTRL_C_jaw.translateX", -1)],                                         # 15: jawLeft
    [("CTRL_C_jaw.translateX", 1)],                                          # 16: jawRight
    [("CTRL_C_jaw.translateY", 1)],                                          # 17: jawOpen
    [("CTRL_C_jaw.translateY", 1),("CTRL_R_mouth_lipsTogetherU.translateY", 1), ("CTRL_R_mouth_lipsTogetherD.translateY", 1), 
     ("CTRL_L_mouth_lipsTogetherU.translateY", 1), ("CTRL_L_mouth_lipsTogetherD.translateY", 1)], # 18: mouthClose
    [("CTRL_R_mouth_funnelU.translateY", 1), ("CTRL_L_mouth_funnelD.translateY", 1), 
     ("CTRL_R_mouth_funnelD.translateY", 1), ("CTRL_L_mouth_funnelU.translateY", 1)], # 19: mouthFunnel
    [("CTRL_L_mouth_funnelU.translateY", 0.75), ("CTRL_L_mouth_funnelD.translateY", 0.75), 
     ("CTRL_R_mouth_funnelU.translateY", 0.75), ("CTRL_R_mouth_funnelD.translateY", 0.75), 
     ("CTRL_L_mouth_purseU.translateY", 1), ("CTRL_L_mouth_purseD.translateY", 1), 
     ("CTRL_R_mouth_purseD.translateY", 1), ("CTRL_R_mouth_purseU.translateY", 1)], # 20: mouthPucker
    [("CTRL_C_mouth.translateX", 1)],                                        # 21: mouthLeft
    [("CTRL_C_mouth.translateX", -1)],                                       # 22: mouthRight
    [("CTRL_L_mouth_cornerPull.translateY", 1)],                             # 23: mouthSmileLeft
    [("CTRL_R_mouth_cornerPull.translateY", 1)],                             # 24: mouthSmileRight
    [("CTRL_L_mouth_cornerDepress.translateY", 1)],                          # 25: mouthFrownLeft
    [("CTRL_R_mouth_cornerDepress.translateY", 1)],                          # 26: mouthFrownRight
    [("CTRL_L_mouth_dimple.translateY", 1)],                                 # 27: mouthDimpleLeft
    [("CTRL_R_mouth_dimple.translateY", 1)],                                 # 28: mouthDimpleRight
    [("CTRL_L_mouth_stretch.translateY", 1)],                                # 29: mouthStretchLeft
    [("CTRL_R_mouth_stretch.translateY", 1)],                                # 30: mouthStretchRight
    [("CTRL_L_mouth_lipBiteD.translateY", 1), ("CTRL_R_mouth_lipBiteD.translateY", 1)], # 31: mouthRollLower
    [("CTRL_R_mouth_lipBiteU.translateY", 1), ("CTRL_L_mouth_lipBiteU.translateY", 1)], # 32: mouthRollUpper
    [("CTRL_L_jaw_ChinRaiseD.translateY", 1), ("CTRL_R_jaw_ChinRaiseD.translateY", 1)], # 33: mouthShrugLower
    [("CTRL_L_jaw_ChinRaiseU.translateY", 1), ("CTRL_R_jaw_ChinRaiseU.translateY", 1)], # 34: mouthShrugUpper
    [("CTRL_L_mouth_pressD.translateY", 1), ("CTRL_L_mouth_pressU.translateY", 1)], # 35: mouthPressLeft
    [("CTRL_R_mouth_pressU.translateY", 1), ("CTRL_R_mouth_pressD.translateY", 1)], # 36: mouthPressRight
    [("CTRL_L_mouth_lowerLipDepress.translateY", 1)],                        # 37: mouthLowerDownLeft
    [("CTRL_R_mouth_lowerLipDepress.translateY", 1)],                        # 38: mouthLowerDownRight
    [("CTRL_L_mouth_upperLipRaise.translateY", 1)],                          # 39: mouthUpperUpLeft
    [("CTRL_R_mouth_upperLipRaise.translateY", 1)],                          # 40: mouthUpperUpRight
    [("CTRL_L_brow_down.translateY", 1)],                                    # 41: browDownLeft
    [("CTRL_R_brow_down.translateY", 1)],                                    # 42: browDownRight
    [("CTRL_R_brow_raiseIn.translateY", 1), ("CTRL_L_brow_raiseIn.translateY", 1)], # 43: browInnerUp
    [("CTRL_L_brow_raiseOut.translateY", 1), ("CTRL_L_brow_raiseIn.translateY", 1)], # 44: browOuterUpLeft
    [("CTRL_R_brow_raiseIn.translateY", 1), ("CTRL_R_brow_raiseOut.translateY", 1)], # 45: browOuterUpRight
    [("CTRL_L_mouth_suckBlow.translateY", 1), ("CTRL_R_mouth_suckBlow.translateY", 1)], # 46: cheekPuff
    [("CTRL_L_eye_cheekRaise.translateY", 1)],                               # 47: cheekSquintLeft
    [("CTRL_R_eye_cheekRaise.translateY", 1)],                               # 48: cheekSquintRight
    [("CTRL_L_nose.translateY", 1)],                                         # 49: noseSneerLeft
    [("CTRL_R_nose.translateY", 1)],                                         # 50: noseSneerRight
    [("CTRL_C_tongue_move.translateY", -1), ("CTRL_C_tongue.translateY", -1), 
     ("CTRL_C_tongue_roll.translateY", -0.5), ("CTRL_C_tongue_inOut.translateY", -0.75)] # 51: tongueOut
]

class ARKitUI:
    def __init__(self):
        self.window = UI_ID
        self.title = "ARKit BlendShape Creator (Optimized)"
        self.size = (400, 450)
        
        if cmds.window(self.window, exists=True):
            cmds.deleteUI(self.window, window=True)
            
        self.window = cmds.window(self.window, title=self.title, widthHeight=self.size, sizeable=True)
        self.main_layout = cmds.columnLayout(adjustableColumn=True, rowSpacing=10, columnAttach=('both', 5))

        # --- 网格加载区域 ---
        cmds.frameLayout(label="1. 加载网格 (Load Meshes)", collapsable=False, marginWidth=5)
        cmds.columnLayout(adjustableColumn=True)
        self.mesh_list_widget = cmds.textScrollList(numberOfRows=8, allowMultiSelection=True)
        cmds.button(label="加载选中的网格 (Load Selected Meshes)", command=self.load_selected_meshes)
        cmds.setParent('..'); cmds.setParent('..')

        # --- 执行区域 ---
        cmds.frameLayout(label="2. 创建混合形状 (Create BlendShapes)", collapsable=False, marginWidth=5)
        cmds.columnLayout(adjustableColumn=True, rowSpacing=5)
        cmds.text(label=" ", height=10)
        self.progress_bar = cmds.progressBar(maxValue=len(ARkit_List), width=390)
        self.status_text = cmds.text(label="请先加载网格。", align="center")
        cmds.text(label=" ", height=10)
        cmds.button(label="开始创建ARKit混合形状 (Start ARKit Creation)", 
                    command=self.run_creation_process, 
                    backgroundColor=(0.357, 0.82, 0.533))
        cmds.setParent('..'); cmds.setParent('..')

        cmds.showWindow(self.window)

    def load_selected_meshes(self, *args):
        cmds.textScrollList(self.mesh_list_widget, edit=True, removeAll=True)
        selection = cmds.ls(selection=True, long=True)
        meshes = cmds.filterExpand(selection, selectionMask=12, expand=True) or []
        if not meshes:
            cmds.warning("所选物体中没有找到有效的多边形网格。")
            self.update_status("错误:未找到网格。")
            return
        for mesh in meshes:
            cmds.textScrollList(self.mesh_list_widget, edit=True, append=mesh)
        self.update_status("{} 个网格已加载。准备就绪。".format(len(meshes)))
        cmds.progressBar(self.progress_bar, edit=True, progress=0)

    def run_creation_process(self, *args):
        meshes_to_process = cmds.textScrollList(self.mesh_list_widget, query=True, allItems=True)
        if not meshes_to_process:
            cmds.warning("请先加载至少一个网格。")
            self.update_status("错误:没有加载网格。")
            return
        sg_create_arkit(meshes_to_process, self.update_status, self.progress_bar)

    def update_status(self, message):
        cmds.text(self.status_text, edit=True, label=message)

def sg_create_arkit(meshes, status_callback, progress_bar_widget):
    cmds.undoInfo(stateWithoutFlush=False)
    cmds.refresh(suspend=True)
    
    try:
        status_callback("开始创建...")
        cmds.progressBar(progress_bar_widget, edit=True, progress=0)

        sg_go_to_build_face_pose()

        status_callback("正在复制默认网格...")
        sg_copy_meshes("defaults", meshes)

        total_poses = len(ARkit_List)
        previous_pose_attrs = []
        for i, pose_name in enumerate(ARkit_List):
            status_callback("处理姿势: {} ({}/{})".format(pose_name, i + 1, total_poses))
            
            for attr, _ in previous_pose_attrs:
                safe_set_attr(attr, 0)
            
            current_pose_attrs = POSE_DEFINITIONS[i]
            for attr, value in current_pose_attrs:
                safe_set_attr(attr, value)
            previous_pose_attrs = current_pose_attrs

            sg_copy_meshes(pose_name, meshes)
            cmds.progressBar(progress_bar_widget, edit=True, step=1)

        status_callback("正在合并混合形状...")
        sg_merge_blendshape(ARkit_List, meshes)

        sg_go_to_build_face_pose()
        status_callback("成功!ARKit混合形状已创建。")

    except Exception as e:
        status_callback("错误: {}".format(e))
        traceback.print_exc()
    finally:
        cmds.undoInfo(stateWithoutFlush=True)
        cmds.refresh(suspend=False)
        cmds.refresh()

def sg_copy_meshes(name, meshes):
    if not cmds.objExists(name):
        cmds.group(empty=True, name=name)
        if name != "defaults":
            cmds.setAttr(name + ".visibility", 0)

    for mesh in meshes:
        if cmds.objExists(mesh):
            short_name = mesh.split('|')[-1]
            duplicate_name = "{}_{}".format(short_name, name)
            duplicated_mesh = cmds.duplicate(mesh, renameChildren=True, name=duplicate_name)[0]
            try:
                cmds.parent(duplicated_mesh, name)
            except RuntimeError as e:
                cmds.warning("无法将 '{}' 父级设置为 '{}': {}".format(duplicated_mesh, name, e))

def sg_merge_blendshape(arkit_list, base_meshes):
    for mesh in base_meshes:
        if cmds.objExists(mesh):
            short_name = mesh.split('|')[-1]
            targets = []
            for shape_name in arkit_list:
                duplicate_name = "{}_{}".format(short_name, shape_name)
                if cmds.objExists(duplicate_name):
                    targets.append(duplicate_name)
            
            default_mesh_transform = "{}_defaults".format(short_name)
            
            if cmds.objExists(default_mesh_transform) and targets:
                blend_shape_node = cmds.blendShape(targets, default_mesh_transform, name="{}_ARKit_BS".format(short_name))[0]
                for i, shape_name in enumerate(arkit_list):
                    if i < cmds.blendShape(blend_shape_node, query=True, weightCount=True):
                        cmds.aliasAttr(shape_name, "{}.w[{}]".format(blend_shape_node, i))

def safe_set_attr(attr, value):
    try:
        cmds.setAttr(attr, value)
    except (RuntimeError, ValueError):
        pass

def sg_go_to_build_face_pose():
    all_controllers = [
    "CTRL_C_jaw", "CTRL_R_eyelashes_tweakerOut", "CTRL_R_eyelashes_tweakerIn", "CTRL_L_eyelashes_tweakerOut", 
    "CTRL_L_eyelashes_tweakerIn", "CTRL_R_eye_eyelidU", "CTRL_L_eye_eyelidD", "CTRL_L_eye_eyelidU", 
    "CTRL_L_mouth_corner", "CTRL_R_mouth_corner", "CTRL_C_tongue_thickThin", "CTRL_R_eye_eyelidD", 
    "CTRL_C_tongue_bendTwist", "CTRL_C_tongue_tipMove", "CTRL_C_tongue_move", "CTRL_C_tongue", 
    "CTRL_C_tongue_wideNarrow", "CTRL_C_tongue_inOut", "CTRL_L_mouth_thicknessD", "CTRL_L_mouth_thicknessU", 
    "CTRL_R_mouth_thicknessU", "CTRL_R_mouth_thicknessInwardU", "CTRL_R_mouth_thicknessD", 
    "CTRL_L_mouth_thicknessInwardU", "CTRL_R_mouth_lipsRollU", "CTRL_R_mouth_thicknessInwardD", 
    "CTRL_L_mouth_thicknessInwardD", "CTRL_L_mouth_lipsRollU", "CTRL_L_mouth_lipsRollD", 
    "CTRL_R_mouth_lipsRollD", "CTRL_C_teethU", "CTRL_C_teethD", "CTRL_C_mouth_lipShiftD", 
    "CTRL_C_teeth_fwdBackU", "CTRL_C_teeth_fwdBackD", "CTRL_C_mouth_lipShiftU", "CTRL_R_eye_pupil", 
    "CTRL_L_nose", "CTRL_R_nose", "CTRL_C_mouth", "CTRL_R_mouth_suckBlow", "CTRL_L_mouth_suckBlow", 
    "CTRL_C_jaw_fwdBack", "CTRL_neck_digastricUpDown", "CTRL_neck_throatExhaleInhale", 
    "CTRL_neck_throatUpDown", "CTRL_L_mouth_pushPullU", "CTRL_R_mouth_pushPullU", "CTRL_L_mouth_pushPullD", 
    "CTRL_R_mouth_cornerSharpnessU", "CTRL_L_mouth_cornerSharpnessD", "CTRL_L_mouth_cornerSharpnessU", 
    "CTRL_R_mouth_pushPullD", "CTRL_R_mouth_cornerSharpnessD", "CTRL_L_eyeAim", "CTRL_C_eyesAim", 
    "CTRL_R_eyeAim", "CTRL_R_eye", "CTRL_L_eye", "CTRL_C_eye", "CTRL_L_eye_blink", "CTRL_L_eye_pupil", 
    "CTRL_R_eye_blink", "CTRL_R_mouth_lipsTowardsTeethD", "CTRL_L_mouth_lipsTowardsTeethU", 
    "CTRL_R_mouth_lipsTowardsTeethU", "CTRL_L_mouth_lipsTowardsTeethD", "CTRL_neckCorrectivesMultiplyerU", 
    "CTRL_neckCorrectivesMultiplyerM", "CTRL_neckCorrectivesMultiplyerD", "CTRL_R_mouth_lipBiteD", 
    "CTRL_R_mouth_stickyOuterU", "CTRL_C_mouth_stickyU", "CTRL_R_mouth_stickyInnerU", 
    "CTRL_L_mouth_stickyInnerU", "CTRL_L_mouth_stickyOuterU", "CTRL_C_mouth_stickyD", 
    "CTRL_L_mouth_stickyInnerD", "CTRL_R_mouth_stickyInnerD", "CTRL_R_mouth_stickyOuterD", 
    "CTRL_L_mouth_stickyOuterD", "CTRL_R_mouth_lipSticky", "CTRL_L_mouth_lipSticky", 
    "CTRL_C_tongue_press", "CTRL_C_tongue_roll", "CTRL_L_jaw_ChinRaiseU", "CTRL_L_jaw_ChinRaiseD", 
    "CTRL_R_jaw_ChinRaiseD", "CTRL_L_jaw_chinCompress", "CTRL_R_jaw_ChinRaiseU", "CTRL_R_jaw_chinCompress", 
    "CTRL_R_nose_nasolabialDeepen", "CTRL_L_nose_nasolabialDeepen", "CTRL_L_eye_faceScrunch", 
    "CTRL_R_eye_faceScrunch", "CTRL_C_eye_parallelLook", "CTRL_L_eye_lidPress", "CTRL_R_eye_lidPress", 
    "CTRL_L_ear_up", "CTRL_R_ear_up", "CTRL_L_nose_wrinkleUpper", "CTRL_R_nose_wrinkleUpper", 
    "CTRL_R_mouth_upperLipRaise", "CTRL_L_mouth_upperLipRaise", "CTRL_L_mouth_sharpCornerPull", 
    "CTRL_R_mouth_cornerPull", "CTRL_R_mouth_sharpCornerPull", "CTRL_L_mouth_cornerPull", 
    "CTRL_L_mouth_dimple", "CTRL_R_mouth_dimple", "CTRL_L_mouth_stretch", "CTRL_R_mouth_lowerLipDepress", 
    "CTRL_L_mouth_lowerLipDepress", "CTRL_L_mouth_cornerDepress", "CTRL_R_mouth_cornerDepress", 
    "CTRL_R_mouth_stretch", "CTRL_R_mouth_stretchLipsClose", "CTRL_L_mouth_stretchLipsClose", 
    "CTRL_R_mouth_funnelD", "CTRL_L_mouth_lipsTogetherU", "CTRL_L_mouth_pressU", "CTRL_R_mouth_pressU", 
    "CTRL_L_mouth_lipsTogetherD", "CTRL_R_mouth_lipsTogetherD", "CTRL_R_mouth_lipsTogetherU", 
    "CTRL_L_mouth_pressD", "CTRL_R_mouth_pressD", "CTRL_R_mouth_lipsBlow", "CTRL_R_mouth_tightenU", 
    "CTRL_L_mouth_tightenU", "CTRL_L_mouth_lipsBlow", "CTRL_L_mouth_lipsPressU", "CTRL_L_mouth_tightenD", 
    "CTRL_R_mouth_tightenD", "CTRL_R_mouth_lipsPressU", "CTRL_L_mouth_lipBiteU", "CTRL_R_mouth_lipBiteU", 
    "CTRL_L_jaw_clench", "CTRL_R_jaw_clench", "CTRL_C_jaw_openExtreme", "CTRL_L_neck_stretch", 
    "CTRL_R_neck_stretch", "CTRL_C_neck_swallow", "CTRL_L_neck_mastoidContract", 
    "CTRL_R_neck_mastoidContract", "CTRL_lookAtSwitch", "CTRL_eyesAimFollowHead", 
    "CTRL_faceGUIfollowHead", "CTRL_L_mouth_purseD", "CTRL_R_mouth_purseD", "CTRL_L_mouth_purseU", 
    "CTRL_R_mouth_purseU", "CTRL_L_mouth_towardsU", "CTRL_L_mouth_towardsD", "CTRL_R_mouth_towardsU", 
    "CTRL_R_mouth_towardsD", "CTRL_L_mouth_funnelD", "CTRL_L_mouth_funnelU", "CTRL_R_mouth_funnelU", 
    "CTRL_convergenceSwitch", "CTRL_R_brow_raiseIn", "CTRL_L_brow_raiseIn", "CTRL_L_brow_raiseOut", 
    "CTRL_L_brow_down", "CTRL_R_brow_down", "CTRL_R_brow_raiseOut", "CTRL_L_brow_lateral", 
    "CTRL_R_brow_lateral", "CTRL_L_eye_squintInner", "CTRL_R_eye_squintInner", "CTRL_R_eye_cheekRaise", 
    "CTRL_L_eye_cheekRaise"
    ]
    for ctrl in all_controllers:
        safe_set_attr(ctrl + ".translateX", 0)
        safe_set_attr(ctrl + ".translateY", 0)

# --- 脚本入口点 ---
arkit_tool_ui = ARKitUI()

使用方法

  1. 准备场景
  • 虽然脚本会先让控制器回到初始状态,但是还是建议,手动将所有控制器都归零
  • 在Maya中打开正常使用的metahuman绑定的文件。请确保场景中存在脚本所需的表情控制器(其命名应为 CTRL_ 开头)。
  1. 启动工具
  • 打开Maya的 脚本编辑器(Script Editor),创建一个新的 Python 选项卡,将工具的完整代码粘贴进去,然后执行脚本。
  1. 如何使用
  • 第一步: 在Maya视窗或大纲视图中,选择所有您希望生成混合形状的网格模型(例如头部、眼睛、牙齿等)。
  • 第二步: 点击界面中的 【加载选中的网格 (Load Selected Meshes)】 按钮,网格列表会出现在UI中。
  • 第三步: 点击绿色的 【开始创建ARKit混合形状 (Start ARKit Creation)】 按钮。
  1. 完成
  • 脚本将自动执行所有步骤,进度条会实时更新。任务完成后,状态栏会提示成功。此时,混合形状已在 _defaults 后缀的网格上创建完毕。