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:
@@ -93,6 +93,34 @@ class MappingEditor(QtWidgets.QDialog):
|
||||
self.face_scale_spin.valueChanged.connect(self._on_face_scale_changed)
|
||||
face_row.addWidget(self.face_scale_spin)
|
||||
|
||||
face_row.addWidget(QtWidgets.QLabel("Face Follow:"))
|
||||
self.face_follow_combo = QtWidgets.QComboBox()
|
||||
self.face_follow_combo.addItems(
|
||||
[mapping_node.FACE_FOLLOW_CAMERA, mapping_node.FACE_FOLLOW_NECK]
|
||||
)
|
||||
self.face_follow_combo.setToolTip(
|
||||
"Camera: the ARKit-blendshape face always faces whichever camera is drawing\n"
|
||||
"(viewport or render camera) -- works regardless of rig conventions.\n"
|
||||
"Neck: the face plane rotates with the mapped Body_Neck joint's own local\n"
|
||||
"X/Y axes instead -- turns with the character's head, but is rig-dependent\n"
|
||||
"(assumes local X = right, local Y = up; adjust the neck joint's rotate axis\n"
|
||||
"if the face looks rotated). Falls back to Camera if no Neck is mapped."
|
||||
)
|
||||
self.face_follow_combo.currentIndexChanged.connect(self._on_face_follow_changed)
|
||||
face_row.addWidget(self.face_follow_combo)
|
||||
|
||||
self.default_eyes_ears_check = QtWidgets.QCheckBox("Default Eyes/Ears")
|
||||
self.default_eyes_ears_check.setToolTip(
|
||||
"Most rigs (including HumanIK) have no REye/LEye/REar/LEar joints. When on\n"
|
||||
"(default), those 4 OpenPose_full slots get a synthetic head-relative position\n"
|
||||
"instead of being invisible, whenever they're unmapped but Body_Nose is mapped\n"
|
||||
"-- anchored at and rotating with the joint mapped to Nose (sized by Face Scale\n"
|
||||
"above). An explicit joint mapped to any of the 4 slots always takes priority\n"
|
||||
"over this fallback."
|
||||
)
|
||||
self.default_eyes_ears_check.toggled.connect(self._on_use_default_eyes_ears_changed)
|
||||
face_row.addWidget(self.default_eyes_ears_check)
|
||||
|
||||
autoconnect_btn = QtWidgets.QPushButton("Auto-Connect ARKit Blendshapes from Selected")
|
||||
autoconnect_btn.setToolTip(
|
||||
"Connects the selected node's attributes to this character's 52 blendshapeWeight\n"
|
||||
@@ -181,13 +209,18 @@ class MappingEditor(QtWidgets.QDialog):
|
||||
self.body_model_combo.setEnabled(False)
|
||||
self.body_model_combo.blockSignals(False)
|
||||
|
||||
for widget in (self.face_source_combo, self.face_scale_spin):
|
||||
face_widgets = (
|
||||
self.face_source_combo, self.face_scale_spin, self.face_follow_combo, self.default_eyes_ears_check,
|
||||
)
|
||||
for widget in face_widgets:
|
||||
widget.blockSignals(True)
|
||||
if character:
|
||||
self.face_source_combo.setEnabled(True)
|
||||
self.face_scale_spin.setEnabled(True)
|
||||
for widget in face_widgets:
|
||||
widget.setEnabled(True)
|
||||
self.face_source_combo.setCurrentIndex(cmds.getAttr(character + ".faceSource"))
|
||||
self.face_scale_spin.setValue(cmds.getAttr(character + ".faceScale"))
|
||||
self.face_follow_combo.setCurrentIndex(cmds.getAttr(character + ".faceFollow"))
|
||||
self.default_eyes_ears_check.setChecked(cmds.getAttr(character + ".useDefaultEyesEars"))
|
||||
connected = sum(
|
||||
1 for i in range(face_blendshapes.BLENDSHAPE_COUNT)
|
||||
if cmds.listConnections("{0}.blendshapeWeight[{1}]".format(character, i), source=True, destination=False)
|
||||
@@ -196,10 +229,10 @@ class MappingEditor(QtWidgets.QDialog):
|
||||
"{0} / {1} blendshapes connected".format(connected, face_blendshapes.BLENDSHAPE_COUNT)
|
||||
)
|
||||
else:
|
||||
self.face_source_combo.setEnabled(False)
|
||||
self.face_scale_spin.setEnabled(False)
|
||||
for widget in face_widgets:
|
||||
widget.setEnabled(False)
|
||||
self.blendshape_status_label.setText("")
|
||||
for widget in (self.face_source_combo, self.face_scale_spin):
|
||||
for widget in face_widgets:
|
||||
widget.blockSignals(False)
|
||||
|
||||
def _on_body_model_changed(self, index):
|
||||
@@ -212,11 +245,21 @@ class MappingEditor(QtWidgets.QDialog):
|
||||
if character:
|
||||
cmds.setAttr(character + ".faceSource", index)
|
||||
|
||||
def _on_face_follow_changed(self, index):
|
||||
character = self._current_character()
|
||||
if character:
|
||||
cmds.setAttr(character + ".faceFollow", index)
|
||||
|
||||
def _on_face_scale_changed(self, value):
|
||||
character = self._current_character()
|
||||
if character:
|
||||
cmds.setAttr(character + ".faceScale", value)
|
||||
|
||||
def _on_use_default_eyes_ears_changed(self, checked):
|
||||
character = self._current_character()
|
||||
if character:
|
||||
cmds.setAttr(character + ".useDefaultEyesEars", checked)
|
||||
|
||||
def _on_autoconnect_blendshapes(self):
|
||||
character = self._current_character()
|
||||
if not character:
|
||||
|
||||
Reference in New Issue
Block a user