fix(pipeline): stand the character up before scaling; reach bones by role
Fixes the two root causes behind four of the seven reported breakages, and rejects the source that cannot be fixed. STAND UP FIRST. flatten_and_scale() now derives the up axis from the skeleton and rotates the model upright before measuring anything. Aria and Momo are correct: 1.75 m tall, 1.43 x 0.41 and 1.52 x 0.39 across, verified by render. The up vector is measured from the FEET to the HIPS, not from the hips to the head. The head is not a reliable landmark — the spine walk ends on the last non-cosmetic bone in the chain, which on a rig with a facial skeleton can sit BELOW the hips. Momo's did, so the first cut of this fix stood her neatly on her head: right size, right proportions, upside down. Feet cannot be mistaken. REACH BONES BY ROLE. SkinnedPlayerModel gained _role_bone(), and set_weapon uses it. Four characters could not hold a gun because one hardcoded lookup knew three spellings and their hands are called "Right wrist" and "J_Bip_R_Hand" — both resolved perfectly in the sidecar the whole time. HIKARI IS REJECTED. She now fails the gate: her feet and spine disagree about which way is up, so the stand-up correction cannot resolve her either, on top of zero-length cosmetic bones and a second armature that was smuggling its own clips into the export. That is not a tuning problem, it is a file that has been through two toolchains. De-registered and removed rather than shipped broken — which is what the gate is for. Six GLB skins remain, all passing. Smoke 0 failures, 11/11 movement tests. Still open, recorded in the skill: kiyoko faces backwards, miku's grown hair stretches under animation, the mannequin's rifle hold does not convince, and taila's front skirt clipping. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2b3e29dd30
commit
27c4117c25
+52
-2
@@ -335,7 +335,7 @@ def rebuild_hierarchy(arm, roles):
|
||||
# ----------------------------------------------------------------- normalize
|
||||
|
||||
|
||||
def flatten_and_scale(arm, meshes, target_height):
|
||||
def flatten_and_scale(arm, meshes, target_height, roles=None):
|
||||
"""Bake the import hierarchy away and set the character's real-world size.
|
||||
|
||||
Sketchfab wraps everything in scaled/rotated empties. Left in place they
|
||||
@@ -363,6 +363,56 @@ def flatten_and_scale(arm, meshes, target_height):
|
||||
|
||||
apply_all()
|
||||
|
||||
# STAND THE CHARACTER UP before measuring anything.
|
||||
#
|
||||
# The scale below normalises the bounding box along Z because Z is up in
|
||||
# Blender. For a model that arrives lying along another axis that measures
|
||||
# the character's THICKNESS — about 0.25 m — so it gets scaled by ~7 and left
|
||||
# on its back. One assumption, both symptoms, and invisible afterwards
|
||||
# because the exporter maps Blender Z to glTF Y: "is the height 1.75" comes
|
||||
# out true on a character who is 7.5 m tall lying down.
|
||||
#
|
||||
# Which way is up is not a convention to assume, it is a property of the
|
||||
# skeleton: the head is above the hips. Snapped to the nearest axis rather
|
||||
# than aligned exactly, so a character with a slight lean in their rest pose
|
||||
# is stood up, not straightened.
|
||||
# Measured from the FEET to the HIPS, not from the hips to the head.
|
||||
#
|
||||
# The head is not a reliable landmark. The spine walk ends on whatever the
|
||||
# last non-cosmetic bone in the chain is, and on a rig with a facial skeleton
|
||||
# that can be a bone sitting BELOW the hips — which points this vector
|
||||
# downwards and stands the character neatly on her head. Momo did exactly
|
||||
# that. Feet cannot be mistaken: they are the bottom of a standing character
|
||||
# on every rig, and foot.L/R resolve on every source met so far.
|
||||
if roles is not None and roles.hips:
|
||||
hips_b = arm.data.bones.get(roles.hips)
|
||||
feet = [arm.data.bones.get(roles.limb.get(("foot", s), ""))
|
||||
for s in ("L", "R")]
|
||||
feet = [f for f in feet if f]
|
||||
ref = None
|
||||
if feet:
|
||||
ref = sum((f.head_local for f in feet), Vector()) / len(feet)
|
||||
elif roles.head and arm.data.bones.get(roles.head):
|
||||
# No feet — fall back to the head, and accept the risk above.
|
||||
ref = hips_b.head_local - (
|
||||
arm.data.bones[roles.head].head_local - hips_b.head_local)
|
||||
if hips_b and ref is not None:
|
||||
d = hips_b.head_local - ref
|
||||
k = max(range(3), key=lambda i: abs(d[i]))
|
||||
src = Vector((0.0, 0.0, 0.0))
|
||||
src[k] = 1.0 if d[k] > 0 else -1.0
|
||||
up = Vector((0.0, 0.0, 1.0))
|
||||
if src.dot(up) < 0.999:
|
||||
axis = src.cross(up)
|
||||
if axis.length < 1e-6:
|
||||
axis = Vector((1.0, 0.0, 0.0)) # upside down
|
||||
R = Matrix.Rotation(src.angle(up), 4, axis.normalized())
|
||||
for obj in [arm] + meshes:
|
||||
obj.matrix_world = R @ obj.matrix_world
|
||||
apply_all()
|
||||
print(f"Stood the character up: feet->hips ran along "
|
||||
f"{'XYZ'[k]}{'+' if d[k] > 0 else '-'}, rotated to Z+")
|
||||
|
||||
lo = Vector((1e9, 1e9, 1e9))
|
||||
hi = -lo.copy()
|
||||
for m in meshes:
|
||||
@@ -1047,7 +1097,7 @@ def main():
|
||||
rebuild_hierarchy(arm, roles)
|
||||
subdivide_cloth_panels(arm, meshes, roles)
|
||||
unbind_cloth_from_legs(arm, meshes, roles)
|
||||
flatten_and_scale(arm, meshes, TARGET_HEIGHT)
|
||||
flatten_and_scale(arm, meshes, TARGET_HEIGHT, roles)
|
||||
fix_unlit_materials(meshes)
|
||||
roles = RigRoles(arm) # rest positions moved; re-read
|
||||
|
||||
|
||||
Reference in New Issue
Block a user