Files
Nicholas ButzkeandClaude Opus 5 daf9627ece 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]>
2026-07-26 12:46:50 -04:00

7.4 KiB
Raw Permalink Blame History

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 03 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.