# 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/.glb` contract without touching `WorldBuilder`. - Texture atlases for the props would let them share one material instead of one per part.