feat/fidelity-pass-2
15
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e97de9aafd |
fix(weapons): the wrist turns the hand, and the gun stays on the aim line
The weapon is a child of a BoneAttachment3D on the trigger hand, so the two were welded by construction. Every degree `wrist_r` turned swung the barrel the same degree off the aim line — and took with it every control that could have corrected for it, because they are all expressed relative to that same hand. There was no combination of sliders that aligned a hand to a gun, which is the one thing the knob exists for. Both outcomes are now computed where the hand's local pose is set: the rotation the hand would take without the wrist offset, and the one it takes with it. The hand gets the second; the difference between them is exactly the counter-rotation the weapon mount needs, in the hand's own local frame, and `SkinnedPlayerModel._hold_weapon_still` applies it to the mount each frame. The gun ends up precisely where the solver put it. That also gives the two controls a clean split, which is what makes them usable together: TRIGGER / SUPPORT WRIST (hold) turns the HAND, gun stays on the aim line Grip roll / pitch / yaw (anchors) turns the GUN inside the hand Identity when `wrist_r` is untuned, so a character nobody has tuned mounts its weapon exactly as before. Applied in `_process` rather than inside the modifier pass on purpose. The gun's mount is not something the skeleton owns, and the compensated value only changes when a slider moves or the ADS blend travels, so one frame of lag is a fraction of a degree; reaching into the modifier to touch a scene node would be worse. wrist_gun_check asserts both halves, because only asserting the first is how this shipped broken: the hand must TURN, or the knob does nothing, and the gun must NOT, or the knob cannot be used. Across both poses and all three axes the hand turns 28.2-28.7 degrees for a 0.5 rad knob and the gun moves 0.1-0.6 — against the ~28 it would move if it were still following the wrist. The residue is the arm's own IK settling, since the hand's rotation feeds the chain that places the shoulder. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
a97ccca13c |
feat(rig lab): wrists in three axes, and sliders that belong to the pose on screen
Three things the lab could not do. WRISTS. Each hand had one scalar, a twist about the barrel. That is the only axis a hand wrapping a cylinder is free in ONCE the arc onto the barrel is solved — which is true of the support hand, was never true of the trigger hand, and in neither case left a way to cock a wrist forward or break it inward. Both now take pitch, yaw and roll, applied in the GUN's frame so the three sliders mean the same thing whether the muzzle is down at low ready or level down the sights. Zero is exactly the old behaviour, since the roll term defaulted to zero too. HAND POINTS. `gun_stock` and `gun_fore` are the distances along the weapon at which each hand sits, and they were labelled by what they measure rather than by whose hand it is. They now say TRIGGER and SUPPORT, next to the off-barrel shifts for the same two hands, so the four controls that place a hand read as four controls that place a hand. POSES. The hold's knobs are now per pose, and the lab shows one pose's at a time. Half of them mean something different at low ready than down the sights; showing both sets at once meant every slider on screen was for one of two poses with nothing saying which. Selecting a pose rebuilds the panel. Two poses, not four, and deliberately: the runtime blends between exactly two holds on `ads`. Running and Crouched are locomotion states that still use the low-ready hold, so they edit the same numbers — and the heading says so, rather than letting someone tune "Running" and wonder why standing still changed. Offering four independent tunings would be inventing a capability the code does not have, and the fourth would silently do nothing. `pitch` is the case that forced the design: down the sights the muzzle follows the CAMERA, so there is nothing there to tune. It exists at low ready and nowhere else, and a spec table where a knob names the poses it applies to is what lets that be said instead of shipping a control that does nothing. hold_pose_check asserts both halves — that no pose shows another's knobs, that aiming offers no muzzle pitch, that the heading names the hold being edited, and that all twelve wrist axes turn the hand they name. Its first version reported every wrist axis as moving the hand by 0.0 degrees, which is precisely the answer it would have given if the wrists had never been implemented: it read `get_bone_pose_rotation` from a SceneTree script, and Godot restores every bone's local pose after the modifier pass. The repo has a reference section about exactly this and it still cost a cycle. Measured through a PoseProbe, every axis turns its hand ~20 degrees. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
b1c8bab714 |
feat(rig lab): drag the anchors themselves, not just the gun under them
The lab could move the GUN and not the anchor points the hands are solved onto. `grip_offset` slides the weapon around inside the fist; `gun_fore` and `gun_stock` are distances ALONG the barrel, so the trigger and support hands could travel up and down the weapon's own axis and nowhere else. Nothing could take a hand off that axis, which is what a handguard below the bore, an angled foregrip, or a pistol whose grip is nowhere near its barrel line all need. Two things fix that. `grip_shift` and `fore_shift` give the two hand anchors real three-dimensional freedom, expressed in the GUN's own across/up/along frame so a sideways nudge stays sideways as the weapon pitches between low ready and ADS. Zero is exactly the old behaviour. Their z overlaps the along-axis distances, which is redundant and deliberate: keeping those separate is what lets the reach solver slide the support hand back down the handguard without undoing a considered sideways offset. And the markers are now draggable. They already showed the anchors; now they are handles. The one under the mouse swells and draws through the body — depth testing is right for judging whether a hand reached its target and wrong for a handle, because at any useful framing the hands occlude all three. Verified three ways, and each one had to be rebuilt once: anchor_shift_check first compared absolute positions and reported a 3.5 mm error that was the character BREATHING — there is a sin() on the muzzle pitch, so no anchor is ever in the same place twice. Measuring each anchor relative to the one it hangs off, rotated into the current gun basis, cancels the breathing, the ADS blend and the recoil exactly. 48 checks, six characters, both poses. anchor_drag_check asserts the drag writes the knob the MOUSE asked for, derived independently from the camera: 0.00-0.01 mm on all three. It does not assert the marker lands under the cursor, because it does not — the anchors hang off the shoulder and the arm chasing them moves the shoulder, so a drag settles at 0.77x-1.13x. Small enough to ignore interactively. That feedback first read as 1.5x-1.8x, because the cases were compounding on each other, and waiting LONGER for the pose to settle made it worse rather than better — which is the opposite of how a settling error behaves and is what gave it away. The buttstock case also failed for a while on a bug entirely in the test: it read an absent knob as zero when `pocket_hip` defaults to (30, -70, 60) mm. The lab has a note about that trap in `_reset`. It is just as easy to walk into from a test, and now has one there too. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
700d0925d7 |
tools: measure rest poses, and diagnose momo
momo's idle plays with her arms overhead and her waist pinched, and every assertion in the suite passes on her. The first suspicion was the retarget's rest-relative delta: it applies "what the clip does to the LIBRARY's rest" to THIS rig's rest, which quietly assumes the two rests are alike. rest_pose_check.gd tests that, and disproves it — miku's arms rest 41° off the library's and taila's 32°, and both animate correctly. The delta retarget handles a rest-pose difference, which is what it is for. Recorded in the reference so nobody spends that hour again. Writing the tool reproduced this project's own recurring mistake in miniature. Measuring "the direction from a bone to its first child" reported kiyoko's and aria's legs 71° off the library — because a thigh's first child is as likely to be a skirt bone as a shin, and it was measuring the hang of a skirt panel. Pointing it at the next limb BY ROLE dropped both to 1°. What momo actually has: `Root_001` through `Root_007` are in her driven_bones, and they are her HAIR roots — Hair_A is dominated by Root_001_001, Root_007 and Root_005. The animation is keying bones the spring solver is supposed to own, which is non-negotiable #2 broken by the role resolver rather than by a clip. The surface table corroborates it: her hair surfaces report 1.8% and 9.4% chain share, because most of their vertices belong to bones in no chain at all. Not fixed here. `Root_00N` matches no COSMETIC stem, and adding "root" to the stems would cost a rig whose actual root is called `Root` its hips. The structural fix is that a bone whose geometry is dominated by a mesh classified `hair` is a hair bone whatever it is called — which the surface table makes answerable at build time, and did not when momo was imported. It needs a Blender re-run and re-verification of all six characters. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
1e6f3001ac |
docs(skill): the surface table, rig anchors, and asserting the consequence
The skill is this project's own instructions, and three commits made it wrong:
it described a runtime that re-guessed what every surface was, a lab that only
tuned weapon holds, and a suite that could not see any of the last five defects.
It also still listed a seventh character that is not in skins.json.
Records what is new — the surface table and how it is decided, the layered
tuning files and why a fixed rotation on the weapon mount is the wrong answer,
the rig lab — and what the work taught:
* assert the CONSEQUENCE. Asking a model which clip it is playing reads a
variable it set on itself, and says "Idle" just as happily when nothing is
ticking. Asking whether an anchor saved says nothing about whether the gun
moved.
* four of the last five real defects came from looking at a PNG. Every one of
them passed every assertion.
* the line-work rule now exists twice, at build time and as the runtime
fallback, and they must be changed together or a model with a surface table
starts rendering differently from one without.
Also records momo's broken idle and the stray Icosphere under known-unsolved,
with what is worth suspecting first in each case, since neither is fixed.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
2a6b321984 |
fix(weapons): orient the support hand by frame, not by arc plus a twist
The support hand was upside down on the handguard. Its rotation was built as a shortest arc from the hand's forearm line to the barrel, plus a constant 0.5 rad twist. A shortest arc is the MINIMAL rotation between two directions and says nothing at all about roll, so the entire roll of that hand came from the constant — and a constant is right only for the one rig it was tuned against. Orienting a hand onto something it grips is a frame-to-frame problem, and saying it that way leaves nothing free to guess. A hand wrapping a cylinder has its curl axis along that cylinder, or the fingers close across the handguard rather than around it; and its palm faces the cylinder, which for a hand supporting from underneath means up. The third axis falls out of the other two. Map the hand's rest anatomical frame onto that target and the roll is determined rather than chosen. The frame is the same one the finger curl already uses — along, palm, curl — now stored whole instead of just its curl axis. Verified by render on the mannequin (Rigify names, 0.524 m arm) and Kiyoko (VRoid names, 0.470 m): fingers wrap the handguard from below and over the top, stock at the shoulder, arms not crossing, consistent across idle, ADS and run. L_HAND_TWIST is deleted rather than retuned — it was the bug. Smoke 0 failures. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
682597a0d2 |
feat(weapons): scale the gun to the arm, and close the hands around it
The hold was wrong on every character in three independent ways, all of them a constant standing where a measurement belonged. MOUNT. set_weapon seated the weapon with rotation_degrees = (0, 90, -90). A bone attachment is expressed in the BONE's axes and no two rigs agree on those, so one constant mounted the gun differently on every model. It never needed to be right — the pose layer aims by rotating the wrist until the weapon's forward lies on the aim line, so the identity means "forward is the hand bone's -Z", true on any rig, and the wrist absorbs the roll. SIZE. The set is modelled at real-world scale; an M4 is 0.84 m butt to muzzle and these characters have 0.47 m arms against an adult 0.52. That put the handguard 0.66 m from the support shoulder, 0.2 m past reach, so the support hand was slid back down the weapon until it fitted — on Taila from an authored 0.35 m to 0.083 m, which puts both fists together at the grip. Two hands on a pistol, not a rifle. Fixed by solving the support arm's triangle rather than picking a factor: its hand must reach stock+fore ahead of the pocket from a shoulder half a shoulder-width off the axis, so scale the gun to the largest that keeps the handguard inside that reach. Taila and Kiyoko now come out at their own scales (0.217 and 0.213 m of hand separation) with the support hand at its FULL authored handguard distance and the slide-back loop never firing. The loop also has a floor now: a straight support arm beats no handguard hold. FINGERS. Nothing posed them — every hand was flat and open, which is the loudest possible tell that a character is not holding anything. They close now, about an axis derived from each hand's own anatomy in the rest pose: along = wrist to middle knuckle, palm = middle knuckle to thumb tip (the thumb opposes the fingers, so it marks the palm side by construction), curl = along x palm. The trigger finger gets a much shallower curl than the rest, because it lies along the trigger. Finger bones resolve by ROLE across all three naming families met so far — Rigify DEF-f_index.01.L, VRoid J_Bip_L_Index1, Blender IndexFinger1_L — ordered by depth below the hand rather than by the number in the name, which is not consistent between them. Verified by render on Taila and by measurement on Kiyoko: stock at the shoulder, trigger hand on the grip, support hand out on the handguard, fingers wrapped, arms not crossing. Smoke 0 failures. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
c8b0337aa7 |
fix(characters): rig-independent weapon mount, measured facing, revert Miku
WEAPON MOUNT, all models. set_weapon() seated the gun with a fixed rotation_degrees = (0, 90, -90). A bone attachment is expressed in the BONE's axes and no two rigs agree on those, so one constant mounted the weapon differently on every character. It never needed to be right: the pose layer aims the gun by rotating the wrist until the weapon's forward lies on the aim line, so the identity means "forward is the hand bone's -Z", which is true on any rig, and the wrist absorbs the roll. The grip now sits at the bone origin, so the gun is in the hand rather than at an offset from a differently-oriented bone. Verified by render on kiyoko: rifle shouldered, both hands on it. FACING. flatten_and_scale() now measures toes-versus-ankles and snaps the character to face Blender -Y, the convention the runtime's blanket flip is built around. Kiyoko was 180 degrees off. Snapped to the nearest quarter turn so splayed feet in a rest pose are not read as a turned character. MIKU. Re-imported without --grow-cloth. Her grown hair chains were the cause of the stretching: cloth_bones.py clears a vertex's body weights and re-assigns it to the fitted polyline, so a poor fit does not degrade to "stiff", it degrades to "torn". She now has no hair simulation — stiff but correct — until that tool blends against the weights it replaces instead of destroying them. ARIA'S SKIRT IS NOT SIMULATED. Worth recording plainly, because it looks better than Taila's and the obvious conclusion is the wrong one: aria has ZERO cloth chains. Her skirt never clips because it is rigidly skinned and follows the legs it is weighted to. There is nothing to port to Taila except switching her simulation off. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
27c4117c25 |
fix(pipeline): stand the character up before scaling; reach bones by role
Fixes the two root causes behind four of the seven reported breakages, and rejects the source that cannot be fixed. STAND UP FIRST. flatten_and_scale() now derives the up axis from the skeleton and rotates the model upright before measuring anything. Aria and Momo are correct: 1.75 m tall, 1.43 x 0.41 and 1.52 x 0.39 across, verified by render. The up vector is measured from the FEET to the HIPS, not from the hips to the head. The head is not a reliable landmark — the spine walk ends on the last non-cosmetic bone in the chain, which on a rig with a facial skeleton can sit BELOW the hips. Momo's did, so the first cut of this fix stood her neatly on her head: right size, right proportions, upside down. Feet cannot be mistaken. REACH BONES BY ROLE. SkinnedPlayerModel gained _role_bone(), and set_weapon uses it. Four characters could not hold a gun because one hardcoded lookup knew three spellings and their hands are called "Right wrist" and "J_Bip_R_Hand" — both resolved perfectly in the sidecar the whole time. HIKARI IS REJECTED. She now fails the gate: her feet and spine disagree about which way is up, so the stand-up correction cannot resolve her either, on top of zero-length cosmetic bones and a second armature that was smuggling its own clips into the export. That is not a tuning problem, it is a file that has been through two toolchains. De-registered and removed rather than shipped broken — which is what the gate is for. Six GLB skins remain, all passing. Smoke 0 failures, 11/11 movement tests. Still open, recorded in the skill: kiyoko faces backwards, miku's grown hair stretches under animation, the mannequin's rifle hold does not convince, and taila's front skirt clipping. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
2b3e29dd30 |
fix(pipeline): check that a character is CORRECT, not merely well-formed
Seven characters shipped "All checks passed" and four were visibly broken
in game — lying on their backs at seven times scale, facing backwards, or
holding a gun that floated near their chest. Nothing in the suite was
wrong; it just never asked the questions that mattered. That distinction
is the whole lesson, and it is now written down in the skill as
references/failure-modes.md, generalised per failure.
Two root causes are measured and certain:
- flatten_and_scale() normalises the bounding box along Blender Z because
Z is up. For a model that arrives lying along Y that measures the
character's THICKNESS, so it scales by ~7 and leaves them on their back.
One assumption, both symptoms. The trap is that the normalised number
always comes out right — the exporter maps Blender Z to glTF Y, so "is
the height 1.75" passes on a character who is 7.5 m tall lying down.
All three casualties are VRM files that went through a Blender
round-trip and came back with a baked axis rotation.
- SkinnedPlayerModel.set_weapon() finds the hand with three hardcoded
spellings, which match none of the four non-Rigify rigs — their hands
resolve perfectly in the sidecar as "Right wrist" and "J_Bip_R_Hand".
When it misses, the weapon is parented to the model root at a fixed
chest offset, so it is not attached to the character at all. Same class
of bug as the leg check that name-matched thigh/shin. rig_map.py exists
so nothing downstream has to guess a bone name; only some consumers read
the roles it publishes.
Three new hard checks, none needing more than the vertices and the
sidecar:
character stands up in world space — against WORLD up, not against the
model's own proportions. "Is the spine the longest axis?" catches
nothing: a model rotated as a whole is internally consistent and
passes it comfortably.
character is a plausible size / height
every role the runtime needs is resolved
They separate the four good characters from the three broken ones on the
first run. Also fixed the posture measurement to read vertices rather than
object.bound_box, which is cached and still stale right after an import —
it reported a 1.75 m character as 1.18 m tall.
Recorded but not yet fixed: kiyoko faces backwards (facing is inferred by
two independent mechanisms and verified by neither), miku's grown hair
chains stretch under animation (generated chains are never validated
against the geometry they drive), and the mannequin's rifle hold does not
convince despite resolving correctly.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
2ce2175d99 |
docs(skill): record what four non-library rigs taught the role resolver
Names lie and anatomy does not — the four VRoid imports each broke role resolution differently, and the lesson generalises: anything guessing anatomy from a bone name needs a structural fallback. Also lists what is shipping and which characters have chains that do not simulate. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
cd0d1b2d99 |
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]> |
||
|
|
7892669319 |
feat(pipeline): grow skirt and hair bone chains for a costume that has none
Miku shipped with 0 cloth chains against Taila's 35, so her twin tails hung off her skull like a helmet. Nothing downstream could fix it: the spring solver simulates cloth BONES, and a garment with none is welded to whatever body bone it was weighted to. Every auto-rigged model is in that state, and her source was an unrigged mesh. tools/cloth_bones.py builds them, which is the job a technical artist does by hand on a model like this. It finds the geometry by MATERIAL SLOT — the artist already answered which surface is hair, and on a joined mesh (what the auto-rig leaves behind) the slot is the only separation left. Hair is split into connected islands, because a strand is a connected piece of surface and clustering by position would merge two ponytails passing near each other. A skirt is split into radial wedges instead, because a skirt is ONE connected surface and islands would return the whole thing as a single piece — the bell-shaped failure. Each clump gets a polyline fitted down its middle by binning vertices by distance and taking centroids, so the chain follows the piece's own curve rather than cutting the corner on a bend, and vertices are re-weighted onto it while the first 22% keeps its original body weight so the scalp stays on the skull. On Miku: 19 chains, 57 bones from one `hair` slot. Sidecar 0 -> 19 chains. Idle stability 0.007-0.018 deg/frame. Mesh intact, verified by render. Opt-in, via `pipeline.py --grow-cloth`, and run before the retarget so describe_rig() finds the chains by name exactly as it would an artist's. Known limits, recorded in the skill: it cannot find a garment sharing a material with the body (Miku's skirt is on her `body` slot, so she got hair and no skirt), and grown chains are a fallback — an artist's chains carry intent that no geometric fit recovers. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
4559a1adc3 |
docs(skill): record why Miku has no cloth — the source had no skeleton
The two shipped characters are a controlled comparison: Taila's source arrived rigged (35 cloth chains, 8 twist bones, 18 meshes, artist weights), Miku's did not (5 meshes, 0 joints), so Miku was auto-rigged into one mesh with nearest-bone weights and no cloth chains at all. Her twin tails and skirt are dead geometry and the destructive load-time weight repair runs on her every spawn. None of that is recoverable downstream, which makes picking a source that already has skirt and hair bones the highest-leverage decision in the pipeline. Added the no-Blender check for vetting a candidate. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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]> |