Commit Graph
6 Commits
Author SHA1 Message Date
Nicholas Butzke 922983429e big 2026-08-02 02:20:02 -04:00
Nicholas ButzkeandClaude Opus 5 53f175ed6d feat(characters): say what every surface IS, and light it accordingly
A character has arrived as separate body, garment and hair meshes since the
pipeline stopped joining them — but nothing recorded which was which, so every
system downstream re-guessed from the material. That guess ("untextured and
nearly black means ink") had already rendered the mannequin's flat yellow body
as a black silhouette once.

The question is answerable once, at build time, where the mesh, the weights and
the skeleton are all in hand. tools/surface_map.py answers it three ways, in
order of how much it trusts them: the material name, which on VRoid exports is
formal and on hand-authored models is still explicit; the weights, which are
decisive when the name says nothing — a surface pulled by the skirt chain is a
skirt whatever it is called; and the material flags, which catch the model's own
line-work. The answer goes in the rig sidecar next to the roles and the chains,
and SkinSurfaces reads it.

All eighteen of Taila's surfaces, and every surface of the other five skins,
now resolve from the table with nothing falling through to the heuristic
(debug/surface_class_check.gd). The heuristic stays as the fallback, which is
the one job it was ever right for.

What that buys immediately is per-class art direction, which was impossible
while every surface had to take numbers calibrated on skin. Hair takes a much
thinner line — at the body's 5 mm each strand's hull swallows its neighbour and
the head reads as a solid dark cap. Cloth takes a heavier line and a crisper
terminator, because a garment's silhouette is most of what separates a character
from the background at range. Accessories take the heaviest. `body` is unchanged
on purpose, so the look this was all calibrated against does not move.

That required moving the outline from the instance to the surface: Miku's body,
face and hair are three surfaces of ONE mesh, so an instance-wide overlay could
only ever give all three the same weight.

Two things found on the way, fixed here because they are one line each: the
surface classifier skips meshes with no vertex groups, which drops the stray
42-vertex Icosphere that rides inside every shipped skin — two older tools
already skipped it by spelling its name — and load_model now clears _rig_info,
which a model with no skeleton used to inherit from the last character loaded.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-27 14:35:06 -04:00
Nicholas ButzkeandClaude Opus 5 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]>
2026-07-26 19:02:11 -04:00
Nicholas ButzkeandClaude Opus 5 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]>
2026-07-26 16:29:04 -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 Butzke 374d9f9822 feat: implement automated 3D character pipeline with retargeting and rig management tools 2026-07-24 20:52:35 -04:00