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]>
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]>
- 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]>