Add faceFollow, fix face contour rendering, add default eyes/ears
Three changesets bundled together: - faceFollow (Camera/Neck) for the ARKit-blendshape face: it can now rotate with the mapped Body_Neck joint's own local axes instead of always billboarding toward the camera. - Fixed a real correctness bug: the face was rendering as isolated dots. Verified against OpenPose's own published keypoint diagrams and source (FACE_PAIRS_RENDER_GPU, 63 pairs) that it should render connected jaw/eyebrow/nose/eye/mouth contour lines; full_limb_data() was also silently dropping face limbs entirely. Both fixed. - Default eyes/ears: most rigs (HumanIK included) have no REye/LEye/REar/LEar joints, only Head/Nose, leaving those 4 slots permanently invisible. They now get a synthetic head-relative position when unmapped but Body_Nose is mapped, anchored at and rotating with the Nose joint's own orientation (not the camera) -- toggleable via useDefaultEyesEars, always overridden by an explicit joint mapping. Also drops the RShoulder->REar / LShoulder->LEar lines from BODY_25 and COCO connectivity by request (a detection-robustness quirk of OpenPose's original network, not real anatomy) so ears stay leaf points -- a disclosed, deliberate deviation from upstream's otherwise-verbatim connectivity. All verified against real Maya (2022/2023/2024), not just logic: rotation math checked exact, explicit-mapping-overrides-fallback checked, shoulder-ear removal checked against full_limb_data() output, rendered test scenes visually confirmed against OpenPose's own reference diagrams. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,8 @@ aBodyModel = None
|
||||
aFaceSource = None
|
||||
aBlendshapeWeight = None
|
||||
aFaceScale = None
|
||||
aFaceFollow = None
|
||||
aUseDefaultEyesEars = None
|
||||
|
||||
# aBodyModel enum field values, in registration order (index 0 = BODY_25).
|
||||
BODY_MODEL_FIELDS = [constants.BODY_MODEL_BODY_25, constants.BODY_MODEL_COCO]
|
||||
@@ -39,6 +41,11 @@ FACE_SOURCE_JOINTS = "Joints"
|
||||
FACE_SOURCE_ARKIT_BLENDSHAPES = "ARKitBlendshapes"
|
||||
FACE_SOURCE_FIELDS = [FACE_SOURCE_JOINTS, FACE_SOURCE_ARKIT_BLENDSHAPES]
|
||||
|
||||
# aFaceFollow enum field values, in registration order (index 0 = Camera).
|
||||
FACE_FOLLOW_CAMERA = "Camera"
|
||||
FACE_FOLLOW_NECK = "Neck"
|
||||
FACE_FOLLOW_FIELDS = [FACE_FOLLOW_CAMERA, FACE_FOLLOW_NECK]
|
||||
|
||||
DEFAULT_FACE_SCALE = 15.0
|
||||
|
||||
|
||||
@@ -65,7 +72,8 @@ class OpenposeCharacterNode(omui.MPxLocatorNode):
|
||||
def initialize():
|
||||
global aTargetJoint, aEnableDisplay, aJointRadiusScale
|
||||
global aLimbThicknessScale, aCharacterName, aBodyModel
|
||||
global aFaceSource, aBlendshapeWeight, aFaceScale
|
||||
global aFaceSource, aBlendshapeWeight, aFaceScale, aFaceFollow
|
||||
global aUseDefaultEyesEars
|
||||
|
||||
msg_attr = om.MFnMessageAttribute()
|
||||
aTargetJoint = msg_attr.create("targetJoint", "tj")
|
||||
@@ -137,6 +145,20 @@ class OpenposeCharacterNode(omui.MPxLocatorNode):
|
||||
face_scale_attr.setMin(0.0)
|
||||
om.MPxNode.addAttribute(aFaceScale)
|
||||
|
||||
face_follow_attr = om.MFnEnumAttribute()
|
||||
aFaceFollow = face_follow_attr.create("faceFollow", "ffol", 0)
|
||||
for field_index, field_name in enumerate(FACE_FOLLOW_FIELDS):
|
||||
face_follow_attr.addField(field_name, field_index)
|
||||
face_follow_attr.keyable = True
|
||||
om.MPxNode.addAttribute(aFaceFollow)
|
||||
|
||||
default_eyes_ears_attr = om.MFnNumericAttribute()
|
||||
aUseDefaultEyesEars = default_eyes_ears_attr.create(
|
||||
"useDefaultEyesEars", "udee", om.MFnNumericData.kBoolean, True
|
||||
)
|
||||
default_eyes_ears_attr.keyable = True
|
||||
om.MPxNode.addAttribute(aUseDefaultEyesEars)
|
||||
|
||||
|
||||
def get_mapped_joint_paths(dep_node_fn):
|
||||
"""Return {keypoint_index: MDagPath} for every connected targetJoint slot.
|
||||
@@ -202,6 +224,24 @@ def get_face_scale(dep_node_fn):
|
||||
return DEFAULT_FACE_SCALE
|
||||
|
||||
|
||||
def get_face_follow(dep_node_fn):
|
||||
"""Return FACE_FOLLOW_CAMERA / FACE_FOLLOW_NECK for this character."""
|
||||
try:
|
||||
field_index = dep_node_fn.findPlug(aFaceFollow, False).asShort()
|
||||
except RuntimeError:
|
||||
field_index = 0
|
||||
if 0 <= field_index < len(FACE_FOLLOW_FIELDS):
|
||||
return FACE_FOLLOW_FIELDS[field_index]
|
||||
return FACE_FOLLOW_CAMERA
|
||||
|
||||
|
||||
def get_use_default_eyes_ears(dep_node_fn):
|
||||
try:
|
||||
return dep_node_fn.findPlug(aUseDefaultEyesEars, False).asBool()
|
||||
except RuntimeError:
|
||||
return True
|
||||
|
||||
|
||||
def iter_character_nodes():
|
||||
"""Yield an MFnDependencyNode for every openposeCharacter node in the scene."""
|
||||
it = om.MItDependencyNodes(om.MFn.kPluginLocatorNode)
|
||||
|
||||
Reference in New Issue
Block a user