feat(characters): import the Quaternius mannequin as a selectable skin
A third playable character, built with the pipeline skill from a source that was already in the repo: the animation library ships a rigged Mannequin mesh on the exact 53-joint reference skeleton, CC0, so it needed no download and retargets perfectly. 18 clips, 0.3% cross-leg bleed, 7% of verts at four influences — a clean authored-weight import. Licence recorded in mannequin.license.json as the other skins do. It has no cloth chains, correctly: it is a mannequin and has neither hair nor clothes. Importing it turned up two real bugs, both of which would have hit any flat-coloured or single-piece model: - LevelMaterials.apply_character_look treated ANY untextured surface on a character as the model's own outline shell and hid it, so the mannequin rendered as a solid black silhouette — its body and joint materials are untextured flat colours, not ink. _is_line_work() now asks whether the surface is named eyes*, is drawn front-face-culled (the inverted-hull setup), or is near-black. Taila and Miku are unaffected: their materials are textured and never reach that branch. Verified by render. - verify_character.py failed the build for having one mesh. That check cannot tell "the pipeline joined them" from "the artist authored one mesh" — Quaternius' mannequin is one piece on purpose. It is advisory now; the join path's two unambiguous signatures, cross-leg bleed and the 4-influences-everywhere spread, are still hard checks. Also restored Miku's description, which the re-import had blanked. 3 GLB skins selectable (6 with the built-in colour skins). Smoke 0 failures, 11/11 movement tests, cloth idle 0.024-0.078 deg/frame. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
270d5f0973
commit
cd0d1b2d99
@@ -114,6 +114,236 @@ nothing in the skeleton says which way a panel hangs or how thick a thigh is.
|
||||
Tuning per class (hair stiffer and lighter, skirt slacker and heavier) lives in
|
||||
`SpringBones.TUNING`.
|
||||
|
||||
### Collision, and why cloth "never settles"
|
||||
|
||||
Getting the capsules wrong does not look like a collision bug — it looks like
|
||||
cloth that jitters forever. Four things had to be right:
|
||||
|
||||
1. **A limb tapers.** Taila's thigh is ~0.10 m across at the hip and ~0.055 m
|
||||
above the knee. Head and tail radii are stored separately and interpolated.
|
||||
2. **A leg's own vertices are not the leg.** Most of the thigh is weighted to
|
||||
the TWIST bone; what is left dominated by `DEF-thigh.L` is the hip flare,
|
||||
which measured a 0.154 m radius — a 30 cm thigh. Twist children are folded
|
||||
in, and only vertices that clearly belong to the limb (dominant weight > 0.6)
|
||||
are counted, or a hip/thigh/glute blend puts the flare straight back.
|
||||
2b. **Measure the taper, do not pick a percentile of a pooled bucket.** Every
|
||||
single-number answer is wrong at one end: the median leaves half the limb's
|
||||
surface OUTSIDE its own collider, so cloth pushed out to it is clear of the
|
||||
capsule while the thigh is visibly through it; a high percentile over-
|
||||
measures the shaft by 30% because the top bucket is still the hip. Take the
|
||||
90th percentile in each of ten bands along the bone, drop the two contaminated
|
||||
end bands, and fit a line. Taila's thigh: 0.116 m at the hip to 0.063 m above
|
||||
the knee, against a real surface of 0.106 → 0.058 plus cloth thickness.
|
||||
3. **The rest pose must be a valid state.** The artist modelled the skirt over
|
||||
these legs, so a capsule is capped per bone to just inside its own rest
|
||||
clearance. Without that, bones resting against the thigh were shoved out and
|
||||
pulled straight back in every frame — measured 13 of 70 cloth bones in
|
||||
permanent contact in a dead-still idle, a limit cycle that never decayed.
|
||||
That was the "hair and skirt never settle", and it also left the collider
|
||||
saturated and useless against real clipping.
|
||||
4. **Resolve as a rotation, sampled along the bone.** A skirt panel is a sheet
|
||||
and its bone tip is at the far edge; pushing only the tip out leaves the
|
||||
middle of the panel inside the thigh — the thigh visibly clipping through the
|
||||
front of the skirt at a stride.
|
||||
5. **Carry the whole rotation back, not a tip.** Most of what lifts a wide sheet
|
||||
off a thigh is rotation about the bone's OWN axis, and a twist moves the tip
|
||||
not at all. Reading the correction off the corrected tip therefore threw away
|
||||
the part that mattered: the solver measured 50–70 mm of thigh inside a panel,
|
||||
corrected it every frame, and the render never changed.
|
||||
6. **Apply the drape to the bone, not to the spring's target.** The final
|
||||
rotation is measured FROM the drape target and applied TO the undraped basis,
|
||||
so rotating only the target cancelled out exactly at equilibrium and the
|
||||
drape was a silent no-op. This is why raising the drape weight — even to 0.99
|
||||
— never moved a panel off a thigh, and it sent three rounds hunting the
|
||||
collision solver for a fault that was never there. In a chain, apply each
|
||||
link's share as the DIFFERENCE from what its parent already carries, or three
|
||||
segments at 0.45 compound to 1.35 of the thigh's swing at the hem.
|
||||
7. **Fix the deepest contact, not the one wanting the largest angle.** The angle
|
||||
is depth ÷ leverage, so a graze on a hull point sitting almost on the bone's
|
||||
head outbids a 60 mm impalement further down and spends the pass rotating
|
||||
about a point that barely moves. Cap the turn so it never throws the rest of
|
||||
the panel further than the overlap being fixed, or a 5 mm contact swings a
|
||||
panel 34°.
|
||||
8. **Take the leg out of the REST TARGET too.** While the spring's target sits
|
||||
inside a thigh, the collision pushes out and the spring hauls straight back
|
||||
forever. Resolving the target first makes resting on a leg an equilibrium.
|
||||
|
||||
### The drape has to RIDE the limb, not copy its angle
|
||||
|
||||
This was the one that mattered, and it hid behind every other theory for a long
|
||||
time. The drape used to take the thigh's ROTATION and apply it to the panel's
|
||||
basis — which turns the panel about ITS OWN head, up at the waist. The thigh
|
||||
turns about the HIP, some 20 cm lower. The two arcs are nothing alike, so the
|
||||
panel never tracked the leg however high the drape weight went, and the thigh
|
||||
walked straight out through the front of the skirt.
|
||||
|
||||
Carry the bone's rest tip through the limb's FULL transform instead — pivot
|
||||
included — and aim the bone at the result:
|
||||
|
||||
```
|
||||
carried = L_now * L_rest⁻¹ * tip_rest # where the limb would take it
|
||||
aim = slerp(current_dir, carried_dir, w) # w = how much of it rests there
|
||||
```
|
||||
|
||||
That is what "the cloth rides the leg" actually means, and it is the difference
|
||||
between a panel that gets overtaken and one that lifts over the thigh.
|
||||
|
||||
Two supporting pieces, both needed:
|
||||
|
||||
- **Which limb, decided per frame.** A static both-thigh split by rest distance
|
||||
is right for a panel hanging between the legs and wrong the moment they
|
||||
scissor — the halves cancel, the panel does not move, and the advancing thigh
|
||||
walks into it. `DRAPE_BITE` keeps a limb's share climbing once its gap goes
|
||||
negative, so the leg actually inside the cloth wins outright.
|
||||
- **The correction walks UP the chain** (`_lift_chains`). A bone rotates about
|
||||
its own head, so it can move a point by at most twice that point's distance
|
||||
from the head — and the front panels' contacts sit 10-48 mm from their pivot
|
||||
against a thigh ~100 mm inside them. Their ancestors have 5-15x the lever, so
|
||||
a few degrees up the chain does what no local rotation could. Keep the per-bone
|
||||
cap small (4°): at 9° over 3 passes the whole panel hiked up.
|
||||
|
||||
Tuning that matters: `DRAPE_MAX` 0.55. At 0.9 the panel over-swings and hikes;
|
||||
at 0.0 the thigh passes straight through. Panels subdivide into 4 segments — 3
|
||||
leaves the pivots too far from the contacts, 6 lets the chain curl up.
|
||||
|
||||
### What a production cloth setup actually does
|
||||
|
||||
Everything above was arrived at by measurement, and it converged on a solver that
|
||||
was still missing the single most important piece. From Magica Cloth 2's BoneCloth
|
||||
skirt guide (the Unity asset most anime-style games use for exactly this garment):
|
||||
|
||||
> Normally, the skirt bones are linked vertically due to the parent-child
|
||||
> structure, **but not horizontally** … If horizontal bones are not connected, the
|
||||
> accuracy of collision detection will be significantly reduced. **This is the
|
||||
> most important work when expressing a skirt with BoneCloth.**
|
||||
|
||||
That was the gap. Every chain here solved alone, so each panel individually
|
||||
satisfied its constraints while the garment came apart. Four things were taken
|
||||
from that guide and its penetration page:
|
||||
|
||||
1. **Horizontal links between chains** (`LINK_PASSES`, `_build_links`). Distance
|
||||
constraints between the TIPS of bones that share mesh vertices, relaxed
|
||||
Gauss-Seidel after the springs and collision — Magica's "Near Point"
|
||||
connection. Adjacency comes from the sidecar's `neighbours`, which is shared
|
||||
vertex weight: the artist's answer to what is sewn to what, not a guess from
|
||||
names or rest distance. 215 links on Taila.
|
||||
2. **A lid across the waist.** "Put one big sphere collider on your waist… it acts
|
||||
as a lid that prevents particles in the skirt from slipping into the body."
|
||||
There was no torso collider at all — leg capsules stop cloth going through a
|
||||
thigh, but nothing stopped a panel swinging INWARD into the pelvis, which is
|
||||
where several of the worst contacts sat. Marked `lid` in the sidecar so the
|
||||
drape does not treat it as a limb to be carried by, and sized from non-cloth
|
||||
geometry (including the garment measured the skirt itself, 0.24 m).
|
||||
3. **A graded bend clamp.** "The start point can be bent up to 20 degrees and the
|
||||
tip up to 50." A flat limit either lets the waist collapse or stops the hem
|
||||
moving; `MAX_SWING_ROOT`/`MAX_SWING_TIP` interpolate along each chain.
|
||||
Collision still overrides it, which is Magica's precedence too.
|
||||
4. **Collision is not enough on its own** — "if you make a vigorous movement, you
|
||||
will inevitably penetrate" — which is why the drape and the bodily shift exist
|
||||
alongside it rather than instead of it. Magica's two extra modes are Surface
|
||||
Penetration (needs the cloth weighted to the main bones) and Collider
|
||||
Penetration (for cloth that does not follow the leg animation). This skirt is
|
||||
deliberately the second kind, since any leg weighting on it tears.
|
||||
|
||||
### The skirt "breaking" instead of stretching
|
||||
|
||||
A separate failure from clipping, and it looks like clipping: the front of the
|
||||
skirt is pushed aside and then splits, and the thigh shows through the split.
|
||||
Measure it with `debug/cloth_stretch_check.gd`, which skins every cloth triangle
|
||||
and compares each edge against its own rest length — a bone-level or capsule-
|
||||
level number cannot see it, because every individual bone is behaving.
|
||||
|
||||
It was the WEIGHTS. A band of skirt vertices carried `DEF-thigh.L` at 0.24 while
|
||||
the vertices next to them carried none, so when the thigh swung one followed it
|
||||
and its neighbour did not. Measured during a slide: 80 mm apart, 3.3x rest
|
||||
length. 0.24 of the thigh's ~0.35 m of travel is 84 mm, so that discontinuity
|
||||
was the whole of it.
|
||||
|
||||
**Cloth is not skinned to the legs at all** — `tools/retarget.py::
|
||||
unbind_cloth_from_legs`. Smoothing the field instead of deleting it does not
|
||||
work, and the arithmetic says why: differential motion is the weight GRADIENT
|
||||
times how far the limb travels, so holding an edge under 10 mm against a thigh
|
||||
that moves 350 mm needs under 0.03 of weight difference across it — and this
|
||||
skirt's edges are ~48 mm long, so the falloff would have to span most of the
|
||||
character. Tried: a 1.7-radius falloff put 0.24 on one vertex and 0.00 on its
|
||||
neighbour and tore by 95 mm. After stripping, the worst cloth edge in the whole
|
||||
sweep grows **0.0 mm**.
|
||||
|
||||
There was also a `bind_cloth_to_legs()` that did this deliberately, on the
|
||||
(wrong) conclusion that clipping was a weights problem. It is gone for the same
|
||||
reason, plus two of its own: a vertex weighted 0.9 to a thigh cannot be moved by
|
||||
its own cloth bone, so the solver loses the authority to push it out; and it
|
||||
poisoned the collider measurement, fitting a 0.28 m thigh out of 2258 skirt
|
||||
vertices.
|
||||
|
||||
Keeping the leg out of the weights costs the one thing the weights were doing,
|
||||
so the solver has to make it up:
|
||||
|
||||
- **The capsule starts 30% down the thigh.** The top of a thigh is hip, not limb.
|
||||
Cloth points there sit 20-30 mm from their own bone's head, and a rotation
|
||||
moves a point near its pivot by at most twice the lever — 48 mm against 86 mm
|
||||
of overlap. The solver spent all six passes saturated at its cap and still left
|
||||
60-90 mm.
|
||||
- **The drape picks its limb per frame, by current proximity.** Splitting a panel
|
||||
between both thighs by REST distance is right for a panel hanging between them
|
||||
and wrong the moment they scissor: the two opposite swings cancel, the panel
|
||||
does not move, and the advancing thigh walks into it.
|
||||
- **Chains are shifted bodily as well as turned** (`PUSH_MAX`). Translation is
|
||||
the degree of freedom rotation lacks, and it is safe here precisely because the
|
||||
whole chain moves as one piece — unlike a weight gradient it cannot stretch the
|
||||
mesh, and neighbouring panels see the same contact and shift the same way, so
|
||||
the seams between them stay shut.
|
||||
- **Neighbouring cloth bones are known to the runtime.** `neighbours` in the
|
||||
sidecar means SHARED VERTICES — the artist's own answer to which pieces are
|
||||
sewn together — and the drape is relaxed across them so sewn panels cannot be
|
||||
handed wildly different shares.
|
||||
|
||||
On the shipped materials the skirt now reads as one continuous sheet with an
|
||||
unbroken hem through the whole sweep, and the thighs emerge from below it.
|
||||
|
||||
Two measurement traps found while confirming that, both of which produced
|
||||
"remaining clipping" that was not there:
|
||||
|
||||
- **A silhouette test cannot see clipping.** When the legs are apart you see the
|
||||
FAR side of the skirt through the gap between them, and the thighs are
|
||||
legitimately in front of that. `tools/measure_clipview.py` counts those as
|
||||
hits. It is a good continuity check — a tear is a hole in the cloth mask — and
|
||||
a bad clipping check. Judge clipping from `debug/skirt_probe.gd` and the plain
|
||||
render.
|
||||
- **The torso is in front of the waistband, and that is correct.** Counting all
|
||||
body pixels reported 5% of the cloth covered on a pose that is clean, so the
|
||||
measurement is cut at the hip joint. Colouring the legs separately does not
|
||||
work either: the bare thigh is part of the body mesh, and a per-surface colour
|
||||
caught only the boots.
|
||||
|
||||
Together those took idle penetration from 65 mm to ~1 mm and contacts from ~10
|
||||
per frame to ~2. Damping then matters: 0.3 rang for about three visible
|
||||
oscillations after every step, so it sits near 0.6. The lag that makes a skirt
|
||||
read as cloth comes from inertia, not from low damping, so this costs nothing in
|
||||
the run cycle. Gravity is small for the same reason — a constant force offsets
|
||||
the resting tip by `g/w²`, and the 5.0 first used pulled the hem 41 mm below
|
||||
where it was modelled, into the thigh it then had to be pushed out of.
|
||||
|
||||
## Locomotion transitions
|
||||
|
||||
Two separate things change when a character starts running, and they have to
|
||||
arrive together:
|
||||
|
||||
- **The clip**, crossfaded by `AnimationNodeTransition` over `BLEND_TIMES`
|
||||
(~0.4 s for locomotion). Tier selection has hysteresis and a minimum dwell:
|
||||
without them, hard acceleration crossed Idle → Walk → Run in under a second
|
||||
and each crossfade cut off the one before it, giving Walk 0.19 s of a 0.40 s
|
||||
blend. With a dwell, a hard start now goes Idle → Run in one step.
|
||||
- **The procedural lean** (`ShooterPoseModifier`), which is NOT part of the
|
||||
blend graph. The controller passes a normalised input direction, so it stepped
|
||||
0 → 1 the instant a key went down and planted a full run posture in about a
|
||||
tenth of a second — the body snapping forward ahead of the run cycle. It is
|
||||
now scaled by actual speed and smoothed on its own slower rate
|
||||
(`LEAN_SMOOTH`), so it grows as the character accelerates.
|
||||
|
||||
Measure with `debug/transition_check.gd`: it reports clip changes, when the lean
|
||||
reaches 10% and 90%, and the worst single-frame change.
|
||||
|
||||
### `<model>.rig.json`
|
||||
|
||||
Written next to every built GLB, so the runtime never re-guesses anatomy:
|
||||
@@ -188,6 +418,41 @@ Missing clips are fine: the game falls back along sensible chains
|
||||
nearly caused bad "fixes".
|
||||
- `godot --path . --windowed --resolution 1280x720 -s res://debug/anim_capture.gd -- <out_dir> <skin_id>`
|
||||
— renders every movement state front and side.
|
||||
- `godot --headless --path . -s res://debug/cloth_settle_check.gd -- <glb>` — does
|
||||
the cloth actually come to rest? Reports deviation, per-frame motion and, most
|
||||
usefully, which bones are penetrating a leg capsule and by how much. A steady
|
||||
non-zero contact count is a limit cycle that no amount of damping will fix.
|
||||
- `godot --headless --path . -s res://debug/transition_check.gd -- <glb>` — clip
|
||||
changes and lean ramp when accelerating from a standstill.
|
||||
- `godot --path . --windowed --resolution 900x900 -s res://debug/skirt_clip_view.gd -- <out_dir>`
|
||||
— **the tool that settles "is the leg through the skirt?"**. Paints every cloth
|
||||
surface flat magenta and the body flat grey across a movement sweep, so grey
|
||||
inside the magenta is the leg in front of the cloth and grey outside it is just
|
||||
the leg past the hem. Those two look identical on the shipped materials and
|
||||
were guessed at, in both directions, for several rounds. It also saves a
|
||||
cloth-only frame, which separates clipping from a gap opening between panels.
|
||||
- `godot --headless --path . -s res://debug/skirt_probe.gd` — per cloth bone, how
|
||||
deep the leg is inside it and whether the solver can SEE that depth (the
|
||||
rest-clearance allowance can hide it). Also reports the lever the solver has on
|
||||
the point, and its height above the hip joint — anything positive is inside the
|
||||
fictional sphere the capsule puts at the top of the thigh, not inside the leg.
|
||||
- `godot --headless --path . -s res://debug/cloth_stretch_check.gd` — **the tool
|
||||
for "the skirt breaks instead of stretching"**. Skins every cloth triangle over
|
||||
a movement sweep and compares each edge against its own rest length, reporting
|
||||
seams between panels separately from edges inside one. Nothing at bone or
|
||||
capsule level can see a tear, because each bone individually is fine. Restrict
|
||||
it to genuinely cloth-owned vertices — body surfaces carry stray cloth
|
||||
influence (one arm vertex measured 0.54 forearm, 0.35 skirt) and counting those
|
||||
made the skirt look like it was tearing by half a metre when the arm moved.
|
||||
- `godot --headless --path . -s res://debug/leg_radius_check.gd` — the real
|
||||
per-band radius of each limb next to the capsule actually shipped.
|
||||
- `godot --path . --windowed --resolution 900x900 -s res://debug/idle_jitter_check.gd -- <out_dir>`
|
||||
— consecutive frames of a still idle. Counting changed PIXELS between them is
|
||||
the only trustworthy settling measure: `cloth_settle_check` reports LOCAL bone
|
||||
rotation, and a parent's correction shows up as an equal and opposite delta on
|
||||
each of its segments, so a hem that has not moved on screen can read 18
|
||||
deg/frame. Measured here: 24866 changed px/frame with collision on against
|
||||
38594 with it off — the collision was damping the idle, not driving it.
|
||||
|
||||
Current Taila, worst over a run/walk/jump/fall/slide/dash sweep: knee
|
||||
cross-section 0.85–0.86, everything else 0.89–1.00, worst stretch 1.16.
|
||||
@@ -222,6 +487,12 @@ cross-section 0.85–0.86, everything else 0.89–1.00, worst stretch 1.16.
|
||||
clips are keying cloth bones (`verify_character.py` checks this).
|
||||
- *Cloth flies off the model* — a spring instability. `SpringBones` substeps and
|
||||
clamps for exactly this; do not remove those guards.
|
||||
- *Cloth jitters and never settles* — almost certainly a capsule the rest pose is
|
||||
already inside, not the damping. Run `cloth_settle_check.gd` and look at the
|
||||
contact count before touching `TUNING`.
|
||||
- *A limb clips through cloth* — check the measured capsule in the sidecar is a
|
||||
believable size for that limb, and remember the collider only knows about the
|
||||
legs.
|
||||
- *Limbs squash at a stride* — measure with `limb_deform_check.gd` before
|
||||
changing anything. Renders are repeatedly misleading; a slim anime leg at full
|
||||
stride genuinely looks stretched.
|
||||
|
||||
Reference in New Issue
Block a user