e2f2e6668a
Maya API 2.0 plugin that maps a skeleton to OpenPose_full keypoints (BODY_25 or COCO body, 21+21 hand, 70 face) via an openposeCharacter node, with a live Viewport 2.0 draw override, an "OpenPose" viewport renderer plus openposeRenderSequence for PNG output, and a PySide2 mapping editor. Face can also be driven procedurally from ARKit's 52 blendshape weights instead of facial joints, for rigs with no facial skeleton. Includes VS Code debug configs and Maya test scenes/renders. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
229 lines
7.8 KiB
Python
229 lines
7.8 KiB
Python
"""``openposeCharacter`` node: per-character OpenPose_full joint mapping.
|
|
|
|
A pure MPxLocatorNode with one connectable message-attribute slot per
|
|
OpenPose_full keypoint (``targetJoint[0..136]``). The mapping lives entirely
|
|
as Maya connections (joint.message -> openposeCharacter.targetJoint[i]) so it
|
|
survives joint renames/reparenting and round-trips through the scene file
|
|
with no separate config to keep in sync. The node has no compute() output --
|
|
it is a passive connection target read directly by the draw override
|
|
(live viewport skeleton) and the render override (OpenPose image output).
|
|
"""
|
|
|
|
import maya.api.OpenMaya as om
|
|
import maya.api.OpenMayaUI as omui
|
|
|
|
from . import constants
|
|
from . import face_blendshapes
|
|
|
|
NODE_NAME = "openposeCharacter"
|
|
NODE_ID = om.MTypeId(0x0012F5C0)
|
|
DRAW_CLASSIFICATION = "drawdb/geometry/openposeCharacter"
|
|
DRAW_REGISTRANT_ID = "openposeRendererPlugin"
|
|
|
|
# Attribute handles, filled in by initialize()
|
|
aTargetJoint = None
|
|
aEnableDisplay = None
|
|
aJointRadiusScale = None
|
|
aLimbThicknessScale = None
|
|
aCharacterName = None
|
|
aBodyModel = None
|
|
aFaceSource = None
|
|
aBlendshapeWeight = None
|
|
aFaceScale = None
|
|
|
|
# aBodyModel enum field values, in registration order (index 0 = BODY_25).
|
|
BODY_MODEL_FIELDS = [constants.BODY_MODEL_BODY_25, constants.BODY_MODEL_COCO]
|
|
|
|
# aFaceSource enum field values, in registration order (index 0 = Joints).
|
|
FACE_SOURCE_JOINTS = "Joints"
|
|
FACE_SOURCE_ARKIT_BLENDSHAPES = "ARKitBlendshapes"
|
|
FACE_SOURCE_FIELDS = [FACE_SOURCE_JOINTS, FACE_SOURCE_ARKIT_BLENDSHAPES]
|
|
|
|
DEFAULT_FACE_SCALE = 15.0
|
|
|
|
|
|
class OpenposeCharacterNode(omui.MPxLocatorNode):
|
|
|
|
kTypeName = NODE_NAME
|
|
kTypeId = NODE_ID
|
|
|
|
def __init__(self):
|
|
omui.MPxLocatorNode.__init__(self)
|
|
|
|
def compute(self, plug, data_block):
|
|
# Pure passive mapping node: nothing to compute.
|
|
return None
|
|
|
|
def isBounded(self):
|
|
return False
|
|
|
|
@staticmethod
|
|
def creator():
|
|
return OpenposeCharacterNode()
|
|
|
|
@staticmethod
|
|
def initialize():
|
|
global aTargetJoint, aEnableDisplay, aJointRadiusScale
|
|
global aLimbThicknessScale, aCharacterName, aBodyModel
|
|
global aFaceSource, aBlendshapeWeight, aFaceScale
|
|
|
|
msg_attr = om.MFnMessageAttribute()
|
|
aTargetJoint = msg_attr.create("targetJoint", "tj")
|
|
msg_attr.array = True
|
|
# indexMatters already defaults to True (sparse, index-preserving
|
|
# array) -- explicitly assigning True here throws kInvalidParameter
|
|
# in Maya's Python binding, so it is deliberately left untouched.
|
|
msg_attr.storable = True
|
|
msg_attr.readable = True
|
|
msg_attr.writable = True
|
|
om.MPxNode.addAttribute(aTargetJoint)
|
|
|
|
num_attr = om.MFnNumericAttribute()
|
|
|
|
aEnableDisplay = num_attr.create(
|
|
"enableDisplay", "ed", om.MFnNumericData.kBoolean, True
|
|
)
|
|
num_attr.keyable = True
|
|
om.MPxNode.addAttribute(aEnableDisplay)
|
|
|
|
aJointRadiusScale = num_attr.create(
|
|
"jointRadiusScale", "jrs", om.MFnNumericData.kFloat, 1.0
|
|
)
|
|
num_attr.keyable = True
|
|
num_attr.setMin(0.0)
|
|
om.MPxNode.addAttribute(aJointRadiusScale)
|
|
|
|
aLimbThicknessScale = num_attr.create(
|
|
"limbThicknessScale", "lts", om.MFnNumericData.kFloat, 1.0
|
|
)
|
|
num_attr.keyable = True
|
|
num_attr.setMin(0.0)
|
|
om.MPxNode.addAttribute(aLimbThicknessScale)
|
|
|
|
str_data = om.MFnStringData()
|
|
empty_string_obj = str_data.create("")
|
|
typed_attr = om.MFnTypedAttribute()
|
|
aCharacterName = typed_attr.create(
|
|
"characterName", "cn", om.MFnData.kString, empty_string_obj
|
|
)
|
|
typed_attr.storable = True
|
|
om.MPxNode.addAttribute(aCharacterName)
|
|
|
|
enum_attr = om.MFnEnumAttribute()
|
|
aBodyModel = enum_attr.create("bodyModel", "bmd", 0)
|
|
for field_index, field_name in enumerate(BODY_MODEL_FIELDS):
|
|
enum_attr.addField(field_name, field_index)
|
|
enum_attr.keyable = True
|
|
om.MPxNode.addAttribute(aBodyModel)
|
|
|
|
face_source_attr = om.MFnEnumAttribute()
|
|
aFaceSource = face_source_attr.create("faceSource", "fsrc", 0)
|
|
for field_index, field_name in enumerate(FACE_SOURCE_FIELDS):
|
|
face_source_attr.addField(field_name, field_index)
|
|
face_source_attr.keyable = True
|
|
om.MPxNode.addAttribute(aFaceSource)
|
|
|
|
weight_attr = om.MFnNumericAttribute()
|
|
aBlendshapeWeight = weight_attr.create("blendshapeWeight", "bsw", om.MFnNumericData.kFloat, 0.0)
|
|
weight_attr.array = True
|
|
weight_attr.keyable = True
|
|
weight_attr.setMin(0.0)
|
|
weight_attr.setMax(1.0)
|
|
om.MPxNode.addAttribute(aBlendshapeWeight)
|
|
|
|
face_scale_attr = om.MFnNumericAttribute()
|
|
aFaceScale = face_scale_attr.create("faceScale", "fsc", om.MFnNumericData.kFloat, DEFAULT_FACE_SCALE)
|
|
face_scale_attr.keyable = True
|
|
face_scale_attr.setMin(0.0)
|
|
om.MPxNode.addAttribute(aFaceScale)
|
|
|
|
|
|
def get_mapped_joint_paths(dep_node_fn):
|
|
"""Return {keypoint_index: MDagPath} for every connected targetJoint slot.
|
|
|
|
``dep_node_fn`` is an MFnDependencyNode wrapping an openposeCharacter.
|
|
Unmapped keypoint indices are simply absent from the returned dict.
|
|
"""
|
|
result = {}
|
|
plug = dep_node_fn.findPlug(aTargetJoint, False)
|
|
for logical_index in plug.getExistingArrayAttributeIndices():
|
|
if logical_index < 0 or logical_index >= constants.TOTAL_KEYPOINTS:
|
|
continue
|
|
element_plug = plug.elementByLogicalIndex(logical_index)
|
|
if not element_plug.isConnected:
|
|
continue
|
|
source_plug = element_plug.source()
|
|
if source_plug.isNull:
|
|
continue
|
|
source_node = source_plug.node()
|
|
if not source_node.hasFn(om.MFn.kDagNode):
|
|
continue
|
|
dag_fn = om.MFnDagNode(source_node)
|
|
result[logical_index] = dag_fn.getPath()
|
|
return result
|
|
|
|
|
|
def get_body_model(dep_node_fn):
|
|
"""Return constants.BODY_MODEL_BODY_25 / BODY_MODEL_COCO for this character."""
|
|
try:
|
|
field_index = dep_node_fn.findPlug(aBodyModel, False).asShort()
|
|
except RuntimeError:
|
|
field_index = 0
|
|
if 0 <= field_index < len(BODY_MODEL_FIELDS):
|
|
return BODY_MODEL_FIELDS[field_index]
|
|
return constants.BODY_MODEL_BODY_25
|
|
|
|
|
|
def get_face_source(dep_node_fn):
|
|
"""Return FACE_SOURCE_JOINTS / FACE_SOURCE_ARKIT_BLENDSHAPES for this character."""
|
|
try:
|
|
field_index = dep_node_fn.findPlug(aFaceSource, False).asShort()
|
|
except RuntimeError:
|
|
field_index = 0
|
|
if 0 <= field_index < len(FACE_SOURCE_FIELDS):
|
|
return FACE_SOURCE_FIELDS[field_index]
|
|
return FACE_SOURCE_JOINTS
|
|
|
|
|
|
def get_blendshape_weights(dep_node_fn):
|
|
"""Return {arkit_blendshape_name: weight} for all 52 slots (0.0 default)."""
|
|
plug = dep_node_fn.findPlug(aBlendshapeWeight, False)
|
|
weights = {}
|
|
for index, name in enumerate(face_blendshapes.ARKIT_BLENDSHAPE_NAMES):
|
|
element_plug = plug.elementByLogicalIndex(index)
|
|
weights[name] = element_plug.asFloat()
|
|
return weights
|
|
|
|
|
|
def get_face_scale(dep_node_fn):
|
|
try:
|
|
return dep_node_fn.findPlug(aFaceScale, False).asFloat()
|
|
except RuntimeError:
|
|
return DEFAULT_FACE_SCALE
|
|
|
|
|
|
def iter_character_nodes():
|
|
"""Yield an MFnDependencyNode for every openposeCharacter node in the scene."""
|
|
it = om.MItDependencyNodes(om.MFn.kPluginLocatorNode)
|
|
while not it.isDone():
|
|
obj = it.thisNode()
|
|
fn = om.MFnDependencyNode(obj)
|
|
if fn.typeName == NODE_NAME:
|
|
yield fn
|
|
it.next()
|
|
|
|
|
|
def register(fn_plugin):
|
|
fn_plugin.registerNode(
|
|
NODE_NAME,
|
|
NODE_ID,
|
|
OpenposeCharacterNode.creator,
|
|
OpenposeCharacterNode.initialize,
|
|
om.MPxNode.kLocatorNode,
|
|
DRAW_CLASSIFICATION,
|
|
)
|
|
|
|
|
|
def deregister(fn_plugin):
|
|
fn_plugin.deregisterNode(NODE_ID)
|