fix: unlink the ankle cuffs, stop the reload head-dive, soften every blend

Three animation complaints, measured before changing anything.

There is NO squash-and-stretch. Instrumented every bone every frame: the
worst deviation of a pose basis from a pure rotation is 0.00000. Every clip's
position tracks match the skeleton's rest pose to within a millimetre (only
DEF-hips translates, as it should) and no scale key deviates from 1. The
"squishing" is a deep BEND, not a scale: the shared library's PistolReload was
authored for someone watching their own hands, and it dives the head 33° with
the whole neck chain at 67°. At that depth Taila's head/hair weights pinch and
the skull changes shape. Dropping neck+head from the upper-body one-shot's
filter leaves them on the locomotion layer: head dive is now 14° and the neck
chain 48°, and the character keeps looking downrange through a reload, which
is what a shooter wants anyway. Trade-off: Throw/Hit no longer swing the head.

The ankle cuffs are not linked by the rig or the clips — the foot bones swing
independently (left/right Z correlation -0.94). They are linked by SKIN
WEIGHTS: 16 vertices belonging to the LEFT cuff, sitting at x = -0.002 just
across the centre line, are weighted to the RIGHT leg, so the far foot drags a
band of cuff across the gap. Position alone cannot classify them (the cuff
they belong to spans x = 0.00 .. 0.05), so SkinWeightRepair decides by
CONNECTIVITY: a vertex that disagrees with 80%+ of the vertices it shares
triangles with gets its leg influences mirrored. Ankle-height cross-leg
triangles go 24 -> 0 on Taila; the pass is conservative enough that Miku needs
exactly 1 vertex and her face/hair/skirt are untouched (only leg bones are
ever considered). Meshes with blend shapes are skipped rather than rebuilt.

Blending: nothing cuts hard any more. Base cross-fade 0.15 -> 0.22, locomotion
0.28-0.30 (Idle/Walk/Run/Sprint switch constantly as speed drifts across their
thresholds, which is where the snapping showed most), reaction moves raised
off their near-instant values but still snappy, and the upper-body one-shot's
own fades widened to 0.14 in / 0.22 out.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Nicholas Butzke
2026-07-21 18:56:21 -04:00
co-authored by Claude Opus 4.8
parent d8b0c08611
commit 35ada4f34a
4 changed files with 219 additions and 9 deletions
+31 -9
View File
@@ -56,12 +56,18 @@ const LOOPING_CLIPS := ["Idle", "Walk", "Run", "Sprint", "Fall", "Crouch",
"CrouchIdle", "CrouchWalk", "Slide", "WallRun", "WallCling", "Grapple",
"Dance", "PistolIdle"]
const BLEND_TIME := 0.15
## Per-clip blend overrides: snappy moves cut fast, locomotion cross-fades.
const BLEND_TIME := 0.22
## Per-clip blend overrides. Reaction moves still need to read as instant, but
## nothing cuts hard any more — every clip cross-fades. Locomotion gets the
## longest fades because Idle<->Walk<->Run<->Sprint switch constantly as speed
## drifts across their thresholds, and that is where hard cuts were most
## visible.
const BLEND_TIMES := {
"Dash": 0.06, "Jump": 0.08, "Hit": 0.05, "Land": 0.08,
"Slide": 0.1, "Death": 0.1, "Throw": 0.06, "PistolReload": 0.12,
"Idle": 0.25, "PistolIdle": 0.25, "Walk": 0.2, "Run": 0.2, "Sprint": 0.2,
"Dash": 0.10, "Jump": 0.12, "Hit": 0.08, "Land": 0.12,
"Slide": 0.16, "Death": 0.15, "Throw": 0.10, "PistolReload": 0.18,
"Idle": 0.30, "PistolIdle": 0.30, "Walk": 0.28, "Run": 0.28, "Sprint": 0.28,
"CrouchIdle": 0.28, "CrouchWalk": 0.28, "Fall": 0.20, "WallRun": 0.22,
"WallCling": 0.20, "Grapple": 0.20,
}
## Named gameplay actions -> (clip, lock seconds). Networked via the
@@ -93,9 +99,17 @@ var _upper_lock: float = 0.0 # seconds the one-shot owns the ARMS
var _upper_total: float = 0.0 # its full duration, for progress 0..1
var _upper_action: String = "" # which ACTIONS entry is playing
## Bone-name fragments that belong to the upper-body one-shot layer.
##
## NECK AND HEAD ARE DELIBERATELY EXCLUDED. The shared library's action clips
## were authored for a character looking at their own hands: PistolReload alone
## dives the head 33° and bends the whole neck chain 67° (measured). At that
## depth Taila's head/hair weights pinch and the skull visibly changes shape —
## which is what "reloading squishes the headshape" was. Nothing is actually
## scaled; no bone in any clip deviates from the rest pose by more than a
## millimetre. Leaving neck+head on the locomotion layer keeps the character
## looking downrange through a reload, which is also what a shooter wants.
const UPPER_BONE_HINTS := ["shoulder", "upper_arm", "forearm", "hand", "thumb",
"f_index", "f_middle", "f_ring", "f_pinky", "spine.002", "spine.003",
"neck", "head"]
"f_index", "f_middle", "f_ring", "f_pinky", "spine.002", "spine.003"]
# Grapple: world-space anchor the hook is attached to (drives the procedural
# zip pose — body aligned to the line, free arm reaching for the point).
@@ -148,6 +162,12 @@ func load_model(path: String) -> void:
push_warning("SkinnedPlayerModel: no skeleton in '%s'" % path)
else:
_ensure_meshes_bound(scene)
# Stray cross-body leg weights make the two ankle cuffs look welded
# together as the legs separate. See SkinWeightRepair.
var reweighted := SkinWeightRepair.repair(scene, skeleton)
if reweighted > 0:
print("SkinnedPlayerModel: re-weighted %d cross-leg vertices in '%s'"
% [reweighted, path.get_file()])
_pose_mod = ShooterPoseModifier.new()
_pose_mod.name = "ShooterPose"
skeleton.add_child(_pose_mod)
@@ -237,8 +257,10 @@ func _setup_anim_tree(scene: Node) -> void:
bt.connect_node("loco_scale", 0, "loco")
var upper := AnimationNodeOneShot.new()
upper.fadein_time = 0.08
upper.fadeout_time = 0.15
# The upper-body one-shot pops in and out over the locomotion clip, so its
# own fades matter as much as the locomotion cross-fade.
upper.fadein_time = 0.14
upper.fadeout_time = 0.22
upper.filter_enabled = true
_upper_anim = AnimationNodeAnimation.new()
bt.add_node("upper_clip", _upper_anim, Vector2(0, 240))