This commit is contained in:
Nicholas Butzke
2026-08-02 02:20:02 -04:00
parent 61669627db
commit 922983429e
226 changed files with 34032 additions and 18521 deletions
+254
View File
@@ -0,0 +1,254 @@
# Graphical Fidelity Direction
**Primary reference: [Sakura Crossing](https://github.com/Kenton-GMI/sakura-crossing)**
(MIT) — a procedural Japanese suburb in three.js, rendered to look like a
hand-painted 2D anime background. This is the target image for the project, with
one deliberate departure: **the line work is slightly harder and thicker.**
Secondary reference: [The Long Silence](https://github.com/achimala/TheLongSilence).
The transferable lesson there is not its realistic-space rendering. It is that
one authored visual law is reused everywhere, expensive detail is paid for only
where it can be seen, and every art decision is checked in repeatable captures.
Character-specific decisions are governed by
[CHARACTER_ART_DIRECTION.md](CHARACTER_ART_DIRECTION.md): 90% urban anime
character read, 10% selectively deployed utility technology.
## The cel law
Four layered techniques, ported from the reference. `scenes/maps/sakura_crossing`
is the map built to demonstrate all four; `scenes/maps/sakura_palette.gd` is the
palette port.
**1. Quantised toon lighting with a hue-shifted shadow**
`assets/shaders/sakura_cel.gdshader`. Direct light is snapped to one of 25 flat
values from a hand-authored ramp, sampled at `dot(N,L) * 0.5 + 0.5`. Then:
```
irradiance = band * mix(shadow_tint, vec3(1.0), band)
```
The band drives its own tint, so full light is untinted and the darkest band is
almost pure cool violet. **That hue shift in shadow is most of what separates
"anime cel" from "low-poly 3D"** — a wall whose shade is merely 40% of its lit
colour reads as an unlit polygon. High-key ramps (`RAMP_SOFT`, `RAMP_SOFT3`)
exist for pale masses like blossom that must stay light on the shadow side.
Faceted shading is on by default and is not a shortcut: a quantised ramp over
smooth normals puts a *curved* band boundary across a curved surface, which
reads as an airbrushed gradient no matter how few steps it has.
**2. Screen-space ink from the SECOND difference of linear depth**
`assets/shaders/ink_edge.gdshader`. This replaced a conventional first-difference
edge detect, and the distinction is the single most important one in the file. A
first difference measures depth *slope*, which is large wherever a surface is
oblique to the camera — so the road, every roof and every long wall carried ink,
and the threshold had to be raised until it smeared. A second difference measures
*curvature*, which is exactly zero across any planar surface however oblique. It
therefore fires only on real silhouettes and real creases, and the threshold can
be low enough to catch a distant railing.
Convex curvature inks strongly, concave faintly — the outside line of a shape is
heavy, the contact line inside it is light, which is what an animator draws.
Two things to know when tuning it: `ink_hardness` also sets the **noise floor**,
because a grazing ground plane quantises in the depth buffer into a staircase and
a staircase has a large second difference at every step. Too soft and the road
comes out finely hatched. And anything smaller than the line width — petals,
distant wires — will be inked *solid*, so sub-pixel scatter belongs in the
transparent pass where the ink cannot see it.
**3. Inverted-hull outlines** on hero props only — `toon_outline.gdshader`,
applied via `LevelMaterials.outline()`.
**4. A four-light anime rig**`LevelEnvironment._build_light_rig`. A warm
quantised key (the only shadow caster), a **strong** cool bounce from the
opposite quarter at roughly a third of the key, a weak violet up-light for
undersides, and a violet-ground hemisphere carried by ambient. The bounce is the
important one: it puts a distinct, cool, non-black value on every shadow plane,
so a surface turning away from the sun turns *into another colour* rather than
into darkness.
The colour grade is a **split-tone** — cool violet into the darks, warm paper
white into the lights — and it lives at the end of the ink pass rather than in
`Environment.adjustment`, so it can be a split rather than a single saturation
number. Environment's adjustment stage is therefore disabled; enabling both
grades the frame twice.
## Lettering and trim
The reference is emphatic that *"every sign, fascia, lantern and price strip is
drawn at runtime — there is not one image asset in `src/`."* That is not a
technical footnote. A Japanese street is **made of lettering**, and the first
build of Sakura Crossing had correct forms, correct colour and no type on it
anywhere; it read as an architectural massing study.
`scenes/maps/sakura_signage.gd` is the answer. Godot's equivalent of the
reference's Canvas2D is **`TextMesh`** — real geometry from a font — which suits
this renderer better than a drawn texture would, because the whole map is
untextured flat colour and, more importantly, **geometry gets inked**. Every
glyph picks up the screen-space outline exactly as the buildings do, which is
what seats the lettering *in* the picture rather than on top of it.
Settings that matter, both measured:
- **`depth = 0`.** Extruded text is 4 500 verts for four kanji; flat is 2 100.
The extrusion's side walls are most of the mesh and are never seen on a sign
fixed to a wall. Flat glyphs sit a centimetre proud of their board instead.
- **`curve_step = 8`.** The 0.5 default is 43 000 verts for three signs; 8 is
13 700 and looks identical at any distance a sign is read from.
The font is a **system stack** — the same one the reference uses (`Yu Gothic`,
`Meiryo`, `Hiragino Kaku Gothic ProN`, `MS Gothic`). Ships no font file; if none
resolve, signs go blank and nothing crashes.
**Ink or no ink is an art decision, per object.** Raised lettering on a sign
board wants the outline — it is an object standing off a wall. Paint on the
ground does not: road markings were inked at first and came out *hollow*,
because a stroke of 止まれ at a grazing angle is a few pixels wide, the ink fired
on both edges and the two outlines met in the middle and ate the letter. Pass
`inked: false` and the glyphs go to the transparent pass where the ink cannot
see them. Same mechanism as the petals — **anything thinner than the line width
gets inked solid.**
Trim is the other half. A cel image is judged on its *third* tier of structure,
and the first build had almost none: no sills, no downpipes, no meters, no
manholes, no tactile dots. All of it is render-only and most of it is a box —
none of it needs to be clever, it needs to be **there**.
One layout lesson worth keeping: unit width is the character of a street. Eight
shop units across this map made each one 19.5 m wide, which is a department
store, not a shop — one awning and one noren stretched across sixty feet of
frontage. Six-to-ten-metre units give twenty awning colours, twenty names and
twenty doorways per side, and that rhythm is most of what a 商店街 *is*.
Cost: ~5 300 mesh instances, 6.85 ms/frame at 1080p in the heaviest view.
## Image contract
Every gameplay frame should have:
- A readable dark, mid and light band with less than 2% practical clipping.
- One dominant warm/cool lighting relationship, not white ambient everywhere.
- Dark contact at feet, props and building bases.
- Large, medium and small visual structure, in that order.
- Ink weight that follows importance: character silhouette, rigid accessory,
architecture crease, then tertiary texture.
- Quiet traversal space around loud signs, characters and combat VFX.
- Surface detail that disappears cleanly before it becomes sub-pixel noise.
The current automatic guardrail is:
```powershell
python tools/levels.py <capture-directory> --inset 8 --gate
```
`debug/fidelity_probe.gd` is the deterministic look-development scene.
`debug/visual_capture.gd` remains the real-game regression capture.
## Lighting and post
- Keep the sun as the form key and ambient as a low-energy colour bed.
- Use tight SSAO only for contact. Broad grey AO fights flat cel bands.
- Keep cast shadow and form shadow separate; cast shadows may be deeper and
harder than a character or prop's terminator.
- Let only authored emissives cross the glow threshold. White paint must never
bloom.
- Preserve a filmic shoulder and measure clipping instead of judging it from a
single monitor.
- The first-person weapon uses a camera-relative warm key and cool fill. Its
graphic read must not change when the player turns in world space.
## Texturing and materials
`LevelMaterials.SURFACE_LAW` is the shared construction language:
- `wall`: storey-scale seams plus a restrained base grade.
- `ground`: larger paving divisions with no vertical grade.
- `panel`: tight machined divisions for metal and kiosks.
- `trim`: grounding grade without a grid.
These are semantic materials, not colour presets. Do not apply one noise field
to every asset. Foliage, vehicles and character silhouettes stay quiet unless
their own authored texture calls for detail.
The next texture pass should add a small reusable decal sheet for:
- one or two edge chips;
- drain and service markings;
- poster tears and tape corners;
- sparse hazard chevrons;
- directional pavement paint.
Place decals by composition and gameplay route. Random coverage is not detail;
it is visual noise.
## Modeling
Build and review assets in three reads:
1. Silhouette at combat distance.
2. Primary planes and construction breaks at traversal distance.
3. Fasteners, bevels and decals only at inspection distance.
For the city kit:
- Give rooflines and street corners deliberate silhouette interruptions.
- Prefer modeled awnings, frames, vents and cable bundles over high-frequency
material noise.
- Reuse a small vocabulary of construction dimensions so adjacent modules feel
manufactured together.
- Merge static pieces by material only after their panel coordinates are stable;
seams should continue across parts meant to read as one assembly.
- Add LOD or visibility ranges before adding tertiary geometry across all 100
city blocks.
## Character and weapon animation
Anime fidelity comes from pose design and timing more than interpolation count:
- Let the body lead every movement verb. Jump jets, grapple hardware and dash
thrusters deploy only long enough to explain the motion, then return visual
focus to the character.
- Gameplay anticipation must be short but unmistakable.
- Key combat silhouettes need a readable held pose before recovery.
- Accelerate through impact, hold the strongest pose, then use a clean recovery
arc instead of uniformly smooth motion.
- Keep weapon, support hand and head intent aligned; the existing hold-profile
checks should remain mandatory.
- Judge locomotion from the gameplay camera, not only in an animation viewer.
- Use `debug/anim_capture.gd`, `debug/fp_weapon_capture.gd` and the hold/pose
checks whenever animation assets change.
Secondary cloth and hair should lag the torso and settle quickly enough not to
obscure aim or team recognition. Their job is to sell acceleration, not to run
an independent simulation showcase.
## VFX
- Reserve the brightest values for hits, muzzle flashes and abilities.
- Shape effects as graphic wedges, rings and speed lines before adding soft
particles.
- Use colour to communicate gameplay class consistently.
- Fade detail by projected size. A distant effect should become one clean spark,
not many sub-pixel particles.
- Keep screen-space distortion local and brief; the HUD and target silhouette
must remain readable.
## Performance and review
Fidelity features should degrade by resolution or distance before being
disabled outright. Panel seams and sky clouds are analytic and band-limited;
future decals and tertiary geometry should use visibility ranges.
Review sets should include:
- deterministic fidelity probe, key and grazing angles;
- Neon Alley street, spawn and aerial views;
- one bright and one dark character skin;
- each first-person weapon family;
- run, slide, dash, reload and one ability impact;
- a low render-scale capture to expose shimmer and sub-pixel detail.
Compare the set as a group. A change is not an improvement if one hero frame is
better while combat readability or another map regresses.