Commit Graph
303 Commits
Author SHA1 Message Date
Nicholas ButzkeandClaude Opus 5 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]>
2026-07-26 13:46:23 -04:00
Nicholas ButzkeandClaude Opus 5 fc9e6f4275 feat(characters): four VRoid characters imported and selectable
Kiyoko, Hikari, Aria and Momo, all CC-BY from Sketchfab, licences recorded
beside each skin. Seven GLB characters selectable now.

  kiyoko  13 meshes  20 cloth chains (62 bones)  0.0% cross-leg bleed
  aria    15 meshes  15 cloth chains (37 bones)  0.0%
  momo     5 meshes   9 cloth chains (35 bones)  0.0%
  hikari  13 meshes  10 cloth chains (37 bones)  0.2%, 12 twist bones

These are the first characters imported that were NOT authored against the
library's own bone spelling, and every one of them broke something that
had been quietly wrong all along. All four failures were in code that
guesses anatomy from names, which is exactly what tools/rig_map.py exists
to stop doing:

- LIMB ROLES went to the first role in LIMB_ORDER that matched at all, so
  shin's catch-all "leg" claimed UpperLeg before thigh's exact "upperleg"
  was ever consulted, and the thigh went unassigned. The result depended
  on the order bones arrived in. Claims are now granted longest-stem
  first. This also broke Mixamo (LeftUpLeg/LeftLeg) and had simply never
  been hit, because every character so far used Rigify DEF- names.

- Names cannot settle thigh-vs-shin at all. A bare "leg" is the SHIN on
  Mixamo and the THIGH on a rig whose shin is "knee" — both common, same
  token, opposite bones. RigRoles now walks the leg from the foot upward
  and fills in whatever the names could not, stepping over twist bones.

- verify_character.py looked for legs by the substrings "thigh"/"shin",
  which VRoid spells UpperLeg/LowerLeg. It declared every locomotion clip
  static while the legs animated perfectly, and the cross-leg bleed check
  found no leg vertex groups at all and passed vacuously. Two green-
  looking lies from one missing lookup; both now read the sidecar's
  resolved roles.

- The cosmetic/spring classifier matched whole tokens only, so Momo's
  HairFL / HairFR / HairF_Top tokenised to "hairfl" and matched nothing.
  She imported with six chains, all bust, and no hair. Both classifiers
  now share one rule that also accepts a two-character positional suffix,
  which is short enough that "forearm" and "earring" are still untouched.

Two more pipeline fixes:

- A source model's own clips leaked into the export. Clearing bpy.data
  .actions before the library import is not enough — hikari carried two on
  a second armature's NLA tracks, and NLA_TRACKS export mode ships
  anything in a track anywhere in the file. They export as rest-pose
  statues. Now everything not retargeted is stripped from every object.

- verify_character.py gained a check for cloth chains with no measurable
  extent. A chain whose bones are zero-length is dropped by the runtime
  and simulates nothing, while the sidecar still cheerfully reports it.

Known: hikari's ten cloth chains are all zero-length and her collider fit
found nothing, so her costume does not simulate — her rig has been through
two toolchains and its cosmetic bones are empty terminators. She animates
correctly otherwise. The new check now reports this instead of hiding it.

tools/retarget.py also carries local working-tree changes that predate
this session.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-26 13:45:27 -04:00
Nicholas ButzkeandClaude Opus 5 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]>
2026-07-26 13:25:26 -04:00
Nicholas ButzkeandClaude Opus 5 270d5f0973 chore: track Godot uid files for the new debug tools
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-26 13:19:55 -04:00
Nicholas ButzkeandClaude Opus 5 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]>
2026-07-26 13:19:44 -04:00
Nicholas ButzkeandClaude Opus 5 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]>
2026-07-26 12:47:42 -04:00
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
Nicholas ButzkeandClaude Opus 5 040b595397 feat(anim): point the legs where the character is actually going
The clip library has one forward locomotion cycle and no strafe or
backpedal clips, so a character sidestepping ran forwards on the spot
while sliding sideways. Nothing in the animation said which way they were
travelling and a body lean was carrying the whole burden of telling the
player.

Yaw the HIPS onto the travel direction and unwind it up the spine. The
legs hang off the hips, so the whole stride turns with them and it costs
no new animation; the chest keeps facing roughly where the player aims.
Past about a right angle the hips cannot follow, so the cycle plays in
reverse and the legs point the other way — a real backpedal instead of a
moonwalk. The regime is hysteretic and the yaw is eased, so crossing
between them reads as a pivot, which is what a person does there.

The lean moved into the travel frame with it. Leaning "forward" along the
facing while the legs run off to one side leans them sideways relative to
their own stride, which is what being dragged rather than running feels
like. It is also driven by how hard the character is moving rather than
by signed forward input, so a sidestep leans into its own stride instead
of standing straight up.

debug/travel_dir_check.gd measures it. How far the stride points from the
actual direction of travel:

  forward 9°   fwd-diagonals 5-6°   backpedal 2°   back-diagonal 2°
  pure sidestep 39-42°, which is the deliberate hip cap

Two things worth knowing about that tool. It reads bone poses from inside
the modifier pass, because Godot restores them afterwards and anything
read later is the animation with the pose layer missing. And it averages
over a full stride: a run cycle twists the torso against the hips by tens
of degrees twice per stride, so a single-frame sample measures the clip,
not the layer.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-26 03:16:24 -04:00
Nicholas ButzkeandClaude Opus 5 0dd9d01ac7 fix(cloth): solve the garment instead of repairing it five times
The skirt glitched when the character moved and the thigh still came
through it. Both came from the same place: the solver integrated one
spring per bone and then ran four more passes behind it — resolve the
collision against the target, resolve it again against the answer, relax
the cross-panel links and rebuild every pose from the corrected tips,
then walk a separate ancestor "lift" — each writing bone poses the next
read back and partly undid. The lift wrote poses that were never fed back
into the spring state at all, so every frame began by pulling against a
pose the springs did not know about.

Replaced with one position-based solve, the shape Magica Cloth 2's
BoneCloth uses. Every JOINT is a particle, so a bone's head can move;
predict with inertia in the anchor's frame; relax length, bend, backstop,
the cross-panel links and the colliders together; convert to rotations
once at the end. A contact with no rotational leverage is now resolved by
the panel moving, which is what a bodily chain push, an ancestor lift and
a drape weight were each approximating separately.

Measured, at a dead-still idle and over a movement sweep:

  idle jitter (skirt)      0.53 -> 0.025 deg/frame, worst 24 -> 1.9
  settling after a dash    103 -> 18 mm of leg left inside the skirt
  fall / air / walk         82 -> 40, 96 -> 75, 72 -> 76 mm
  run / slide / dash        unchanged, ~95 mm

The idle buzz and the failure to come home after a hard move are gone —
those were the "glitches out". Peak clipping in a run, a slide and a dash
is NOT fixed and is still around 95 mm.

Four things this turned up on the way:

- debug/cloth_clip_check.gd was measuring the animation, not the render.
  Godot restores bone poses after the modifier pass, so reading them with
  force_update_all_bone_transforms() afterwards sees nothing any modifier
  did. It reported the same ~95 mm with collision fully enabled and with
  it commented out. It now observes from inside the pass. Every number
  ever taken from this tool before now was measuring the wrong pose.

- The collision hulls came from ten farthest-point samples per bone,
  which describe a panel's corners and hem and leave its MIDDLE unsampled
  — exactly where a thigh comes through. Built from the real mesh at load
  time instead.

- The drape term is gone. It was there to move a panel the old solver
  could not, and once the solver could, it was worse in every state but a
  walk and cost 20x in stability: its target sat inside the leg the
  collision was pushing out of, so the two ran against each other forever.

- Cost was 10.9 ms per character. The inner loop rebuilt every capsule
  and reallocated the hull array for every (bone, collider, pass). Now
  2.6 ms at full quality with a distance LOD behind it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-26 03:07:30 -04:00
Nicholas Butzke 374d9f9822 feat: implement automated 3D character pipeline with retargeting and rig management tools 2026-07-24 20:52:35 -04:00
Nicholas ButzkeandClaude Opus 4.8 afc954e129 fix: no limb vertex may carry opposite-leg weight — thigh collapse gone
The thighs going thin was never the knee bend. It was inner-thigh vertices
keeping ~20% of the weight of the OPPOSITE leg, so a split stride tore them
across the body. Printing the weight composition of the worst-collapsing
vertices is what finally showed it: a right-thigh vertex reading
DEF-thigh.R=0.58 DEF-shin.R=0.21 DEF-thigh.L=0.13 DEF-shin.L=0.08.

Every earlier pass had a guard that let these through. The below-knee pass does
not reach them; the above-knee pass demanded one leg chain be 1.25x nearer than
the other, which excludes everything near the centre line — precisely where the
damage was; and the limb-radius gates are measured from the bone AXIS, so a
vertex on the front or back of a thigh clears them easily.

SkinLegRepair now ends with an unconditional pass: any vertex within a generous
radius of either leg chain is forced onto the leg that actually drives it, at
any height. It runs last so none of the staged rules can reintroduce the
problem.

Measured per vertex, across jump, fall, run and dash:

                     before        after
  Body below 0.80      70            0
  Body worst          0.46         0.82
  ClothCAndW below 0.80  107          0
  ClothCAndW worst    0.66         0.85
  ClothB worst        0.81         0.87

No vertex anywhere on the legs now loses more than 18% of its thickness, down
from 54%. The skirt still deforms freely (0.87-0.88) — clothes flow, limbs hold.

Also fixed in SkinJointHelper along the way: it picked the FIRST joint chain
that matched a vertex rather than the one holding most of its weight, which
bound right-thigh vertices with stray left-leg weight to the LEFT knee's helpers
and dragged them across the body. It now picks the dominant joint and requires
the joint to own at least half the vertex.

Taila snaps 2021 vertices, Miku 659. FSM tests 11/11, spawn smoke test 0
failures.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 09:03:45 -04:00
Nicholas ButzkeandClaude Opus 4.8 d33f9cd812 docs: record why bone-scale volume preservation does not work here
Tried widening the helper bone to cancel the joint collapse. The geometry is
right — blending two rotations θ apart thins anything perpendicular to the bend
by exactly cos(θ/2), which is |q.w| of the pose rotation, so scaling the helper
across the bone by the reciprocal is the exact correction.

It does not work, because a BONE scale is the wrong granularity. It fattens
every vertex weighted to the helper whether or not that vertex was collapsing:
parts of the thigh ballooned to 167% while the worst-collapsing vertices, which
are not on the helper at all, stayed at 0.83. The idea needs per-vertex
correction and a bone cannot express it. Removed, with the reasoning kept in the
file so it is not retried.

Hip joints were also retested, since two other variables had changed since the
first attempt (one-joint-per-vertex, and the limb gate removed). Same verdict a
second time: upper thigh 0.90 -> 0.84, and the skirt 0.80 -> 0.72. Knees only.

Shipped state is unchanged from the previous commit; this is the reasoning and a
revert of an experiment.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 08:49:14 -04:00
Nicholas ButzkeandClaude Opus 4.8 1ab7e99231 fix: handle the knee bend with a joint, and record what made it worse
Renames SkinKneeHelper to SkinJointHelper and generalises it to a list of
[parent, child] joints, so the half-angle joint technique is stated once and can
be pointed at any joint. It stays pointed at the KNEES only, and the header now
says why, because "add more joints" is the obvious next idea and it is wrong
here.

Three things were tried this round and measured, on the thigh, per vertex,
against the rest transforms:

  * hip and ankle helpers as well as knees: the knee got WORSE, 0.80 -> 0.50,
    and the skirt went 0.80 -> 0.72. The hip mostly swings rather than folding,
    and linear-blend skinning handles a swing far better than a fold; the helper
    just added another blend for nothing. Reverted.
  * gating the re-weight to vertices near the leg bone chain, to keep the hip
    helper off the skirt: that also excludes the OUTSIDE of the knee, which sits
    further from the bone axis than the limb radius and is exactly the geometry
    that needs helping. Knee went back to 0.50. Removed — with knees only there
    is no skirt to protect against.
  * one joint per vertex (break after the first match): kept, since a second
    job would read the bones the first just rewrote.

Net result on the thigh, worst per height bucket during a jump:

  y=0.60  0.80 -> 0.83     y=0.75  0.84 -> 0.84
  y=0.65  0.81 -> 0.84     y=0.85  0.90 -> 0.90

and the skirt is untouched at 0.80/0.84, still free to move.

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 08:43:56 -04:00
Nicholas ButzkeandClaude Opus 4.8 0bb1db0806 revert: let the skirt move again; keep the rigid-limb work
The previous commit stiffened the skirt onto the hips to stop it flattening at a
wide stride. That was the wrong trade: it made the clothes read as a rigid shell
welded to the pelvis, which is a worse look than the flattening it fixed, and it
broke the model more than the original bug did. Reverted in full.

What stays is the work that makes the HUMANOID hold its shape — below-knee side
snapping, the above-knee limb pass, the torso cap, the half-angle knee helpers,
and the cross-leg triangle drop. Those only ever touch vertices hugging a leg's
own bone chain.

The header now states the rule the passes are actually built around, because it
is what keeps getting violated: limb vertices are cleaned up hard and must not
deform; drape vertices are left alone and must be free to move. It also records
the skirt-stiffening attempt so it does not get retried.

Measured split now, from debug/limb_deform_check.gd: the body holds 0.98 of its
cross-section at the knee while the skirt sits at 0.88 — rigid limbs, moving
cloth. Taila snaps 1404 vertices (down from 4024 with the skirt pass), Miku 434.

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 08:31:25 -04:00
Nicholas ButzkeandClaude Opus 4.8 20b70e5eda fix: hang the skirt from the hips so it stops collapsing at a stride
It was the SKIRT, not the legs. Every leg check I ran hid the skirt to see the
thighs, so I was never looking at what is actually on screen. Rendering the full
model side-on shows it immediately: at rest the skirt is a flared shape with
real volume, and the moment the legs split for a run or a jump it collapses flat
against the body. The lower-body silhouette loses its shape, which reads as the
legs squashing.

Cause: the skirt is weighted to BOTH thighs. At a split stride the two thighs
pull it in opposite directions and linear-blend skinning averages them, so the
skirt is dragged inward and flattened. The earlier repair deliberately left it
that way, on the reasoning that a skirt should be free to drape across both
legs — correct in principle, wrong at these stride angles.

Draping cloth now hangs from the hips: vertices that clear both leg bone chains
keep at most 20% leg influence and the rest goes to DEF-hips, so the skirt holds
its shape and swings as a unit. Limb vertices are still snapped to one leg, so
the two are now modelled differently on purpose.

Bounded ABOVE THE KNEE. The first attempt used distance from the bone chain
alone, and chunky boots sit further from the ankle than the limb radius, so they
read as "drape", lost their leg weight and trailed off the hips — the legs came
out far worse than before. A skirt hem is above the knee; boots and stockings
are not.

Legs unchanged by this: knee cross-section stays 0.99, and the knee helper work
from the previous commit is intact. Taila snaps 4024 vertices, Miku 670.

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 08:20:27 -04:00
Nicholas ButzkeandClaude Opus 4.8 050011666c fix: half-angle helper bones at the knees kill the candy-wrapper collapse
This is the leg squashing. It was at the KNEE, and every previous measurement
missed it because they averaged a ring of vertices, which hides a collapse that
only affects part of the ring.

Measured per vertex instead, against an exact rest-transform baseline: 532 of
Taila's leg vertices lose more than 20% of their distance from the bone axis and
the worst lose 40%, all at y=0.54 — exactly the knee — and worst in the air pose.
That is "squashing while running or jumping, easy to see from the side".

Cause is inherent to linear-blend skinning: it averages TRANSFORMS, not shapes.
A vertex half on the thigh and half on the shin is placed at the average of two
rotations, and at a hard bend that average falls well inside the leg, pinching
it to a ribbon.

SkinKneeHelper adds a bone at each knee, parented to the thigh and holding the
shin's rest transform, driven every frame to HALF the shin's rotation from
inside the skeleton's modification pass. Blend-zone vertices are re-weighted off
the thigh/shin pair and onto the helper, taking equal weight from each side and
reusing the slot that falls to zero — so no extra influence slot is needed and
the weights still sum to exactly 1. The sharpest blend any vertex now sees is
half the knee angle, and halving the angle roughly quarters the collapse.

Result: vertices losing more than 20% of their radius go from 532 to 0 on the
body. The worst anywhere is now 0.79, on the outline shell, which is hidden.
Verified visually too, legs rendered bare from the side through rest, run and
jump: knees hold their volume and the bind is intact.

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 08:04:24 -04:00
Nicholas ButzkeandClaude Opus 4.8 e707229283 fix: stop the torso holding back the top of the thigh
Hiding the skirt and jacket and rendering the legs alone, side-on, finally
showed where the deformation actually is: not the knee, the HIP. The top of the
thigh flattens into a wedge at full stride. That matches the worst number the
corrected measurement reports — cross-section 0.85 at the hip, against
0.97-0.99 at the knee — which had been dismissed as ordinary.

Cause is the usual linear-blend failure at a big rotation: upper-thigh vertices
are weighted between the near-static hips and a thigh swung far out, and the
average of those two transforms collapses the top of the leg. SkinLegRepair now
caps torso (hips/spine) influence on LEG vertices at 15% and hands the excess to
the leg bone that already dominates them. Only vertices within the limb radius
of a leg bone chain are touched, so the skirt keeps swinging from the hips.

Hip cross-section 0.85 -> 0.88, and the flattened wedge is visibly fuller in the
side-on render. This is a real improvement but a modest one; the legs are slim
low-poly geometry and some collapse at a 60 degree hip rotation is inherent to
linear-blend skinning.

Taila now snaps 1217+ vertices, Miku 434.

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 07:57:19 -04:00
Nicholas ButzkeandClaude Opus 4.8 1e12c36cca tools: give the limb deform check a baseline that validates itself
The measurement was comparing skinned lengths against BIND-POSE lengths. A
character is never in bind pose, so ordinary posing registered as deformation:
with the animation frozen it still reported 0.42x-2.56x edge ratios, and during
a run it reported edges collapsing to 0.08x and blowing out to 5.2x. Those
numbers were an artefact of the metric, not the model.

The baseline is now built from the skeleton's REST transforms, so every ratio
reads exactly 1.00 on an unposed model and the tool validates itself. Corrected
readings on Taila through the full runtime stack, worst over run, walk, jump,
fall, slide and dash:

  lengthwise stretch  1.07 Body, 1.08 ClothB, 1.09 ClothCAndW, 1.03 shell
  cross-section       0.97-0.99 knee, 0.85 hip, 0.92-0.94 ankle

That is ordinary linear-blend skinning.

Also records, in the header, the two ways this measurement was previously wrong,
because both nearly produced a bad "fix":

  * the bind-pose baseline above;
  * flagging vertices weighted across "non-adjacent" leg bones with
    max_slot - min_slot >= 2, which flags shin+foot+toe — a perfectly normal
    contiguous run. A repair pass built on that was written and reverted before
    it shipped; it was stripping legitimate toe and shin influences.

Checks that did come back clean and are worth not repeating: bone pose bases are
pure rotations to 0.00001, bone lengths never drift from their rest offsets, and
of 4019 coincident-vertex groups not one carries mismatched weights, so there
are no split seams tearing at the joints.

No character or gameplay code changed. skin_leg_repair.gd is byte-identical in
behaviour to the last commit; only its header comment gained a pointer to this
tool.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 07:51:18 -04:00
Nicholas ButzkeandClaude Opus 4.8 204846bf09 tools: measure limb deformation instead of eyeballing it
Adds debug/limb_deform_check.gd, which skins the leg vertices by hand and
compares them against the rest pose across a sweep of run, walk, jump, fall,
slide and dash. It reports lengthwise stretch (bending a knee can only shrink
this, so growth is genuine stretching) and cross-section loss (the linear-blend
"candy wrapper" collapse at a bent joint).

No gameplay or character code changes in this commit. Current readings on
Taila, through the full runtime stack:

  lengthwise stretch   1.05 worst (Body, ClothB, ClothCAndW), 1.03 outline shell
  cross-section        0.93 knee, 0.84 hip, 0.91 ankle

Sanity check on the method: the measured rest span of the leg vertex cloud is
0.818 m against 0.843 m of thigh+shin bone length, so the skinning maths is
producing real positions rather than plausible-looking noise.

That is ordinary linear-blend skinning. Combined with the earlier findings — no
bone pose basis deviating from a pure rotation by more than 0.00001, and no
bone length drifting by more than 0.0000 m — the leg geometry is not squashing
or stretching in any measurable way, so there is nothing here to fix blind.
Committing the instrument so the next round starts from a number.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 07:39:42 -04:00
Nicholas ButzkeandClaude Opus 4.8 a6e009d24a fix: repair thigh weights too, and keep the gun shouldered at low-ready
Legs: the previous pass deliberately stopped at the knee, on the reasoning that
cross-leg weighting above it is legitimate for skirts. It is — but the LEGS
themselves are up there too, and 122 vertices on Taila's thigh mesh carry up to
35% of the opposite leg. Running and jumping are exactly what splits the legs
far enough for that to show, which is why the cuffs came good while the legs
kept squashing.

The repair now runs above the knee as well, but only on vertices that belong to
a leg. Limb membership is decided by distance to that leg's own bone chain, not
by height and not by surface name: a thigh vertex hugs its bone, while a skirt
vertex hangs clear of both chains and is left blended so it can still drape over
the two legs. The radius is half the gap between the thigh bones, so it scales
with the character rather than being a magic number. Taila now snaps 1217
vertices (was 720), Miku 370 (was 214).

Weapons: at low-ready the stock was dropped 0.20 m below the shoulder joint down
to the ribs, with the muzzle pitched 22 degrees down. That pulls the whole
weapon off the shoulder and out of the supporting arm — it read as dangling
from the hands rather than being carried. Low-ready now stays in the shoulder
pocket and merely relaxes out of it (0.07 m drop), with a shallow 9 degree
muzzle tilt. All six weapons now sit in the shoulder with both hands on them.

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 02:08:07 -04:00
Nicholas ButzkeandClaude Opus 4.8 f60941d1fb fix: stop the boots being dragged by both legs; keep the support hand on the gun
Found the real cause of the leg stretching, after three wrong guesses.

720 vertices on Taila are weighted to BOTH legs at once below the knee — 262 in
the boot/cuff surface, 208 in the outline shell, 176 in the body, the worst at a
dead-even 49/51 split. A vertex pulled equally by both feet sits halfway between
them and STAYS there while the legs separate, stretching every triangle around
it. That is the band between the ankles and the elongated boot that reads as the
legs being squashed. SkinLegRepair snaps each below-knee vertex to whichever leg
already dominates it, renormalises, then drops the few triangles still spanning
the two legs (48 on Taila, 0 on Miku, which needs 214 vertices snapped).

What finally located it was reproducing the artefact through the FULL runtime
stack in a bright scene. Every previous "clean" render had bypassed the game's
animation stack — playing the clip straight off the GLB's AnimationPlayer with
no AnimationTree and no pose modifier — so it could never show the bug. With the
real stack the boot visibly stretches, and freezing the AnimationTree at the
rest pose renders it perfectly, which proves the pose is fine and the skinning
is not. Confirming measurements, all through the runtime stack during a run:
no bone's pose basis deviates from a pure rotation by more than 0.00001, no
bone's length drifts from its rest offset by more than 0.0000 m, and no
below-knee vertex is influenced by any non-leg bone. There is no squash-and-
stretch in this rig; there never was.

The two earlier attempts at this are both superseded: re-weighting by
triangle-neighbour majority (tore the cloth) and deleting cross-leg triangles
(too narrow — 24 triangles against 720 bad vertices). Hiding the model's own
outline hull, from the last commit, stays: that hull is genuinely broken and
redundant, it just was not the whole story.

Weapon support hand: gun_fore was a single constant 0.35 m for every weapon, so
on a short gun it hung the off hand out past the muzzle with nothing to hold —
that is the "not supported by the arms" case. It is now clamped to 80% of the
weapon's own grip-to-muzzle distance, which only bites on the short guns (MP7
0.35 -> 0.20). All six weapons now sit in the shoulder with both hands on them.

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 01:25:04 -04:00
Nicholas ButzkeandClaude Opus 4.8 28dc255d17 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]>
2026-07-21 22:20:02 -04:00
Nicholas ButzkeandClaude Opus 4.8 ec6b8228da fix: cut the welded ankle band, seat the gun in the hand, flash at the barrel
1. Ankle cuffs. The previous pass re-weighted the offending vertices to the
   nearer leg, which was the wrong call: the strip between the ankles is REAL
   geometry spanning the gap (~10 cm of ClothCAndW crossing x = -0.05 .. +0.05
   at ankle height), so re-weighting only tore it in half — a visible seam that
   still stretched. A triangle with one corner weighted to each leg has no
   correct pose; it must stretch the moment the legs separate. SkinMeshRepair
   now deletes those triangles instead (48 on Taila: the cloth band plus its
   outline shell). The cut is limited to BELOW THE KNEE, taken from the
   skeleton's own rest pose rather than a hardcoded height, because above the
   knee cross-leg geometry is legitimate — the skirt and shorts genuinely span
   left-thigh to right-thigh weights at the crotch.

2. The M4 floated because _measure_weapon derived the grip from mesh AABBs, and
   the FBX guns report bind-pose bounds tens of metres across — it measured the
   M4 as 24 m long and pushed the gun 7.5 m in front of the character. Bounding
   boxes are simply not trustworthy for these meshes. The grip was already
   authored elsewhere: WeaponManager places the first-person viewmodel's hands
   at fixed points in weapon space, and every weapon marks its barrel tip with
   muzzle_flash.position. Those move to WeaponGrips (dependency-free, so both
   the weapon system and the character models can use it without dragging each
   other's load order along) and third person now reaches for exactly the
   points the viewmodel uses. Every weapon's grip now lands 0.073 m from the
   hand — the M4 included, down from 2.32 m — with barrel lengths that match
   the models (0.30 m for the MP7, 0.68 m for the DMR).

3. Muzzle flash and tracers were spawned off the viewmodel's muzzle. The
   viewmodel is parented to the camera, so its muzzle sits inside the player's
   head — in third person the flash appeared by the character's shoulder.
   world_muzzle() returns the muzzle of the gun actually in the character's
   hands whenever the character is what the viewer sees, and the networked
   fire-effect RPC now sends that position too, so remote players stop seeing
   tracers leave the shooter's head. Measured: the third-person origin sits at
   the held gun, 0.54 m below the head, instead of on the camera.

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-21 19:16:22 -04:00
Nicholas ButzkeandClaude Opus 4.8 35ada4f34a fix: unlink the ankle cuffs, stop the reload head-dive, soften every blend
Three animation complaints, measured before changing anything.

There is NO squash-and-stretch. Instrumented every bone every frame: the
worst deviation of a pose basis from a pure rotation is 0.00000. Every clip's
position tracks match the skeleton's rest pose to within a millimetre (only
DEF-hips translates, as it should) and no scale key deviates from 1. The
"squishing" is a deep BEND, not a scale: the shared library's PistolReload was
authored for someone watching their own hands, and it dives the head 33° with
the whole neck chain at 67°. At that depth Taila's head/hair weights pinch and
the skull changes shape. Dropping neck+head from the upper-body one-shot's
filter leaves them on the locomotion layer: head dive is now 14° and the neck
chain 48°, and the character keeps looking downrange through a reload, which
is what a shooter wants anyway. Trade-off: Throw/Hit no longer swing the head.

The ankle cuffs are not linked by the rig or the clips — the foot bones swing
independently (left/right Z correlation -0.94). They are linked by SKIN
WEIGHTS: 16 vertices belonging to the LEFT cuff, sitting at x = -0.002 just
across the centre line, are weighted to the RIGHT leg, so the far foot drags a
band of cuff across the gap. Position alone cannot classify them (the cuff
they belong to spans x = 0.00 .. 0.05), so SkinWeightRepair decides by
CONNECTIVITY: a vertex that disagrees with 80%+ of the vertices it shares
triangles with gets its leg influences mirrored. Ankle-height cross-leg
triangles go 24 -> 0 on Taila; the pass is conservative enough that Miku needs
exactly 1 vertex and her face/hair/skirt are untouched (only leg bones are
ever considered). Meshes with blend shapes are skipped rather than rebuilt.

Blending: nothing cuts hard any more. Base cross-fade 0.15 -> 0.22, locomotion
0.28-0.30 (Idle/Walk/Run/Sprint switch constantly as speed drifts across their
thresholds, which is where the snapping showed most), reaction moves raised
off their near-instant values but still snappy, and the upper-body one-shot's
own fades widened to 0.14 in / 0.22 out.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-21 18:56:21 -04:00
Nicholas ButzkeandClaude Opus 4.8 d8b0c08611 fix: Taila reads like her source render — no white rim, matte hair
Two separate causes behind "too glossy with a thin white outline":

1. The model carries its OWN cel line-work as extra, UNTEXTURED surfaces —
   an inverted-hull outline shell plus eye-line/highlight cards (Taila names
   them FullBlack / EyesFullBlack / EyesInvL / EyesHL). They are authored to
   read as flat black, but the glTF import hands them a default near-white
   albedo, and apply_toon_recursive then LIT them. That is the thin white
   rim: a pale, toon-shaded outline shell tracing every hair strand. They now
   render unshaded flat ink (flat white for the "HL" highlight card).

2. The skin textures already have cel shading painted in. Stacking our hard
   3-tone break on top produced a bright stripe that slid across the hair as
   the camera moved — the "gloss". Characters now get a soft terminator and
   an almost-invisible second step, so the painted shading carries the form.

Both live in a new LevelMaterials.apply_character_look(), called only for
imported character GLBs, so props and level geometry keep exactly the crisp
banding they were calibrated with. The one shared change is that the toon
shader's second-step darkness is now the mid_tone uniform, defaulting to the
0.82 that was hardcoded — no change for existing callers.

Character shadow tint (0.64, 0.56, 0.72) is measured off the Sketchfab
reference render rather than guessed: sampling the hair there, shadow/midtone
lands near (0.63, 0.53, 0.70). Green drops hardest, which is what keeps
copper hair copper instead of washing it to brown.

debug/character_look_capture.gd renders the raw import beside our treatment
under matched lighting — the comparison this was tuned against.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-21 17:54:41 -04:00
Nicholas ButzkeandClaude Opus 4.8 af8c9831c9 feat: thinner cel outline + procedural model actually holds its weapon
Outline: the inverted-hull width was tuned when the look was heavier and
now reads as a thick marker stroke. Cut every call site to roughly a third
(characters 0.012 -> 0.004, level geometry 0.015 -> 0.005, Neon Alley's
size-scaled clamp 0.02..0.05 -> 0.007..0.018) plus the shader default, so
the ink line stays readable without fattening every silhouette.

Third-person weapon on HumanoidModel: the gun was parented to root_pivot at
a fixed (-0.15, 1.0, 0.4) — floating 40 cm off the chest while both arms
posed at it from a distance. Give the rig a real right-hand node at the end
of the forearm and hang the weapon there, so it travels with the arm. Its
aim is re-derived from the body each frame (hand_r_pivot's global basis is
copied from root_pivot), otherwise the barrel would swing around with the
arm animation. The hold pose is retuned to match: right elbow tucked at the
ribs with a level forearm, left arm crossing to the handguard.

_seat_weapon_in_hand() slides each weapon along its own barrel axis so a
plausible grip lands in the fist regardless of where the artist left the
model origin. It measures with MeshInstance3D.get_aabb(), not
mesh.get_aabb() — the FBX gun parts are skinned, so the Mesh resource still
carries bind-pose bounds tens of metres across.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-21 17:32:37 -04:00
Dotts 9f60982416 Merge pull request 'Feat/visual audio overhaul' (#21) from feat/visual-audio-overhaul into main
Reviewed-on: #21
2026-07-21 14:09:17 -07:00
Nicholas Butzke 1172e465c1 chore: add UID file for bone dumping debug utility 2026-07-21 17:08:50 -04:00
Nicholas ButzkeandClaude Opus 4.8 0d125dc03f fix: third-person gun actually held — IK to the weapon, real recoil, no flip
Four separate third-person defects, one shared root: the arms were posed at
art-directed ANGLES that merely approximated the gun, and a stuck one-shot
flag kept handing them back to the animation clip.

- The rifle hold was silently DEAD after the first reload or throw. The gate
  trusted AnimationNodeOneShot's `active` parameter, which never clears, so
  clip_owns_arms stayed true forever and the clip drove the arms while the
  gun hung off the hand. `_upper_lock` (a timer we own) is now the
  authority, and the one-shot is explicitly faded out when it expires.
  This alone is why the gun was never held correctly.
- Arms are now solved with real two-bone IK (`_ik_arm`) onto points derived
  from the WEAPON: the stock is anchored in the shoulder pocket, the grip
  and foregrip fall out along the barrel, and the support hand is placed on
  the actual handguard (sliding inboard if the model is too long to reach).
  Verified numerically: the hand lands within 2mm of its target.
- `_measure_weapon` measures the model's AABB along its barrel and re-seats
  it so the hand sits at a realistic pistol-grip point. Weapon models put
  their origin anywhere — the M4's was 10cm from the muzzle end, so parking
  its stock in the shoulder drove the hand INTO the shoulder and folded the
  arm up behind the head.
- Recoil now shows for the OWNER in third person: server_play_fire_effects
  only ever fired for remote shooters, so the local player saw nothing. The
  ammo counter dropping now kicks the model. The kick rides in the gun's aim
  direction so the IK carries BOTH hands up with it, instead of rotating the
  arms and shoving the support hand off the gun.
- Reload no longer flips the gun. The library's pistol-reload rotates the
  wrist the gun is parented to, which turned the rifle upside-down (mag to
  the sky) while the hand reached down for it. The hold now keeps the right
  arm through a reload and the support hand does the magazine work at the
  real mag well, under the receiver.

anim_capture gains recoil shots and lets ADS settle before framing;
debug/dump_bones.gd prints a skin's bone hierarchy.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-21 10:15:54 -04:00
Nicholas ButzkeandClaude Fable 5 df0bf18e0f feat: real per-weapon reload/recoil choreography + knife slash, fully matte hair
Weapons no longer reload by spinning the gun 360 (that was the upside-down
flip; nothing detached). New ViewmodelAnim module drives per-style manual-of-
arms sequences on the existing model_root + named arm pivots, with a temporary
magazine/shell/rocket/cell prop that visibly leaves and returns to the gun:

  - mag     (M4/AK/MP7/DMR): roll gun to present the well, hand rips the mag
            down and away, fresh mag up, seat, charge. Gun stays UPRIGHT.
  - boltmag (AWP): mag swap + a distinct bolt-cycle rock.
  - break   (double barrel): hinge open, flick both shells out, drop fresh
            shells in, snap shut.
  - tube    (rocket launcher): tip the tube in, shove a rocket home, shoulder.
  - cell    (plasma/nail gun): glowing energy cell swapped on the side.

Per-shot viewmodel recoil kick (ViewmodelAnim.apply_kick) scaled by each
weapon's recoil_amplitude, so an AWP slams and an MP7 chatters — replaces the
old flat model behaviour. Weapons expose reload_style; base classes call
play_reload / stop (on unequip) / apply_kick.

Knife: the old barely-visible flick is now a real diagonal slash that
alternates backhand/forehand, whips the blade through screen centre, and
fires the third-person melee arm-swing action (synced to other players).

Hair/materials: specular fully to 0 (even a 2% stepped glint swept as shine
across Taila's hair when the camera moved); rim trimmed to a whisper. Fully
matte cloth/hair now.

Verify: debug/fp_weapon_capture.gd screenshots each weapon's reload phases,
the knife slash, and the fire kick.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-21 00:26:29 -04:00
Nicholas ButzkeandClaude Fable 5 d46530bd8c fix: color-accurate grade — ginger hair stays ginger, characters stay matte
The saturation grade (1.3) was shifting authored hues — Taila's auburn
ginger hair rendered fire-truck red and the whole game read filtered.
Saturation 1.3 -> 1.08, contrast 1.05 -> 1.03: textures now carry their
own vibrancy. Verified by sampling the rendered hair against the source
texture: lit hue 18-20 deg vs authored ~20 deg.

Character rim/specular cut again (rim 0.15/0.4 -> 0.08/0.32, spec
0.06 -> 0.025 @ 64): hair and cloth read fully matte, no wet sheen.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-20 22:55:58 -04:00
Nicholas ButzkeandClaude Fable 5 1ad3563e5a feat: animations follow the mechanics — upper/lower split + real grapple pose
Two fidelity gaps closed:

- Reload/throw/shoot/hit used to REPLACE the whole-body clip, so reloading
  mid-slide snapped the character upright on standing legs. The model now
  runs a runtime AnimationTree (clips -> Transition -> TimeScale -> OneShot)
  whose one-shot layer is FILTERED to upper-body bones: the arms play the
  action while the legs keep sliding/running/falling. Land stays full-body.
  The rifle hold releases exactly while the one-shot node reports active.
- Grapple no longer plays the library's horizontal-swim clip. The zip pose
  is procedural: Fall as the airborne base, the spine pivots to fly along
  the line to the actual anchor point (capped so overhead shots don't fold
  the body), the head sights the anchor, legs trail behind, the FREE left
  hand reaches up the rope — and the right hand keeps holding the rifle.
  The anchor point feeds in from the controller for local and remote
  players (synced_grapple_point).
- anim_capture: deterministic open-ground teleport + two new regression
  shots (reload-while-sliding, grapple zip toward a real anchor).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-20 22:51:04 -04:00
Nicholas ButzkeandClaude Fable 5 87b8ae70df feat: Fortnite-style OTS camera + Alt free-look orbit in third person
- Third-person camera chain is now HeadPivot > OrbitPivot > ShoulderOffset
  (0.55m right, 0.12m up) > SpringArm(2.6m, 14 deg) — the character sits
  slightly left of screen centre over-the-shoulder instead of dead centre
  behind. Aim is unchanged (still the first-person camera).
- Hold Alt in third person to orbit the camera freely around the character
  with the mouse (yaw wraps, pitch clamped -69..+29 deg); the character
  keeps facing and aiming where it was. Release Alt and the camera springs
  back behind the shoulder.
- debug/orbit_capture.gd: screenshots the OTS framing, a driven orbit, and
  the spring-back for visual regression.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 23:59:44 -04:00
Nicholas ButzkeandClaude Fable 5 b0f26e6dde feat: matte anime materials — kill the glass/mirror look, keep the vibrancy
Everything read as wet plastic or glowing porcelain for two stacking
reasons, both fixed centrally:

- toonify() shipped every character/prop/weapon with a hot rim (0.45 over
  65% of the silhouette) and broad stepped specular (0.25 @ shininess 24) —
  patent-leather highlights on any dark surface. Now a thin rim (0.15/0.4)
  and a small tight glint (0.06 @ 56); shader defaults match.
- The LINEAR tonemapper clipped lit white surfaces (sun + ambient push
  facades past 1.0) and the glow pass bloomed the clip, so buildings glowed
  like porcelain. Switched to FILMIC with white=2.4: near-linear through
  the cel bands (they stay crisp), soft shoulder above 1.0. Glow threshold
  1.1 -> 1.45 so only genuine emissives (neon, tracers, plasma) bloom.
- Saturation 1.22 -> 1.3 compensates filmic mids — vibrancy unchanged.
- Neon Alley: ambient 1.7 -> 1.35 / sun 2.2 -> 2.5 — the shoulder no longer
  hides the ambient lavender cast, so let the sun shape the facades.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 23:53:05 -04:00
Nicholas ButzkeandClaude Fable 5 d0c746d084 feat: Taila — second anime character through a hardened import pipeline
New selectable skin: Taila (original anime character by Partaevil,
CC-BY, license kept alongside the model), cel-shaded with ink outlines,
running the full 18-clip animation set + the new rifle-hold pose layer.

Pipeline hardening learned the hard way (each of these produced a broken
character before the fix):
- tools/strip_rig.py: strips a foreign rig (VRoid/Mixamo names bake FLAT
  clips in the name-based retarget), keeps only meshes skinned to that rig
  (scene props were joining into the player model), recentres feet-on-origin,
  and reroutes image textures into Principled base color for re-export.
- tools/unlit_to_pbr.py: KHR_materials_unlit anime models carry their albedo
  in emissiveTexture over a black base — Blender 5.1 drops the texture on
  import, rendering the character pitch black. Rewrites the GLB JSON to
  standard textured PBR before Blender ever sees it.

Also dropped the Elle import attempt: her rest pose is seated (posed scene),
which the T-pose autorig cannot use.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 23:07:17 -04:00
Nicholas ButzkeandClaude Fable 5 6c227b5538 feat: real two-hand rifle hold — armed animations finally read as a shooter
The clips were playing all along, but every locomotion state used unarmed
jog arms while the gun floated at the hip, so nothing looked animated.

- ShooterPoseModifier now REPLACES the arm chain with a direction-based FK
  rifle hold (low-ready at hip, shouldered on ADS) instead of nudging the
  unarmed clip additively; the wrist is solved so the muzzle points exactly
  along the aim line, gun kept upright (no-roll constraint).
- Per-arm release logic: reload/throw/hit one-shots, dance, death, the
  slide ground-brace, wall-run reach and grapple free the authored clips.
- Third-person weapon: fixed re-hide on weapon swap while in third person,
  scaled to character proportions, gripped along the hand.
- Armed idle uses Idle (arms owned by the hold) instead of arms-crossed
  PistolIdle base.
- FP viewmodel arms: character-styled sleeve + teal cuff + skin hand
  instead of the blue slabs.
- debug/anim_capture.gd: front+side screenshots of every movement state,
  one-shot action and ADS for visual regression of the model pipeline.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 17:55:05 -04:00
Nicholas ButzkeandClaude Fable 5 39af109893 feat: first-person reload animation on the viewmodel
The weapon dips down-and-inward with a roll when a reload starts, holds
through the reload, and rises as it completes — driven by the weapon's
actual reload_timer so the motion always matches the reload duration.
Complements the reload ring UI and the third-person reload clip.

Smoke 0 failures, movement 11/11.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 12:04:49 -04:00
Nicholas ButzkeandClaude Fable 5 7920aecec5 feat: player animation quality pass — aim follow, reload/throw, recoil
Third-person player characters now animate every action with readable
intent (all networked, cel style preserved):

- Look-around: upper body follows the owner's camera pitch — distributed
  over spine/neck/head so aiming up/down reads on the whole silhouette
  (driven by the already-synced HeadPivot rotation on remotes)
- Reload: the baked PistolReload clip plays as a one-shot whenever the
  equipped weapon starts reloading (local edge-detect poll -> new synced
  action counter replays it on every peer)
- Grenade throw: new Throw clip baked from the library's overhead swing
  (Sword_Attack retarget); triggers on throw, synced the same way
- Shot recoil: visible kick on the shooter's model (shoulders snap back,
  forearms rise, fast decay) driven by the existing fire-effects RPC —
  remote players' shots now look like shots
- Slide: trailing arm braces against the ground for balance on top of the
  feet-first pose
- Armed idle confirmed in capture: weapon held at ready, not arms-down

Plumbing: synced_action/synced_action_seq added to all three player
spawners' replication configs; ACTIONS table on the model maps names to
clips + lock times.

Verified: 18 clips resolve (incl Throw), smoke 0 failures, movement
11/11, acoustics PASSED; third-person run + armed idle captures clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 12:03:30 -04:00
Nicholas ButzkeandClaude Fable 5 5224a455a6 feat: screen-space ink edge pass — drawn outlines on everything
New assets/shaders/ink_edge.gdshader: fullscreen depth+normal discontinuity
detection draws thin ink lines over the whole scene — the renderer-level
line pass anime-styled games use. Works on every model regardless of mesh
topology (kit buildings, cars, heroes, characters), with distance fade so
the skyline stays clean. Attached via LevelEnvironment so every map gets
it for whatever camera renders (first-person, third-person, spectator).

Street capture verified: crisp uniform outlines citywide, no artifacts.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 11:54:55 -04:00
Nicholas ButzkeandClaude Fable 5 911066875b feat: hero batch 3 — pedestrian footbridge + viaduct station platforms
- footbridge.glb: 18m truss footbridge over the central avenue one block
  south of the rail line — X-pattern teal trusses, mesh railings, real
  stair flights at both ends. Deck and stairs are WALKABLE (box deck
  collider + invisible ramp colliders over the stair visuals): a third
  elevated crossing route between the viaduct and the plaza, and a new
  grapple anchor over the avenue
- platform.glb: station platform strips on the viaduct deck flanking the
  crossing — canopy on posts, benches, line signs; dresses the deck route
  players actually run
- Fixed a parse error from an undefined color constant caught by the
  capture loop (blank render -> error -> fix -> verified)

Movement 11/11, smoke 0 failures; full-city aerial verified.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 03:10:05 -04:00
Nicholas ButzkeandClaude Fable 5 18f50adb78 feat: hero asset batch 2 — yatai, koban, phone booths, bus stops, bikes
Second round of original Blender-kitbashed street furniture
(tools/build_hero_assets.py):

- yatai.glb: ramen cart — noren curtain strips on a hanger bar, counter,
  wheels, three stools, paper lantern, red roof with ink trim; one at
  every market mouth (wood collider)
- koban.glb: police box with teal roof, door, framed window, sign board
  and red lamp — watches the central crossing from the SE plaza corner
- phonebooth.glb: green-framed glass booth on ~25% of retail corners
- busstop.glb: bench + glass canopy + yellow sign disc, alternating
  blocks along the central avenue, both directions
- bicycle.glb: torus-wheeled bikes with frame/seat/basket, parked in
  seeded rows of 2-4 beside building fronts

All toonified with ink outlines; colliders acoustics-tagged (wood/
concrete/glass/metal). Movement 11/11, smoke 0, acoustics ALL PASSED.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 03:04:54 -04:00
Nicholas ButzkeandClaude Fable 5 7f9a579b63 feat: original kitbashed hero assets built in Blender
First bespoke art authored for the project (tools/build_hero_assets.py,
headless Blender 5.1 -> assets/props/hero/):

- konbini.glb: 8m convenience-store front — fascia + teal sign band with
  ink trim, framed windows with sills and window ads, recessed double
  glass door, angled awning, drink machine, hedge planter. Placed on
  ~1/3 of wide buildings (glass-tagged collider)
- station.glb: viaduct station entrance — canopy on pillars, real stair
  steps with handrails and balusters, sign totem; placed at the central
  avenue crossing of the rail line
- clock.glb: 10m plaza clock tower — tiered base, ringed column, 4 clock
  faces with hands, teal crown; anchors the south-west plaza cell
- torii.glb: proper torii with segmented curved kasagi, shimaki, nuki
  beam and plaque — replaces the box torii in every shrine courtyard
  (invisible pillar colliders keep gameplay identical)

All hero pieces run through the toon pipeline with fine ink outlines.
Every part beveled; flat cel palette matches the city.

Verified: city renders clean with hero pieces placed; movement 11/11,
smoke 0 failures, acoustics ALL PASSED.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 03:00:17 -04:00
Nicholas ButzkeandClaude Fable 5 6b3e363dfd feat: protruding double-sided shop signs over the sidewalks
1-2 per building at second-floor height on metal brackets, double-faced
with generated ad graphics — the perpendicular sign clutter that defines
Akihabara/anime-city streets and makes rows read dense from street level.

Verified via street capture; smoke test 0 failures.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 02:53:03 -04:00
Nicholas ButzkeandClaude Fable 5 c8a85586bd feat: modeled market stalls, mega wall ads, traffic lights, awnings
Final detail pass — no gameplay object remains a plain box:

- Markets: box stalls replaced with modeled kit stalls (4 styles seeded),
  hidden box colliders keep the wood acoustics; hand carts at the market
  mouths; standing lanterns between stalls
- Shrines: hanging lanterns on the stone pillars, wooden fences flanking
  the torii approach
- Towers: giant mid-height wall ads on the street faces (the anime-city
  mega-ad look), on top of crown boards and corner columns
- Storefront awnings (kit detail piece) on ~40% of buildings
- Traffic signals at every central-avenue crossing, diagonal from the
  streetlights
- Fantasy Town Kit (CC0) installed at assets/props/town

Verified via street capture (wall-scale glyph ads, pharmacy signs,
banner ads, stalls, carts); movement 11/11, smoke 0, acoustics PASSED.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 02:50:28 -04:00
Nicholas ButzkeandClaude Fable 5 648872a16d feat: ad-graphic layer — generated posters, billboards and sign columns
The city's flat color sign panels are now real graphic designs, closing
the street-level "graphic density" that anime cities live on:

- tools/generate_ads.py: procedural ad library (24 textures) — bold
  two-tone layouts (diagonal splits, bands, circles, stripes) with
  fake-glyph text blocks that read as kanji/kana from gameplay distance,
  inked borders, seeded palette system
- Every building gets 1-3 street-level posters plastered on its facade
- Vertical sign columns render a stacked-glyph column texture
- Rooftop billboards, tower crown boards (both faces) and the central
  4-face screen tower all show distinct ad graphics
- Posters under the viaduct arches on every pier
- All panels lit (albedo + emission of the same texture) so they glow
  as signage without blooming out

Verified via street capture (glyph posters, pharmacy-cross signs, ad
boards visible down the avenue); movement 11/11, smoke 0 failures,
acoustics ALL PASSED.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 02:44:55 -04:00
Nicholas ButzkeandClaude Fable 5 56db9eea59 feat: parked cars across the city (Kenney Car Kit, CC0)
Streets are now populated: seeded parking lanes hug the curbs of every
N-S street with sedans, taxis, vans, SUVs, hatchbacks, delivery trucks
and the occasional police car (~100+ across the city).

- Wheels attach at the models' wheel-socket empties at spawn
- Metal-tagged box colliders: cars are cover that sounds like cover
- Toonified + darkened base tint (kit albedo clips under the map light)
  with per-car hue jitter for varied paint jobs
- Slight rotation jitter so parking reads natural, not stamped

Verified via street capture (colored cars parked down the avenue, no
clipping/glow); movement 11/11, smoke 0 failures, acoustics ALL PASSED.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 02:39:35 -04:00
Nicholas ButzkeandClaude Fable 5 04d91dea31 feat: street-level detail pass — real roads, streetlights, trees, cables
Closes more of the gap to hand-authored city art at eye level:

- Kenney City Kit Roads (CC0) installed: every street in the 10x10 grid is
  now real road meshes — lane-marked straights (single stretched mesh per
  segment) and line-marked crossroad tiles at all 81 intersections, tinted
  to sit under the toon lighting without clipping
- Kit streetlights (curved arms over the road) along both central avenues,
  with slim pole colliders kept for grapples
- Kenney Nature Kit trees replace the cone/box stand-ins in shrine
  courtyards and plaza planters (original kit materials kept — the toon
  swap mangled their vertex colors into cyan)
- Overhead cable bundles sag across the east-west streets (the classic
  urban-Japan sky clutter), drawn as ink lines; central avenue kept clear
- Construction sites get cones + barriers from the roads kit
- Per-building tint jitter so repeated kit models read as distinct shops

Verified via street/aerial captures (dark marked asphalt, green trees,
detailed facades); movement 11/11, smoke 0 failures, acoustics ALL PASSED.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-19 02:33:31 -04:00
Nicholas Butzke f62f4f2503 feat: add Kenney road and nature assets and implement map builder script 2026-07-19 02:32:06 -04:00
Nicholas ButzkeandClaude Fable 5 67040ba12d feat: detailed building models across the city (Kenney City Kit, CC0)
Boxes with lines can't reach the requested fidelity — the city now uses
real modeled architecture, toonified into the cel style:

- Kenney City Kit Commercial (CC0, 41 GLBs) installed at
  assets/props/citykit: 13 mid-rise buildings + 5 glass skyscrapers with
  actual modeled window insets, doors, storefronts, awnings, trim and
  rooftop detail, plus parasol/awning props
- Standard + market blocks: street edges fill with seeded kit buildings
  (measured catalog drives placement, x13 scale -> 11-32m facades) over
  box colliders; Akiba signage layer (corner sign columns, glowing
  segments, roof billboards) attaches aligned to each model's bounds
- Tower blocks: kit skyscrapers (up to ~70m curtain-wall glass) on the
  walkable podium, seeded rotation, crown billboards kept
- Market blocks add cafe parasols among the stalls
- SSAO enabled + exposure recalibrated for the kit's bright albedo
  (sun 2.2 / ambient 1.7); lighter asphalt/paving to match; lighter haze

All kit meshes run through apply_toon_recursive: toon banding over the
kit textures, matching characters/weapons/VFX.

Verified: street + aerial captures (detailed facades at eye level, dense
believable skyline); movement 11/11, smoke 0, acoustics ALL PASSED.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-18 14:59:52 -04:00
Nicholas ButzkeandClaude Fable 5 b0af2ea9ee feat: Akiba Crossing city — 100 blocks with unique landmarks + detail pass
Scaled the single block to a full 10x10 city grid (48m cells, 16m streets,
640m across) with a hand-authored layout so the fabric stays varied:

Block types:
- S standard retail (70%): paired parametric buildings facing the N-S
  streets, seeded heights/colors per block; ~half get a mid-block alley
  with fire-escape climbs so no two blocks play identically
- T landmark towers: 8-10 floors on a walkable retail podium, mega corner
  sign, double-sided rooftop crown billboard (skyline wayfinding)
- J shrine courtyards: walled court, torii, slide-roof hall, lanterns,
  trees — the green quiet break in the fabric
- M market streets: dense stall rows + lantern strings between low
  buildings (close-quarters cover mazes)
- P plaza cluster at the city centre with planter cover and a giant
  4-face screen tower at the exact middle (the Crossing)
- C construction sites: open slab frame to wallrun, access ramp, container
  stacks, and a 28m crane whose jib is the city's tallest grapple anchor

City systems:
- Brick rail viaduct runs the full east-west width on per-block piers with
  vending arcades under alternating arches + slide ramps at the centre
- Central avenues x=0/z=0 get matched streetlights and zebra crossings at
  every block line; brick wall rings the district; city-haze fog for depth
- Detail pass per building: window bands every floor, water tanks/antenna
  masts seeded on roofs, denser sign stacking

Perf/physics: decoration (signs, windows, leaves, lamp heads) is
render-only MeshInstances; only gameplay surfaces are static bodies, all
acoustics-tagged. Spawns distributed across block types facing centre.

Verified: full-city aerial (all 100 blocks, aligned grid), avenue-level
capture (clean canyon, viaduct vista); movement 11/11, smoke 0 failures,
acoustics ALL PASSED.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-18 14:45:48 -04:00