docs(skill): compact the character pipeline into a reusable skill
Everything about getting an anime-styled character into the game — import, stylization, the body/garment/hair separation, rigging, retargeting, cloth and hair physics, and how to measure any of it — collected into .claude/skills/character-pipeline/. Organised around the principle the Hoyoverse-class pipelines are built on and that every failure in this project traced back to: a character is not one object. It is a body, a set of garments and hair, authored and rigged separately and moved by different systems. The body is skinned and animated; the garments and hair are bone chains the animation never touches and physics moves. The skill's non-negotiables are the four ways that separation has been destroyed here before — joining meshes, keying cosmetic bones, skinning cloth to a leg, and running the auto-rig repair on authored weights. references/verification.md leads with the trap that invalidated every cloth measurement ever taken in this repo: Godot restores bone poses after the modifier pass, so a tool that reads them afterwards measures the animation and never sees what any modifier did. Also records what is known-unsolved, with numbers: peak cloth clipping in a run/slide/dash, no foot IK, no strafe clips. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
040b595397
commit
daf9627ece
@@ -0,0 +1,146 @@
|
||||
# Cloth and hair — `characters/spring_bones.gd`
|
||||
|
||||
A position-based (PBD) solver over the rig's own cosmetic bones, the same shape
|
||||
as Magica Cloth 2's BoneCloth, which is the tool the Hoyoverse-style pipelines
|
||||
are built around.
|
||||
|
||||
## Why cloth cannot be solved with weights
|
||||
|
||||
Weight a skirt to the thighs → trousers. Weight it to the hips → a rigid bell.
|
||||
Neither is cloth. A skirt reads as cloth because it **lags** — it keeps going
|
||||
when the hips stop, swings out through a turn, floats on the way up through a
|
||||
jump. That is inertia, and inertia has to be integrated, not skinned.
|
||||
|
||||
## The solve
|
||||
|
||||
Installed as a `SkeletonModifier3D` **after** `ShooterPoseModifier`, so it reacts
|
||||
to the final body pose (animation + lean/slide/aim layer).
|
||||
|
||||
1. **Every JOINT is a particle.** Bone `i` spans particle `i` to `i+1`, so a
|
||||
bone's HEAD can move. This is the load-bearing choice: a contact with no
|
||||
rotational leverage — a thigh against the top of a panel — is resolved by the
|
||||
whole panel moving, which is what a real skirt does.
|
||||
2. **Predict** with inertia, gravity and wind, in the chain ANCHOR's frame, so
|
||||
travelling at a steady speed excites nothing.
|
||||
3. **Relax everything together**, Gauss-Seidel: cross-panel links, then per
|
||||
chain — bone length, bend limit, backstop, colliders.
|
||||
4. **Convert to rotations once**, at the end, and feed back the poses the
|
||||
skeleton ACTUALLY got.
|
||||
|
||||
Order matters: **links first, chains second**, so the last thing to touch any
|
||||
particle is its collision. With the chains first, every iteration ended by
|
||||
pulling neighbouring panels back toward their rest separation — straight into the
|
||||
leg just cleared. Measured on a slide: 93 mm in, 95 mm out; with the links off
|
||||
entirely the same frame solved to 27 mm.
|
||||
|
||||
Then a short tail of **collision-and-length-only** passes, because the bend limit
|
||||
and the backstop are shape constraints and re-imposing them after each collision
|
||||
makes the two argue rather than converge.
|
||||
|
||||
## What the previous version did wrong
|
||||
|
||||
One spring per bone plus FOUR repair passes behind it, each writing bone poses the
|
||||
next read back and partly undid — and the last (an ancestor "lift") wrote poses
|
||||
never fed back into the spring state, so every frame began pulling against a pose
|
||||
the springs did not know about. That feedback was the skirt "glitching out".
|
||||
Three bolt-on stages (a bodily chain push, an ancestor lift, a drape weight) all
|
||||
existed because a rotation-only solver cannot clear a contact near the head it
|
||||
rotates about.
|
||||
|
||||
## Per-class tuning (`TUNING`)
|
||||
|
||||
| | w | zeta | gravity | wind | stray | hinge | bend |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| hair | 15 | 0.45 | 1.6 | 0.30 | 0.45 | 26° | 52° |
|
||||
| skirt | 12 | 0.48 | 2.0 | 0.18 | 1.25 | 78° | 55° |
|
||||
| cloth | 13 | 0.55 | 1.9 | 0.18 | 1.10 | 62° | 52° |
|
||||
|
||||
- **`hinge` vs `bend`** are the same constraint meaning different things. Between
|
||||
segments it is BENDING stiffness (how sharply cloth may crease) and belongs
|
||||
tight. At the root it is a HINGE at the waistband — a knee coming up to hip
|
||||
height puts a thigh horizontally through where the front panel hangs, and the
|
||||
panel must ride onto it, which is most of a right angle. Magica's skirt guide
|
||||
suggests 20° at the root, but that is for a chain whose first bone is a large
|
||||
share of the skirt; Taila's first segment is 49 mm of a 288 mm panel, so 20°
|
||||
there moves the panel below it by **17 mm**.
|
||||
- **`stray`** is Magica's "Max Distance" — how far a particle may end from where
|
||||
the animation alone would have put it. Tight on hair (it has nothing to get out
|
||||
of the way of; this is what stops flailing), loose on cloth (a skirt has to lie
|
||||
along a thigh that has gone horizontal in a slide).
|
||||
- **`gravity` is small** because the authored rest pose already has the garment
|
||||
hanging. A constant force offsets the resting particle by `g/w²`, so a large
|
||||
value pulls the hem below where it was modelled — into the thigh it then has to
|
||||
be pushed out of.
|
||||
|
||||
## Colliders
|
||||
|
||||
Five capsules, measured from the mesh by `retarget.py::_leg_colliders`:
|
||||
|
||||
- A **waist LID** (`lid: true`) across the pelvis. Magica's skirt guide is blunt
|
||||
about this: one big sphere at the waist "acts as a lid that prevents particles
|
||||
in the skirt from slipping into the body". Leg capsules alone only stop cloth
|
||||
going through a thigh; nothing stops a panel swinging INWARD into the pelvis.
|
||||
- **Tapered** thigh and shin capsules — separate head and tail radii. A limb is
|
||||
not a cylinder: Taila's thigh is ~0.11 m at the hip and ~0.06 m above the knee.
|
||||
Fitted as a least-squares line through ten bands, dropping the contaminated end
|
||||
bands, with twist children folded in.
|
||||
- `from: 0.10` — the capsule starts BELOW the hip joint. The top of a thigh is
|
||||
hip, buried inside the body the skirt hangs from.
|
||||
|
||||
**Per-point rest clearance.** Each (bone, collider) point's radius is capped to
|
||||
just inside where that point rests, so the authored rest pose is a valid state.
|
||||
Without it, cloth hanging against a thigh is shoved out and pulled back every
|
||||
frame forever. The cap is PER POINT, not per bone — scaling a whole bone by its
|
||||
worst point switches collision off for every panel whose top hangs against the
|
||||
thigh, which is all the ones that matter.
|
||||
|
||||
## Collision hulls come from the MESH
|
||||
|
||||
`SkinnedPlayerModel._cloth_hulls`, at load time: every vertex a cloth bone
|
||||
dominates, binned into a ~20 mm grid, outermost cells kept, capped at 14 points.
|
||||
|
||||
The sidecar's ten farthest-point samples describe a panel's corners and hem and
|
||||
leave its MIDDLE unsampled — exactly where a thigh comes through. The solver
|
||||
reported every contact resolved while 158 vertices sat 95 mm inside a leg.
|
||||
|
||||
## There is no drape term
|
||||
|
||||
"Cloth takes a share of the leg's motion before the solver runs" is a real
|
||||
technique (Hoyoverse rigs carry a partial constraint from the leg onto the upper
|
||||
skirt bones). It was here to move panels the old rotation-only solver could not.
|
||||
With it against without, over the movement sweep:
|
||||
|
||||
```
|
||||
run 101 -> 92 mm fall 82 -> 49 mm dash 136 -> 95 mm
|
||||
idle after a dash 103 -> 20 mm
|
||||
```
|
||||
|
||||
Worse in every state but a walk, and 20× worse in stability (0.48 vs 0.05
|
||||
deg/frame at a dead idle) because its target sat inside the leg the collision was
|
||||
pushing out of. **If you reintroduce it, the target must be collision-free
|
||||
first.** A naive "seat the reference on the limb" pass was tried and destabilised
|
||||
the reference chain, because a parent's seat rotation cascades into every child.
|
||||
|
||||
## Cost and LOD
|
||||
|
||||
~2.6 ms per character per frame at full quality, three quarters of it collision.
|
||||
It was 10.9 ms before the inner loop stopped rebuilding every capsule and
|
||||
reallocating the hull array for every (bone, collider, pass).
|
||||
|
||||
`SpringBones.lod` 0–3 drops passes then collision;
|
||||
`SkinnedPlayerModel._update_cloth_lod` picks it from camera distance
|
||||
(6 / 14 / 28 m) four times a second.
|
||||
|
||||
If you add cloth bones, re-run `debug/cloth_perf_check.gd`. The cost is the
|
||||
product of joints × colliders × hull points × passes and all four are easy to
|
||||
raise by accident.
|
||||
|
||||
## Hair specifically
|
||||
|
||||
- Hair DOES collide now. It used to be excluded because a collision push happened
|
||||
after the integrator and so was deaf to spring tuning — long back hair got
|
||||
shoved out of a thigh and hauled back at stride frequency, which was the blur.
|
||||
Inside the relaxation there is no such fight.
|
||||
- Hair chains are NOT linked sideways; linking them stiffens them into rope.
|
||||
- Hair sits silent at idle (0.005 deg/frame). If it does not, something is
|
||||
driving its target — that was the drape, and it is the first thing to suspect.
|
||||
Reference in New Issue
Block a user