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]>
The finger roles only reach the runtime through <model>.rig.json, so a
character whose sidecar predates them keeps flat, open hands however good
the pose layer is. Aria, Momo, Miku and the Mannequin re-imported; all six
skins now carry 10 finger chains.
Verified on the Mannequin, which is untextured and so the clearest
diagnostic of the three checked: stock at the shoulder, trigger hand on the
grip, support hand out on the handguard, both fists closed, arms not
crossing. Its arm is a full 0.524 m against Taila's 0.468 and it scales the
weapon independently — 0.231 m of hand separation against her 0.217 — with
the slide-back loop never firing on either.
Known remaining: the support hand's wrist roll leaves the hand hanging a
little under the handguard rather than wrapping it squarely. The grip
POSITION is right on every character; it is the roll about the barrel that
wants another pass.
Co-Authored-By: Claude Opus 5 <[email protected]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
- 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]>
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]>
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]>
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]>
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]>
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]>
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]>
The library's master sheet revealed the primary AK-47 sample (C_27P) was a
full-auto LONG BURST, and two other slots used burst takes (C_34P, P_22P) —
one trigger pull audibly played several rounds. Beyond that, the single-shot
recordings carry outdoor-range slap-back echoes arriving as discrete pops at
~0.23s/0.33s/0.45s, which also read as extra shots.
- All fire sounds rebuilt from sheet-verified SINGLE-shot takes only
(AK-47 gets C_28/C_31 + same-caliber SKS as third variation; MP7 gets
single PPSh takes + Walther PPQ 9mm)
- Every sample now ends before the first echo can play: full level through
the crack (~0.145s), faded out by 0.21s (times scale with pitch rate)
- Verified with silencedetect: every fire WAV is a single transient with
no mid-file re-attacks
Co-Authored-By: Claude Fable 5 <[email protected]>
- Gunshots: strip lead-in silence from every firearm recording (the audible
delay between trigger and bang) and hard-trim to a single 0.7s crack with
a fast fade — the outdoor-range echo read as 2-3 extra shots. Big single
shots (AWP/shotgun/rocket/mortar) keep a 1s tail but still one boom.
- Hit confirm: short fleshy punch thock instead of the bell ("taco bell")
- Dash: clean synthesized air whoosh (bandpassed pink noise swell) instead
of thruster fire crackle; grapple shoot inherits it
- Jump: push-off foot scuff + faint whoosh instead of the balloon boing
- Wind speed-loop: deep lowpass so it reads as rushing air, not fire
- Grenade: silent in flight — the rocket-engine loop made it sound like it
was burning; bounces still audible
Verified: reimport + smoke test 0 failures, 11/11 movement tests.
Co-Authored-By: Claude Fable 5 <[email protected]>
Audio:
- All ballistic weapons now fire real firearm recordings (The Free Firearm
Sound Library, CC0): AK-47 uses actual AK-47 shots, M4=AR-15, MP7=PPSh,
AWP=Mosin Nagant, DMR=Savage 10, shotgun=Mossberg/Nova, nailgun=Ruger .22,
rocket/mortar layered with real booms. Plasma keeps its energy sound.
- Reload is real lever-action foley instead of interface clicks
- Projectile flight loops localized: unit_size 50->6, max_db 6->0,
max_distance 45 — no more mortar/rocket drone in every player's ear;
loops also got proper loop_end (was a zero-length loop)
- Flight loops lowpassed into soft whooshes instead of sci-fi engine drones
- Menu music converted to OGG with native full-track looping (47s seamless,
replaces the broken manual WAV loop points)
Animations (movement-shooter pass):
- Rebaked miku.glb with 4 new clips from the CC0 library: Grapple (swim
reach — reads as a swing), PistolIdle (armed idle with weapon up),
PistolShoot, PistolReload
- Armed idle: standing with a weapon now uses PistolIdle instead of
arms-at-sides
- Slide pose overhaul: legs kick out front (lead/trail thighs, knees
straighten) — feet-first slide instead of "sitting in a crouch"
- Wall run: forward drive pitch + inner arm reaches out to the wall
- Per-transition blend times: dash/hit/jump cut fast (0.05-0.08s),
locomotion cross-fades smoothly (0.2-0.25s); CrouchWalk speed-scales
Fixes:
- Third-person camera no longer renders the first-person viewmodel layer
(the gun/arms floated in front of the third-person view)
Verified: 11/11 movement tests, smoke test 0 failures (17 clips resolved),
third-person run screenshot shows grounded animated model, no floating gun.
Co-Authored-By: Claude Fable 5 <[email protected]>
Audio (all CC0 — Kenney packs + OpenGameArt, see assets/sounds/SOURCES.md):
- Replace every procedural synth WAV with forged sounds (ffmpeg pitch/layer
mixes): distinct fire sounds per weapon with 2-3 variations, explosions,
footsteps x5, land/jump/dash/vault, knife swing, hit/kill confirms,
bullet impacts, reload, flight loops, UI hover/click/confirm/error,
match jingles, menu music
- AudioManager.stream_for(): AudioStreamRandomizer per sound id — every
weapon now gets variation + pitch randomization on each shot
- Explosions and bullet impacts play positional audio; landing has its own
sound instead of a pitched footstep; ambient wind bed on every map
Visuals:
- ExplosionVFX: shared cel-shaded burst (white-hot stepped core, ink
shockwave ring, star spikes, flat smoke puffs) replaces the orange
sphere in both local and remote-replay paths
- Comic star muzzle flashes on all weapons; unified cel tracer bolts with
ink outlines across hitscan/shotgun/remote paths
- Impact decals: hard-stepped ink-splat gradients instead of soft airbrush
- Toon shading on first-person view models + arms, third-person weapons,
and the procedural humanoid fallback (no hull outlines on FBX weapons —
their hard normals tear the inverted hull)
- Fix: giant soft "blob" highlight on floors — toon rim/specular disabled
on level-geometry materials (pre-existing artifact since the cel commit)
- Fix: own third-person weapon rendered into the first-person camera
(shadows-only now covers first_person_mode)
UI:
- UITheme: comic theme on the root window — Bangers display font (OFL),
paper panels with thick ink borders + hard drop shadows, papaya accent,
themed buttons/inputs/popups; hover/click sounds on all menu buttons
- Main menu: tilted comic wordmark, sunset toon diorama, looping menu music
- Match HUD: themed scoreboard/killfeed/kill counter
Viewports:
- 4x MSAA project-wide + viewmodel/diorama viewports (crisp ink lines)
- Viewmodel camera near plane 0.01; third-person camera FOV syncs settings
- debug/visual_capture.gd: dev tool to screenshot menu + level for review
Verified: 11/11 movement tests, spawn smoke test 0 failures, before/after
screenshot comparison of menu and level.
Co-Authored-By: Claude Fable 5 <[email protected]>
- SkinnedPlayerModel: generic play_oneshot() (generalizes the Land lock),
Hit and Dance in the clip table (Dance loops), restartable clips
- Hit flinch: victims visibly react on every peer's screen via the existing
damage broadcast — no new RPC
- Dance emote on B: toggles while grounded and idle, breaks on any movement;
synced to remotes via synced_is_dancing (all three spawner configs)
- Anime radial speed-lines overlay (canvas shader): fades in past ~1.2x walk
speed, spikes on dash, widescreen-corrected clear center
- New input action 'emote' (B) in project.godot
11/11 FSM tests, spawn smoke 0 failures.
Co-Authored-By: Claude Fable 5 <[email protected]>
- toon.gdshader: banded diffuse with cool-tinted shadows (3 tones), stepped
specular, rim light, and an optional world-triplanar albedo path so level
geometry keeps the 0.5 m grid with no UVs
- toon_outline.gdshader: inverted-hull ink outline, applied via
material_overlay so it works on skinned (deforming) meshes
- LevelMaterials: tinted() now returns toon grid materials; toonify()/
apply_toon_recursive() convert imported character/prop materials in place
- SkinnedPlayerModel: characters get toon shading + outline on load
- LevelEnvironment: shared stylized environment for all maps — saturated
anime sky, linear tonemap (filmic/ACES crush cel bands), bloom for
emissives, saturation+contrast grade, light depth haze; test level and
dust2 now use it and the procedural arena (which had NO environment)
gets it plus runtime toonification of its baked geometry
Co-Authored-By: Claude Fable 5 <[email protected]>
- Generated CC0-style prototype grid textures (0.5 m cells) and a shared
LevelMaterials factory: world-space triplanar mapping over every code-built
level (test level, dust2, procedural arena) with the existing color coding
kept as tints. Readable surfaces at speed instead of flat color boxes;
materials are cached per tint so identical surfaces batch.
- .gdignore + .gitignore the raw downloaded asset packs in addons/ whose
overlong paths broke Godot import scans and spammed git warnings.
- Replaced the rotted FSM test file (invalid call(t) on Callables, stateless
stub nodes, hung forever without quitting) with a real suite: 10 tests
covering state registration, jump bookkeeping, wall-run momentum
preservation and upward-carry cap, per-player dash cooldown (regression for
the old shared static), dash speed stacking, chain soft cap, landing
events, and slide entry momentum. New headless entrypoint:
godot --headless --path . -s res://movement/tests/run_fsm_tests.gd
Co-Authored-By: Claude Fable 5 <[email protected]>
The shipped skin GLB only had Idle/Walk/Run/Jump, so Slide, WallRun, Crouch,
Dash, Fall and Land all fell back to the wrong clips. Rebuilt via the Blender
pipeline from the library-skeleton rigged model (the old miku_proper_anim was
on a different skeleton, so retargeting silently baked flat clips — and the
shooter pose layer's DEF-* bone names never matched it either).
miku.glb now ships Idle, Walk, Run, Sprint, Jump, Fall, Land, CrouchIdle,
CrouchWalk, Dash (roll), Death, Hit, Dance from the Quaternius Universal
Animation Library (CC0). Verified per-bone keyframe motion in the export and
rendered pose stills for deformation QA (no crossed arms / stretched feet).
Also removed five stale experimental skin GLBs + extracted textures and the
one-off editor import scripts; skins.json now lists only the working skin.
Co-Authored-By: Claude Fable 5 <[email protected]>
Godot import artifacts that pair with already-committed assets:
- miku_test_Image_0..3.png: textures extracted from miku_test.glb on import
- .uid files for spawn_smoke_test.gd and audio_manager.gd
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The model animated but deformed wrong: arms crossed the torso and foot heels
stretched to the model's center-bottom. Two root causes, both fixed:
1. Pose mismatch. Sketchfab models are modelled in an A-pose (arms angled
down) but the Quaternius library rests in a T-pose. Binding the A-pose mesh
to T-pose arm bones misweighted the arms, and the library clips (which
rotate arms ~70° down from the T rest) then shoved the already-drooped arms
across the body.
Fix: autorig.fit_arms_to_mesh() reorients each arm bone chain along the
model's real arm direction; merge_animations retargets every clip from the
library's T-pose rest onto the fitted rest via world-space constraint bake
(Copy Rotation/Location + nla.bake), so world motion is reproduced
regardless of the rest difference.
2. Feet-to-center stretch. The library's deform-flagged `root` bone runs up
the body centre-line; nearest-bone weighting assigned inner-foot/heel verts
to it, stretching them to center when animated.
Fix: exclude root/control/mechanism bones from weighting (_NON_DEFORM_HINTS).
Also upgraded nearest-bone weighting from rigid (1 bone/vert) to a smooth
inverse-distance blend across the nearest 4 bones, so joints deform cleanly.
Bone-heat still fails on Miku's joined hair/clothing mesh, so this is the
primary path.
Verified by rendering Idle/Walk/Run poses (Blender): natural stance, arms at
sides swinging correctly, feet planted, proper running stride. Smoke test
still 30/30.
Co-Authored-By: Claude Fable 5 <[email protected]>
The model animated its skeleton but rendered a permanent T-pose: the mesh
wasn't skin-bound. Root cause was in autorig's binding, introduced while
chasing an earlier "skins:0" export:
- The earlier "skins:0" was actually caused by bone-heat weighting failing
(0 weighted verts → empty skin gets dropped), NOT by the Armature modifier.
- The "fix" then stripped the Armature modifier and used parent_type=ARMATURE.
That makes the glTF exporter emit a skin OBJECT but with no node.skin
reference and no per-vertex JOINTS/WEIGHTS — an orphan skin. The mesh then
renders its bind pose (T-pose) forever while the skeleton animates unseen.
Fix: keep the standard ARMATURE_AUTO result (Armature modifier + vertex
groups). With the nearest-bone fallback ensuring real weights, the exporter
now writes a COMPLETE skin (verified: meshnode.skin=0, JOINTS/WEIGHTS present).
Also:
- skinned_player_model: _ensure_meshes_bound() re-binds any skinned mesh whose
skeleton NodePath doesn't resolve at load — graceful degradation instead of
a silent T-pose for imperfect GLBs.
- smoke test now asserts the spawned player's mesh is bound to its skeleton,
so this class of bug fails the test instead of shipping. 30/30 pass.
- regenerated miku_test.glb with the corrected pipeline.
Co-Authored-By: Claude Fable 5 <[email protected]>
Verified the full chain on a real Sketchfab model (Miku UID -> rigged ->
13 clips -> loads and animates in-engine, 24/24 smoke checks pass).
Fixes found while running it for real:
- sketchfab_import: read .sketchfab_token as UTF-16/BOM too (PowerShell's
`echo >` writes UTF-16LE, which crashed the UTF-8 reader)
- autorig: rig to the animation library's OWN skeleton when _library.glb
is present (zero retargeting); robust binding — fall back to rigid
nearest-bone weights when Blender bone-heat fails on complex meshes
(Miku's joined hair/clothing gave 0 weighted verts); export as
parent_type=ARMATURE with no modifier (Blender 5.x only emits a glTF
skin in that exact form — a lingering Armature modifier gave skins:0);
export_apply=False (applying modifiers baked away the skin); flatten
the Sketchfab empty hierarchy before binding
- merge_animations: support a single multi-clip library GLB via
LIBRARY_CLIP_MAP; version-safe fcurve access for Blender 4.4+/5.x
slotted actions (Action.fcurves was removed)
Assets/tooling:
- bundle assets/characters/animations/_library.glb — CC0 Quaternius
Universal Animation Library (13 game clips), + .gdignore so Godot
skips the pipeline-input folder
- requirements.txt (stdlib-only; documents Blender-python separation)
- pipeline docs updated for the bundled library + swap instructions
Co-Authored-By: Claude Fable 5 <[email protected]>
Character pipeline (tools/):
- sketchfab_import.py: search/download via Sketchfab Download API with
license/attribution tracking (SKETCHFAB_API_TOKEN)
- autorig.py: headless Blender auto-rig — fits a Mixamo-named skeleton
to unrigged humanoids, binds automatic weights
- merge_animations.py: merges the shared Mixamo-skeleton animation
library onto any rigged character, strips root motion, canonical names
- pipeline.py: one command chains download -> rig -> animate -> register
In-game skin system:
- SkinManager autoload reads skins.json (auto-written by the pipeline)
- SkinnedPlayerModel rewritten: canonical clips with fallback chains,
blend times, speed-scaled locomotion, weapon bone attachment
- First-person: full animated body for the owner, head hidden via
SkeletonModifier3D; third-person: full model for other players
- Skin selector in main menu; skin id synced in multiplayer
- Fixed GLBLoader crash (GLTFDocument.get_animation_count doesn't exist)
Multiplayer sync overhaul:
- Movement is now client-authoritative: the owning peer simulates
locally (no input round-trip), server keeps health/kills/death
- Remote players interpolate synced_position/velocity with
extrapolation and snap-on-teleport
- Knockback/impulses routed to the simulating peer
Audio:
- AudioManager autoload: SFX/Weapons/Footsteps/UI/Music buses, pooled
3D players, variation + pitch randomization, auto-registration from
assets/sounds
Docs: 3D_ASSET_PIPELINE.md rewritten end-to-end, new ASSET_SOURCES.md
(non-procedural animation/map/sound sources) and SOUND_DESIGN.md.
Verified with debug/spawn_smoke_test.gd (headless: 24/24 checks pass).
Co-Authored-By: Claude Fable 5 <[email protected]>
The Blender GLTF exporter was stripping animation tracks (only 1 of 51
channels survived export). Now SkinnedPlayerModel detects this at
runtime and creates a proper Idle animation programmatically using Godot's
Animation API with 5 bone tracks (Spine, Arms, Neck, Hips).
Also switched model_path back to miku_rigged_final.glb since the
re-export wasn't adding usable animations.
The twintails were moving as arms because the auto-rig script treated the
combined mesh as one piece. Now using the original miku_rigged_final.glb
skeleton (which has correct skin weights from the model creator) and
adding 6 new animations (Idle, Walk, Run, Jump, Crouch, Death) on top
of the existing skeleton.
- tools/add_animations.py: adds animations to an existing rigged GLB
- tools/rig_and_animate.py: updated to separate mesh into loose parts
before rigging (for future use with other models)
- miku_rigged_animated.glb: now 7 animations with correct skin weights
- Add miku_rigged_animated.glb (3.27MB) with 18-bone skeleton and 6 animations
(Idle, Walk, Run, Jump, Crouch, Death) via Blender auto-rig script
- Add tools/rig_and_animate.py: reusable Blender script for rigging any humanoid
mesh with Mixamo-compatible bone naming
- Update SkinnedPlayerModel: scale_factor, first-person mode, animation state
matching (Idle/Walk/Run/Jump/Crouch/Death)
- Update level_runtime.gd: use skinned model for local player, procedural
humanoid for remote players
- Update skin_manager.gd and test_level_builder.gd to use animated model
- Fix Godot 4.2.1 'is not Type' syntax in 5 weapon files
- Add editor/import_miku_to_tscn.gd for editor-based GLB import
- Fixed Blender export: remove FBX armature modifier before scaling,
then transform_apply(scale=True) before creating new armature
- Model now exports at correct 1.8 unit height (verified in Blender)
- SkinnedPlayerModel now polls MovementStateMachine in _process
to drive animations based on movement state
- Player 1 gets Miku at origin, others get procedural humanoid at y=-0.9
Pipeline:
1. Downloaded TDA-style Miku model from Sketchfab (19MB FBX, 38K verts)
2. Imported to Blender, scaled to Godot humanoid proportions (1.8u tall)
3. Created clean 17-bone humanoid armature (Hips/Spine/Chest/Head/Arms/Legs)
4. Auto-weight-painted mesh to armature
5. Added Idle (breath bounce) and Run (cycle) animations
6. Exported as GLB with embedded animations (3MB)
New:
- SkinnedPlayerModel class: loads GLB with own armature+animations
- Player 1 spawns with Miku model, others get procedural humanoid
- Completely separate from procedural model system
Files:
- assets/characters/skins/miku_rigged_final.glb (3MB, 48K verts, 17 bones, 2 anims)
- characters/skinned_player_model.gd
- Removed old procedural GLB attempts
Skin System:
- PlayerSkin resource class (model_path, color_tint, etc.)
- SkinManager with default skins: Default, Red Team, Forest, Miku
- HumanoidModel.apply_skin() supports both GLB model replacement
and procedural color tinting
- Procedural meshes auto-hide when GLB model is loaded
Miku Skin:
- 43-part procedural model generated in Blender (headless)
- Twin tails, headphones, thigh-high socks, idol outfit
- Teal/cyan color scheme with red accents
- Exported to assets/characters/skins/miku.glb
- Applied to player 1 in test level builder
Pipeline:
- Blender 5.0.1 + MCP addon installed
- Bridge script for socket-based Blender control
- docs/3D_ASSET_PIPELINE.md documents full workflow