Rendering and presentation - Shared surface-material module (src/game/materials.gd) with per-surface roughness/metallic/uv profiles; ACES tonemapping, glow, fog and phase-driven sky energy. WorldBuilder rebuilt on those profiles with facade windows that light up, door trim/handles, curb, lane dashes, bollards, bench, bin and planter foliage. - NPC silhouettes: per-role clothing palettes, caps/hoods/backpacks/sunglasses, hands and shoes, arm/leg walk swing. - Six generated low-poly GLB props (bollards, bench, bin, planter, newspaper box, hydrant) built by tools/model_pipeline.py, loaded by WorldBuilder with a procedural fallback; ADR-0012. Selection and movement - Focus ranking by view angle then distance (NpcFocus.rank_indices); Tab and the mouse wheel cycle nearby people initially, right click clears focus with a re-acquire grace, HUD shows the candidate count, Shift walks at half speed. ADR-0011. Windowed smoke extended to cover wheel cycling and clearing. QA tooling - screen_designs.py catalogue grown to 19 screens (8 scene, 11 UI) plus a `list` subcommand; draft generation keeps existing AI references unless --force. Web export - New Web preset using the non-threaded Godot template (static hosting, no COOP/COEP). tools/build_releases.sh exports build/web and packages build/step-line-web-<ver>.zip; publish_release.sh uploads it with the desktop artifacts. test_web_export.gd covers the preset and release wiring. Verification - Headless suite 177 -> 181, Python tooling 53 tests, all eight smokes pass (including the windowed focus/wheel smoke). Web bundle serves index.html, index.wasm (35 MB) and index.pck (30 MB) with HTTP 200 from a static server. - Version 0.3.0; README, PLAN, RELEASING, TODO/DONE and MEMORY updated.
45 lines
4.4 KiB
Markdown
45 lines
4.4 KiB
Markdown
# ADR-0012: Surface material profiles and generated prop models
|
|
|
|
- **Status:** Accepted
|
|
- **Date:** 2026-10-31
|
|
|
|
## Context
|
|
|
|
The procedural street was built from `BoxMesh` primitives with `StandardMaterial3D` defaults: albedo colour (or a texture) and everything else stock. Every surface therefore shared one roughness, street furniture was a set of scaled boxes, and the sky was a flat `ProceduralSkyMaterial` with no tonemapping, glow, or fog. Captures looked like a blockout: brick tiles were metres wide, the sidewalk blew out to white, the facade had no windows, and night mode only switched the lamp bulbs and the marquee.
|
|
|
|
Two constraints shaped the fix:
|
|
|
|
- The project renders with the **GL Compatibility** backend so it stays small and portable, which rules out features that only exist in Forward+ (`SSAO`, `SSIL`).
|
|
- The repo must build offline in CI, so any asset step has to work without a network or an API key.
|
|
|
|
## Decision
|
|
|
|
**One material module.** `src/game/materials.gd` (`SurfaceMaterials`) owns a named profile table (`brick`, `concrete`, `asphalt`, `awning`, `stone`, `metal`, `painted_metal`, `wood`, `cloth`, `skin`, `glass`, `rubber`) with roughness/metallic pairs, plus the recipes the world and avatars use (`brick()`, `concrete()`, `asphalt()`, `awning()`, `flat()`, `emissive()`, `glass()`, `window_pane()`). The world builder, the NPC avatars, and the prop fallbacks all call it, so a surface has one definition.
|
|
|
|
**One environment tune-up.** `SurfaceMaterials.configure_environment()` applies ACES tonemapping, soft glow (for the marquee, lamp bulbs, and lit windows), aerial fog, and a mild contrast/saturation lift. `GameManager.PHASE_LIGHTING` now carries the per-phase values (`exposure`, `glow`, `fog_color`, `fog_density`, `fog_energy`, `contrast`, `saturation`, `ambient_energy`) next to the existing sun/sky entries, so a phase change still happens in one place. Features unavailable in Compatibility are ignored by the driver rather than gated in code.
|
|
|
|
**Generated prop models.** `tools/model_pipeline.py` describes street furniture (bollard, bench, bin, planter, newspaper box, hydrant) as boxes and cylinders and writes binary glTF (`.glb`) into `assets/models/generated/`. Godot imports each file as a `PackedScene`, and `WorldBuilder._add_prop_model()` instantiates it with a procedural fallback when the file or its import is missing. The build is deterministic and needs no network.
|
|
|
|
**AI only supplies colour.** `model_pipeline.py concept` asks an image model (OpenRouter, same backend as the texture tools) for one studio-style concept per prop. `dominant_colors()` reads the centre of that image and ranks colours by frequency weighted by saturation (a grey backdrop otherwise wins), and `assign_roles()` maps the result onto material roles by hue and value: darkest tone → metal, warm saturated tone → wood, light desaturated tone → stone, green tone → foliage. Colours are converted from sRGB to **linear** before they are written into glTF `baseColorFactor`, because the factor is linear space and writing sRGB made every prop render pastel.
|
|
|
|
## Consequences
|
|
|
|
### Positive
|
|
|
|
- Surfaces read as materials: brick has fine, correctly scaled tiling, asphalt is smoother than brick, glass is smooth and slightly metallic, and the night sky carries warm lit windows.
|
|
- Street furniture is real geometry with per-part materials instead of scaled boxes, and it costs ~700 triangles for the whole set.
|
|
- The prop pipeline works with no key and no network; the concept step is optional and only changes colours.
|
|
- Tests can assert material profiles, environment values, and GLB structure without rendering.
|
|
|
|
### Negative
|
|
|
|
- The generated models are low-poly primitives, not sculpted assets; they are an improvement in silhouette, not photoreal props.
|
|
- Two colour spaces now exist in the tooling. `model_pipeline.srgb_to_linear` must be used for every colour written into glTF, which is easy to forget.
|
|
- Concept images are per-prop inputs that have to be regenerated (and reviewed) when the art direction changes; a poor concept now produces a poor palette.
|
|
- Compatibility rendering means no ambient occlusion, so contact shadows rely on the directional light only.
|
|
|
|
## Follow-up
|
|
|
|
- A 3D-model API (image-to-mesh) could replace the primitive builder behind the same `assets/models/generated/<prop>.glb` contract without touching `WorldBuilder`.
|
|
- Texture atlases for the props would let them share one material instead of one per part.
|