Files
PoseRenderer/python/openpose_renderer/draw_override.py
T
indigo 67b3deedbd 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>
2026-07-14 10:12:10 +08:00

171 lines
6.4 KiB
Python

"""Viewport 2.0 draw override for ``openposeCharacter``: a live, colored
skeleton overlay drawn in world space on top of the mapped joints, so a rig
can be visually QA'd against the OpenPose_full mapping without rendering.
This is independent of the render override (render_override.py) -- it always
draws (when the node's enableDisplay attr is on) in every viewport, in 3D
world space, and tracks animation live via prepareForDraw() being re-run
whenever the node is dirtied.
"""
import maya.api.OpenMaya as om
import maya.api.OpenMayaRender as omr
from . import constants
from . import face_blendshapes
from . import mapping_node
from . import projector
def _mcolor(rgb255):
return om.MColor((rgb255[0] / 255.0, rgb255[1] / 255.0, rgb255[2] / 255.0, 1.0))
def _fill_default_eye_ear_positions(dep_fn, mapped, positions):
"""Fill in synthetic REye/LEye/REar/LEar world positions (see
constants.DEFAULT_EYE_EAR_LOCAL_OFFSETS) for whichever of those slots
aren't already explicitly mapped, anchored at and oriented by the
joint mapped to Nose (the head) -- so they turn with the head's actual
rotation rather than facing the camera. No-op if Nose isn't mapped.
"""
missing_offsets = {
index: offset
for index, offset in constants.DEFAULT_EYE_EAR_LOCAL_OFFSETS.items()
if index not in positions
}
if not missing_offsets:
return
nose_index = constants.NAME_TO_INDEX["Body_Nose"]
if nose_index not in mapped:
return
anchor = positions[nose_index]
face_scale = mapping_node.get_face_scale(dep_fn)
computed = projector.default_head_relative_points(anchor, mapped[nose_index], face_scale, missing_offsets)
positions.update(computed)
class OpenPoseUserData(om.MUserData):
def __init__(self):
om.MUserData.__init__(self, False) # Maya should not delete after one draw
self.lines = [] # [(p0, p1, MColor, thickness), ...]
self.points = [] # [(p, MColor, pointSize), ...]
class OpenPoseDrawOverride(omr.MPxDrawOverride):
@staticmethod
def creator(obj):
return OpenPoseDrawOverride(obj)
def __init__(self, obj):
omr.MPxDrawOverride.__init__(self, obj, None, isAlwaysDirty=True)
def supportedDrawAPIs(self):
return omr.MRenderer.kAllDevices
def isBounded(self, obj_path, camera_path):
# Joints being displayed can be anywhere in the scene relative to
# this node's own (irrelevant) transform -- don't cull on bbox.
return False
def hasUIDrawables(self):
return True
def prepareForDraw(self, obj_path, camera_path, frame_context, old_data):
data = old_data if isinstance(old_data, OpenPoseUserData) else OpenPoseUserData()
data.lines = []
data.points = []
dep_fn = om.MFnDependencyNode(obj_path.node())
try:
enabled = dep_fn.findPlug(mapping_node.aEnableDisplay, False).asBool()
except RuntimeError:
enabled = True
if not enabled:
return data
radius_scale = dep_fn.findPlug(mapping_node.aJointRadiusScale, False).asFloat()
thickness_scale = dep_fn.findPlug(mapping_node.aLimbThicknessScale, False).asFloat()
body_model = mapping_node.get_body_model(dep_fn)
mapped = mapping_node.get_mapped_joint_paths(dep_fn)
positions = {index: projector.world_position(path) for index, path in mapped.items()}
if mapping_node.get_use_default_eyes_ears(dep_fn):
_fill_default_eye_ear_positions(dep_fn, mapped, positions)
if mapping_node.get_face_source(dep_fn) == mapping_node.FACE_SOURCE_ARKIT_BLENDSHAPES:
nose_index = constants.NAME_TO_INDEX["Body_Nose"]
anchor = positions.get(nose_index)
if anchor is not None:
weights = mapping_node.get_blendshape_weights(dep_fn)
face_scale = mapping_node.get_face_scale(dep_fn)
local_points = face_blendshapes.compute_face_local_points(weights)
neck_index = constants.NAME_TO_INDEX["Body_Neck"]
if mapping_node.get_face_follow(dep_fn) == mapping_node.FACE_FOLLOW_NECK and neck_index in mapped:
world_points = projector.oriented_world_points(
anchor, mapped[neck_index], local_points, face_scale
)
else:
world_points = projector.billboard_world_points(anchor, camera_path, local_points, face_scale)
for offset, world_point in enumerate(world_points):
positions[constants.FACE_START + offset] = world_point
active_indices = set(constants.active_point_indices(body_model))
pairs, colors, thickness_list = constants.full_limb_data(body_model)
for pair_i, (a, b) in enumerate(pairs):
if a in positions and b in positions:
thickness = thickness_list[pair_i] * thickness_scale
if thickness <= 0.0:
continue
data.lines.append((positions[a], positions[b], _mcolor(colors[pair_i]), thickness))
for index, pos in positions.items():
if index not in active_indices:
continue
radius = constants.KEYPOINT_POINT_RADIUS[index] * radius_scale
if radius <= 0.0:
continue
data.points.append((pos, _mcolor(constants.KEYPOINT_POINT_COLORS[index]), radius * 2.0))
return data
def addUIDrawables(self, obj_path, draw_manager, frame_context, data):
if not isinstance(data, OpenPoseUserData):
return
draw_manager.beginDrawable()
draw_manager.setDepthPriority(omr.MRenderItem.sActiveWireDepthPriority)
for p0, p1, color, thickness in data.lines:
draw_manager.setColor(color)
draw_manager.setLineWidth(thickness)
draw_manager.line(p0, p1)
for pos, color, point_size in data.points:
draw_manager.setColor(color)
draw_manager.setPointSize(point_size)
draw_manager.point(pos)
draw_manager.endDrawable()
def register(fn_plugin):
omr.MDrawRegistry.registerDrawOverrideCreator(
mapping_node.DRAW_CLASSIFICATION,
mapping_node.DRAW_REGISTRANT_ID,
OpenPoseDrawOverride.creator,
)
def deregister(fn_plugin):
omr.MDrawRegistry.deregisterDrawOverrideCreator(
mapping_node.DRAW_CLASSIFICATION,
mapping_node.DRAW_REGISTRANT_ID,
)