fix: hide the model's broken outline hull, reverse the backpedal, slower blends

The "linked ankle cuffs" and the "squashed legs" were the same thing, and it
was never the cloth or the weights: it is the model's OWN outline hull.

Isolating it took rendering the raw GLB in a bright scene and toggling one
surface group at a time. The hull ("FullBlack" and "material", the untextured
surfaces that trace the body and hair silhouettes) is a duplicated shell whose
skin weights do not track the base mesh through a deep bend. During a run it
tears into spikes and stretches sheets across the ankles. Hiding just those
surfaces makes the legs render perfectly clean, with no other change — so the
two previous attempts here were both treating symptoms:

  * re-weighting stray ankle vertices only tore the cloth (reverted last time)
  * deleting cross-leg triangles removed real geometry for no benefit
    (SkinMeshRepair is deleted in this commit — the clean render above was
    produced WITHOUT it)

The hull is also redundant: characters already get an inverted-hull overlay
from apply_toon_recursive AND the screen-space ink_edge pass, so dropping it
costs nothing visually. The EYE cards (EyesFullBlack lashes, EyesInvL, EyesHL
highlight) are real facial features rather than a hull, so those are kept, flat
as before. They are told apart by name prefix, not by guesswork about geometry.

Worth recording: no bone is EVER scaled, in any clip. Measured again here
across the whole Run cycle — worst deviation of a bone's pose basis from a pure
rotation is 0.00000. There is no squash-and-stretch in this rig; it only ever
looked that way because of the shell.

Backpedalling now plays the locomotion cycle in reverse (negative TimeScale)
instead of moon-walking with the forward clip — the library ships no authored
backward run. Directional lean is raised from 0.18/0.30 to 0.30/0.42 rad: with
one forward cycle and no strafe clips, the lean is the only cue for which way
the character is travelling, so it has to be legible rather than subtle.

Blends raised again: base 0.22 -> 0.32, locomotion 0.28 -> 0.40. Verified the
cross-fade genuinely applies rather than snapping — instrumented an idle->run
switch and the thigh ramps gradually over the window instead of stepping on
frame one, with xfade_time reading 0.400 s on the Transition node.

FSM tests 11/11, spawn smoke test 0 failures.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Nicholas Butzke
2026-07-21 22:20:02 -04:00
co-authored by Claude Opus 4.8
parent ec6b8228da
commit 28dc255d17
4 changed files with 48 additions and 180 deletions
+19 -15
View File
@@ -56,18 +56,18 @@ const LOOPING_CLIPS := ["Idle", "Walk", "Run", "Sprint", "Fall", "Crouch",
"CrouchIdle", "CrouchWalk", "Slide", "WallRun", "WallCling", "Grapple",
"Dance", "PistolIdle"]
const BLEND_TIME := 0.22
const BLEND_TIME := 0.32
## 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.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,
"Dash": 0.14, "Jump": 0.16, "Hit": 0.10, "Land": 0.16,
"Slide": 0.22, "Death": 0.20, "Throw": 0.14, "PistolReload": 0.24,
"Idle": 0.42, "PistolIdle": 0.42, "Walk": 0.40, "Run": 0.40, "Sprint": 0.40,
"CrouchIdle": 0.40, "CrouchWalk": 0.40, "Fall": 0.28, "WallRun": 0.30,
"WallCling": 0.28, "Grapple": 0.28,
}
## Named gameplay actions -> (clip, lock seconds). Networked via the
@@ -162,13 +162,6 @@ func load_model(path: String) -> void:
push_warning("SkinnedPlayerModel: no skeleton in '%s'" % path)
else:
_ensure_meshes_bound(scene)
# Below-the-knee geometry welded across both legs can only ever stretch
# as they separate — it made the ankle cuffs look linked. See
# SkinMeshRepair.
var culled := SkinMeshRepair.repair(scene, skeleton)
if culled > 0:
print("SkinnedPlayerModel: removed %d cross-leg triangles from '%s'"
% [culled, path.get_file()])
_pose_mod = ShooterPoseModifier.new()
_pose_mod.name = "ShooterPose"
skeleton.add_child(_pose_mod)
@@ -438,11 +431,19 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
# Scale locomotion playback so feet keep up with actual movement speed.
if _anim_tree:
var s := 1.0
var is_loco := false
match clip:
"Walk", "CrouchWalk":
s = clampf(speed / walk_anim_reference_speed, 0.7, 1.6)
is_loco = true
"Run", "Sprint", "WallRun":
s = clampf(speed / run_anim_reference_speed, 0.7, 1.8)
is_loco = true
# Backpedalling: run the cycle BACKWARDS rather than moon-walking with
# the forward clip. The shared library has no authored reverse run, and
# a reversed stride reads correctly for a backpedal.
if is_loco and _cur_fwd < -0.25:
s = -s
_anim_tree.set("parameters/loco_scale/scale", s)
@@ -743,8 +744,11 @@ class ShooterPoseModifier extends SkeletonModifier3D:
var reload_phase: float = 0.0
# Tuning (radians). Positive pitch leans forward; positive roll leans right.
const LEAN_ROLL := 0.30
const LEAN_PITCH := 0.18
# The lean is the ONLY thing that tells a viewer which way this character is
# travelling — the library has one forward locomotion cycle and no strafe
# clips — so it has to be legible, not subtle.
const LEAN_ROLL := 0.42
const LEAN_PITCH := 0.30
const SLIDE_BACK := 0.75 # torso lean-back during slide
const SLIDE_HEAD_UP := 0.7 # head pitch to keep looking forward
const SLIDE_LEG_FWD := 0.95 # thighs swing forward so feet lead the slide