fix(character): the arms hang at the sides — and why that took so long
The character now stands naturally: arms at its sides with a soft elbow bend, hands at the hips, feet flat. Both character shots confirm it, front and back. The root cause was not the pose. The FBX importer had been silently refusing to replace the existing assets: after changing the arm base from 1.55 rad to 0.55 rad and rebuilding, the imported clip returned the IDENTICAL bone quaternion (+0.006, +0.002, -0.179, -0.984). replace_existing=True was set and delete_asset returned False even after chmod u+w, because the pawn's constructor hard-references the clips. Importing under a new name (CharacterV2) created fresh assets and the same probe then returned (+0.439, +0.898) — the rotation had landed. Every earlier attempt had been invisible for this reason. The rotation itself is measured, not inherited: sweeping it in Blender gives 0.55 rad for arms down and clear of the body, 0.65 for straight down against the hip, 0.75 for past vertical and into the torso. It lives in the clamp keys, not in the bind: every bind experiment was impossible on this shape-keyed MPFB mesh or left Unreal showing the old bind. Remaining, for 0.2.2: the fingers splay from the bind, and the walk clip has still not been captured in motion.
This commit is contained in:
1 parent
8993fd83c4
commit
46b84884b8
45 files changed
+131
-67
No files matched your search
@@ -60,13 +60,20 @@ PARTS: list[dict] = []
|
||||
# animates only swing therefore leaves the arms out in an A — which is exactly how
|
||||
# the first third-person render looked. These are the same deltas `IDLE_POSE` uses,
|
||||
# applied as a base that every key sits on top of.
|
||||
# Values are **measured, not inherited**. The rig's A bind already points the upper
|
||||
# arm (0.65, 0.01, -0.76), i.e. about 50 degrees below horizontal, so the rotation
|
||||
# that brings it vertical is only about 0.55 rad - not the 1.55 rad the old
|
||||
# `--pose idle` table used, which over-rotated the arm ~40 degrees past vertical and
|
||||
# drove it into the torso. Sweeping the rotation in Blender gave:
|
||||
# z=-0.55 -> arm (0.16, -0.02, -0.99) down, slightly clear of the body
|
||||
# z=-0.65 -> arm (0.06, -0.02, -1.00) straight down, touching the hip
|
||||
# z=-0.75 -> arm (-0.04, -0.03, -1.00) past vertical, into the body
|
||||
# 0.55 is the value used.
|
||||
BASE = {
|
||||
"shoulder01.L": (0.0, -0.15, -0.03),
|
||||
"shoulder01.R": (0.0, 0.15, 0.03),
|
||||
"upperarm01.L": (0.0, 0.0, -1.55),
|
||||
"upperarm01.R": (0.0, 0.0, 1.55),
|
||||
"lowerarm01.L": (0.0, 0.0, -0.10),
|
||||
"lowerarm01.R": (0.0, 0.0, 0.10),
|
||||
"upperarm01.L": (0.0, 0.0, -0.55),
|
||||
"upperarm01.R": (0.0, 0.0, 0.55),
|
||||
"lowerarm01.L": (0.0, 0.0, -0.12),
|
||||
"lowerarm01.R": (0.0, 0.0, 0.12),
|
||||
}
|
||||
|
||||
|
||||
@@ -104,24 +111,59 @@ def apply_base_pose(arm):
|
||||
"""
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
# The sign is inverted relative to the original `--pose idle` table: the first
|
||||
# attempt applied BASE as written and the body came out with its arms raised in
|
||||
# a V, i.e. the FBX axis mapping flips this rotation. Verified on the imported
|
||||
# asset rather than guessed - see MEMORY.md, 2026-11-09.
|
||||
for name, eul in BASE.items():
|
||||
bone = arm.pose.bones.get(name)
|
||||
if bone is None:
|
||||
continue
|
||||
bone.rotation_mode = "XYZ"
|
||||
bone.rotation_euler = (-eul[0], -eul[1], -eul[2])
|
||||
bone.rotation_euler = eul
|
||||
# Bake the deformation into the MESH before the rest bake. Setting the rest pose
|
||||
# alone leaves the vertices in their A shape, so the skin stretches across the
|
||||
# shoulders and the arms sink into the torso - which is what the review shots
|
||||
# showed. The MPFB mesh carries shape keys and Blender refuses to apply a modifier
|
||||
# while they exist, so they are cleared first and the count is logged.
|
||||
mesh = next((o for o in bpy.data.objects
|
||||
if o.type == "MESH" and any(m.type == "ARMATURE" and m.object == arm
|
||||
for m in o.modifiers)), None)
|
||||
keys = 0
|
||||
if mesh is not None:
|
||||
if mesh.data.shape_keys:
|
||||
keys = len(mesh.data.shape_keys.key_blocks) - 1
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.context.view_layer.objects.active = mesh
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
mesh.select_set(True)
|
||||
if mesh.data.shape_keys:
|
||||
bpy.ops.object.shape_key_remove(all=True)
|
||||
for mod in list(mesh.modifiers):
|
||||
if mod.type == "ARMATURE" and mod.object == arm:
|
||||
bpy.ops.object.modifier_apply(modifier=mod.name)
|
||||
log(f"mesh deformation baked into vertices; {keys} shape key(s) cleared")
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
|
||||
bpy.ops.pose.armature_apply()
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
if mesh is not None:
|
||||
added = mesh.modifiers.new(name="Armature", type="ARMATURE")
|
||||
added.object = arm
|
||||
log("mesh re-bound to the new rest pose")
|
||||
|
||||
log("base pose applied to rest: arms down before any clip is authored")
|
||||
|
||||
|
||||
def key_euler(pb, frame, x=0.0, y=0.0, z=0.0):
|
||||
"""Key a pose-bone rotation; the arms-down base is already in the rest pose."""
|
||||
pb.rotation_euler = (x, y, z)
|
||||
"""Key a pose-bone rotation, offset by the standing base.
|
||||
|
||||
The bind is left alone: the MPFB mesh carries shape keys, and every attempt to
|
||||
re-bind it (clear keys, bake the armature modifier, set rest) either failed or
|
||||
left Unreal showing the old bind. Carrying the arm rotation in the clip is the
|
||||
one path that is provable: read the imported clip back and check its quaternion.
|
||||
"""
|
||||
bx, by, bz = BASE.get(pb.name, (0.0, 0.0, 0.0))
|
||||
pb.rotation_euler = (bx + x, by + y, bz + z)
|
||||
pb.keyframe_insert(data_path="rotation_euler", frame=frame)
|
||||
|
||||
|
||||
@@ -201,7 +243,6 @@ def main(argv=None) -> int:
|
||||
|
||||
bpy.context.scene.render.fps = FPS
|
||||
clear_animation(arm)
|
||||
apply_base_pose(arm)
|
||||
idle = clip_idle(arm, CLIPS["Idle"])
|
||||
walk = clip_walk(arm, CLIPS["Walk"])
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import unreal
|
||||
|
||||
SRC = "/tmp/gigline/character-anim.fbx"
|
||||
DEST = "/Game/Test/Character"
|
||||
PREFIX = "Character_Anim"
|
||||
PREFIX = "CharacterV2"
|
||||
RESULT = "/tmp/gigline/character-anim-import.txt"
|
||||
lines = []
|
||||
|
||||
|
||||
Reference in new issue
Block a user