diff --git a/CHANGELOG.md b/CHANGELOG.md index c52514a..32f5f9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ Semantic Versioning. - NPC fabric maps now repeat four times across the body and skip the baked per-character AO, removing the large blotchy patches from shirts, jackets and trousers at portrait distance. +- The Walk of Fame band now uses the paver/terrazzo texture instead of a flat + dark strip, the neighbour awning keeps its cream stripe at close range, and the + distant skyline fades into horizon haze instead of reading as a crisp cut-out. ## [0.9.4] — 2026-11-02 diff --git a/MEMORY.md b/MEMORY.md index e6ce255..d601156 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -71,6 +71,13 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o albedo/normal/ORM maps instead of 128² (AI shirt/trousers are resized to 256). The base and all eight library GLBs plus AO maps were rebuilt; texture files total ~0.5 MB, eight GLBs ~5.75 MB, and the 24-NPC smoke remains 6.90 ms. +- **Environment surface audit (0.9.5, 2026-11-02):** `capture_realism.gd` gained + `surfaces` (Hollywood pavers/asphalt/limestone/facade) and `surfaces_joke` + (neighbour awning, canopy, pavers) stages with the HUD hidden. The Walk of Fame + band now uses `T_CONCRETE` as dark terrazzo; the neighbour awning uses a neutral + tint (`0.62`) and 4× UV so the red/cream stripes survive; the skyline shader + fades its lower 42 m toward a per-phase haze colour. Brick/asphalt picks needed + no replacement. No seams or broken UV scales found. - **NPC fabric UV/AO (0.9.5, 2026-11-02):** cloth/pants use `Materials.NPC_FABRIC_UV_SCALE = Vector3(4,4,1)`, and `NpcAvatar` skips the baked per-character AO on those two slots. The 256² AO was magnifying into large blotches; the fabric now reads as a diff --git a/TODO.md b/TODO.md index 7106b08..97070fa 100644 --- a/TODO.md +++ b/TODO.md @@ -57,16 +57,22 @@ Compatibility/Web parity, deterministic assets and offline CI. ### Environment textures and materials -- [ ] Audit the v2 picks (brick, sidewalk pavers, dry/wet asphalt, awning canvas, +- [x] Audit the v2 picks (brick, sidewalk pavers, dry/wet asphalt, awning canvas, day/dusk/night skylines) at close range and in the entrance/street shots for - seams, tiling, scale, contrast and normal-map strength. -- [ ] Refine brick mortar/wear, paver joints/stains, asphalt aggregate/wet response + seams, tiling, scale, contrast and normal-map strength. (New `surfaces` and + `surfaces_joke` capture stages; no seam or UV-scale failures found.) +- [x] Refine brick mortar/wear, paver joints/stains, asphalt aggregate/wet response and awning weave using the existing image/asset tooling; never overwrite a - pick, use `-vN` candidates and promote validated results. -- [ ] Check UV scales and material roughness/metallic profiles; keep day/night - phase behavior and the wet-street transition intact. -- [ ] Review distant skyline haze and foliage/planter surfaces against the concept - references; fix obvious flatness without regressing readability. + pick, use `-vN` candidates and promote validated results. (The Walk of Fame + band now uses the paver/terrazzo texture; the neighbour awning keeps its + cream stripe via a neutral tint and 4× UV. Brick/asphalt picks held up.) +- [x] Check UV scales and material roughness/metallic profiles; keep day/night + phase behavior and the wet-street transition intact. (Profiles and UV + scales passed; dry/wet asphalt and phase switching unchanged.) +- [x] Review distant skyline haze and foliage/planter surfaces against the concept + references; fix obvious flatness without regressing readability. (Added + world-height haze fade to the skyline shader; foliage/planters stayed + stylized and readable, so no mesh change.) ### Character and prop textures @@ -74,7 +80,8 @@ Compatibility/Web parity, deterministic assets and offline CI. refine fabric weave, skin variation and fixed detail materials. (256² maps rebuilt; fabric now uses a 4× UV weave and skips baked per-character AO, removing the portrait blotches; skin/hair/gear keep AO. Comparison `sheet_npc_baseline_compare-texuv.jpg`.) -- [ ] Re-bake AO where geometry changes; verify the 256² maps and material wiring. +- [x] Re-bake AO where geometry changes; verify the 256² maps and material wiring. + (No character geometry changed in the texture pass; asset/base audits pass.) - [ ] Audit library props and UI/card/icon textures at their actual screen sizes; remove blurry/oversized or unused textures where safe. - [ ] Run texture audits, import, and the full asset test suite; record texture memory diff --git a/docs/asset-tooling.md b/docs/asset-tooling.md index 28c2965..3915098 100644 --- a/docs/asset-tooling.md +++ b/docs/asset-tooling.md @@ -235,11 +235,19 @@ godot --path . --script res://src/test/capture_npc_baseline.gd -- \ python3 tools/npc_baseline.py sheet --dir tmp/npc-baseline ``` -`capture_realism.gd` runs seven stages (day, portrait, evening, late, Joke Shop day, Joke Shop night, studio); +`capture_realism.gd` runs nine stages (day, Hollywood surface close-ups, Joke Shop surface close-ups, portrait, evening, late, Joke Shop day, Joke Shop night, studio); `--stages=day,late` limits it while tuning; `--flashlight` turns on the guard lamp for night stages (used by the Desktop HQ A/B). The `portrait` shot freezes one NPC on open sidewalk and frames the face, which is the only capture that can judge avatar detail -(`--portrait-type=N` picks a role). The generated realism runs under +(`--portrait-type=N` picks a role). For the 0.9.5 texture audit, run the surface +stages directly; the HUD is hidden for them: + +```bash +godot --path . --script res://src/test/capture_realism.gd \ + -- --out=res://tmp/surfaces --stages=surfaces,surfaces_joke +``` + +The generated realism runs under `docs/screens/realism/` are **local, gitignored QA artifacts**; commit only the resulting decisions/docs, not the run folders. The texture-adoption run compared the pre-adoption captures with the current build: on the shots both sets captured, diff --git a/docs/development-status.md b/docs/development-status.md index c538bdb..eb2aa1d 100644 --- a/docs/development-status.md +++ b/docs/development-status.md @@ -43,6 +43,7 @@ Implemented so far: - NPC face-depth pass (0.9.4 work): sclera are recessed and capped by a thin upper lid, and the separate jaw/cheek spheres are replaced by a chin plate and cheekbones sculpted into the head mesh. The full library was rebuilt; the comparison is `sheet_npc_baseline_compare-face.jpg`. - NPC torso/hip transition (0.9.4 work): the pelvis and chest are slightly wider than the waist, giving shirts and jackets a readable taper into the hips without changing the build multipliers or the skeleton. The comparison is `sheet_npc_baseline_compare-torso.jpg`. - Release v0.9.4 (2026-11-02): the tagged commit `a57b86c` passed the Gitea checks and full build and published seven artifacts (Linux, Windows, macOS, NSIS setup, AppImage, Web and Desktop HQ). Release page: . +- Environment texture pass (0.9.5 work): `capture_realism.gd` now has hidden-HUD `surfaces`/`surfaces_joke` close-up stages. The Walk of Fame band uses the paver texture as dark terrazzo, the neighbour awning keeps its cream stripe, and the skyline shader blends its lower 42 m toward a per-phase haze colour. Brick, asphalt, limestone and paver UV scales/profiles passed without replacement. - NPC texture pass (0.9.5 work): the NPC base and all eight library characters now use 256² albedo/normal/ORM maps instead of 128². Cloth and pants use `NPC_FABRIC_UV_SCALE` (4× weave) and skip the baked per-character AO, removing the large blotchy patches in portraits; skin, hair and gear keep baked AO. The base and eight GLBs plus AO maps were rebuilt; the 24-NPC smoke remains green. - NPC visual optimization (0.9.4, complete 2026-11-02): all eight variants passed the portrait/readability, proportion, gear, foot/shoe, headgear and final-context gates. The library uses an 8,000-triangle / 8,000-glTF-vertex cap (up from 6,000/5,000; `npc_base` fallback remains 6,000/4,000); current characters measure 4,482–5,474 triangles and 4,260–5,494 vertices. The 48-frame baseline and 96-frame final-context review pass, the hand/gear offset and shoes are in, and the accepted stylized limits are documented in `npc-visual-review.md`. - NPC face-detail pass (0.9.4 work): ala wings and a smaller nose bridge/tip, one rounded mouth slot, and a darker ear concha; all eight rebuilt. The comparison is `sheet_npc_baseline_compare-face-detail.jpg`. diff --git a/src/game/world_builder.gd b/src/game/world_builder.gd index cfa7c68..e5fd316 100644 --- a/src/game/world_builder.gd +++ b/src/game/world_builder.gd @@ -17,10 +17,14 @@ const T_SKYLINE_NIGHT := preload(PICK_DIR + "skyline_night.png") ## Height of each backdrop plane (metres); the bases sit on the ground plane. const SKYLINE_HEIGHT := 90.0 const SKYLINE_BY_PHASE := { - Rules.Phase.EARLY: {"texture": T_SKYLINE_DAY, "tint": Color(0.98, 1.0, 1.04)}, - Rules.Phase.MIDDAY: {"texture": T_SKYLINE_DAY, "tint": Color(1.02, 1.04, 1.08)}, - Rules.Phase.EVENING: {"texture": T_SKYLINE_DUSK, "tint": Color(0.9, 0.92, 1.0)}, - Rules.Phase.LATE: {"texture": T_SKYLINE_NIGHT, "tint": Color(1.5, 1.55, 1.65)}, + Rules.Phase.EARLY: {"texture": T_SKYLINE_DAY, "tint": Color(0.98, 1.0, 1.04), + "haze": Color(0.62, 0.71, 0.82), "haze_strength": 0.36}, + Rules.Phase.MIDDAY: {"texture": T_SKYLINE_DAY, "tint": Color(1.02, 1.04, 1.08), + "haze": Color(0.66, 0.75, 0.85), "haze_strength": 0.34}, + Rules.Phase.EVENING: {"texture": T_SKYLINE_DUSK, "tint": Color(0.9, 0.92, 1.0), + "haze": Color(0.55, 0.43, 0.40), "haze_strength": 0.30}, + Rules.Phase.LATE: {"texture": T_SKYLINE_NIGHT, "tint": Color(1.5, 1.55, 1.65), + "haze": Color(0.07, 0.09, 0.14), "haze_strength": 0.22}, } ## steps, building facade, restricted door, sidewalk, pavement, and street edge. ## Surfaces use the shared PBR profiles in `src/game/materials.gd`; street @@ -648,8 +652,10 @@ func _build_sunset_neighbours() -> void: # Ground-floor shopfront and a red canvas awning. _add_box(west, Vector3(8.6, 2.4, 0.12), Vector3(-22.6, 1.5, west_front + 0.08), Color(0.08, 0.10, 0.12), false, null, Vector2.ONE, "glass") + # Keep the cream stripe: tinting the whole atlas dark red collapsed the + # canvas into a flat band at portrait distance. _add_box(west, Vector3(9.2, 0.22, 1.3), Vector3(-22.6, 3.0, west_front + 0.65), - Color(0.48, 0.10, 0.08), true, T_AWNING, Vector2(3, 0.5), "awning") + Color(0.62, 0.62, 0.62), true, T_AWNING, Vector2(4, 1), "awning") # Balcony rails on the upper floor. for x in [-24.8, -21.2, -17.6]: _add_box(west, Vector3(2.4, 0.08, 0.85), Vector3(x, 5.6, west_front + 0.55), @@ -1379,8 +1385,10 @@ func _build_walk_of_fame() -> void: var walk := Node3D.new() walk.name = "WalkOfFame" root.add_child(walk) + # Reuse the paver map as a dark terrazzo: a flat colour read as a black + # strip at close range, while the texture supplies the small aggregate. _add_box(walk, Vector3(30.0, 0.035, 1.75), Vector3(0, 0.198, 9.15), - Color(0.20, 0.18, 0.17), false, null, Vector2.ONE, "stone") + Color(0.34, 0.30, 0.27), false, T_CONCRETE, Vector2(18, 1.05), "concrete") var star_mesh := _star_mesh(0.40, 0.17) var brass := Materials.flat(Color(0.70, 0.55, 0.24), "metal") var coral := Materials.flat(Color(0.72, 0.30, 0.28), "stone") @@ -1595,6 +1603,8 @@ func _build_skyline_backdrop() -> void: mat.shader = shader mat.set_shader_parameter("sky_texture", SKYLINE_BY_PHASE[_phase]["texture"]) mat.set_shader_parameter("tint", SKYLINE_BY_PHASE[_phase]["tint"]) + mat.set_shader_parameter("haze_color", SKYLINE_BY_PHASE[_phase]["haze"]) + mat.set_shader_parameter("haze_strength", SKYLINE_BY_PHASE[_phase]["haze_strength"]) mat.set_shader_parameter("repeats", 2.0) _skyline_mat = mat var mesh := PlaneMesh.new() @@ -1633,11 +1643,18 @@ render_mode unshaded, cull_disabled, fog_disabled; uniform sampler2D sky_texture : source_color, repeat_enable, filter_linear_mipmap; uniform vec4 tint : source_color = vec4(1.0); +uniform vec4 haze_color : source_color = vec4(0.62, 0.71, 0.82, 1.0); +uniform float haze_strength = 0.36; uniform float repeats = 2.0; void fragment() { vec4 panorama = texture(sky_texture, vec2(UV.x * repeats, UV.y)); - ALBEDO = panorama.rgb * tint.rgb; + // Atmospheric perspective: blend the lower part of the panorama toward + // the horizon colour so the towers sit in the distance instead of reading + // as a crisp cardboard cut-out. + float world_y = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).y; + float horizon = clamp(1.0 - world_y / 42.0, 0.0, 1.0); + ALBEDO = mix(panorama.rgb * tint.rgb, haze_color.rgb, horizon * haze_strength); ALPHA = panorama.a * tint.a; ALPHA_SCISSOR_THRESHOLD = 0.5; } @@ -2241,6 +2258,8 @@ func set_phase(phase: int) -> void: if _skyline_mat != null: _skyline_mat.set_shader_parameter("sky_texture", entry["texture"]) _skyline_mat.set_shader_parameter("tint", entry["tint"]) + _skyline_mat.set_shader_parameter("haze_color", entry["haze"]) + _skyline_mat.set_shader_parameter("haze_strength", entry["haze_strength"]) var night: bool = phase == Rules.Phase.EVENING or phase == Rules.Phase.LATE set_night_mode(night) if _street_mat != null: diff --git a/src/test/capture_realism.gd b/src/test/capture_realism.gd index 509d3ed..aff9bb2 100644 --- a/src/test/capture_realism.gd +++ b/src/test/capture_realism.gd @@ -62,6 +62,18 @@ const SHOTS_JOKE_DAY := [ const SHOTS_STUDIO := [ {"name": "studio_door", "pos": Vector3(0, 0.1, 6.2), "yaw": 0.0, "pitch": 2.0}, ] +## Surface close-ups for the 0.9.5 texture audit; HUD is hidden for this stage. +const SHOTS_SURFACES := [ + {"name": "surface_pavers", "pos": Vector3(0, 0.1, 10.5), "yaw": 0.0, "pitch": -55.0}, + {"name": "surface_asphalt", "pos": Vector3(0, 0.1, 14.0), "yaw": 180.0, "pitch": -42.0}, + {"name": "surface_limestone", "pos": Vector3(0, 0.1, 2.0), "yaw": 0.0, "pitch": 8.0}, + {"name": "surface_facade_wide", "pos": Vector3(-2.0, 0.1, 13.2), "yaw": 12.0, "pitch": 20.0}, +] +const SHOTS_SURFACES_JOKE := [ + {"name": "surface_awning", "pos": Vector3(-22.6, 0.1, 2.6), "yaw": 0.0, "pitch": 14.0}, + {"name": "surface_canopy", "pos": Vector3(0, 0.1, 7.5), "yaw": 0.0, "pitch": 24.0}, + {"name": "surface_pavers_joke", "pos": Vector3(0, 0.1, 10.0), "yaw": 0.0, "pitch": -55.0}, +] ## A face-on portrait: staged relative to a live NPC by `_stage_portrait()`. const SHOTS_PORTRAIT := [ {"name": "npc_portrait", "pos": Vector3.ZERO, "yaw": 0.0, "pitch": 0.0, @@ -70,6 +82,8 @@ const SHOTS_PORTRAIT := [ ## Stage list: shift time selects the phase, `venue` switches the scenario first. const STAGES := [ {"name": "day", "elapsed": 0.0, "venue": "", "shots": SHOTS_DAY, "night": false}, + {"name": "surfaces", "elapsed": 0.0, "venue": "", "shots": SHOTS_SURFACES, "night": false}, + {"name": "surfaces_joke", "elapsed": 0.0, "venue": "joke_shop", "shots": SHOTS_SURFACES_JOKE, "night": false}, # Portrait next, while the day crowd is still on the street. {"name": "portrait", "elapsed": 0.0, "venue": "", "shots": SHOTS_PORTRAIT, "night": false}, @@ -182,6 +196,7 @@ func _begin_stage() -> void: gm.hud.clear_log() gm.hud.set_target_label("") gm.hud.set_action_hint("") + gm.hud.visible = not str(stage["name"]).begins_with("surfaces") if _flashlight: var want := bool(stage["night"]) if want != bool(gm.flashlight_on): diff --git a/src/test/tests/test_world.gd b/src/test/tests/test_world.gd index 140f148..f01afec 100644 --- a/src/test/tests/test_world.gd +++ b/src/test/tests/test_world.gd @@ -113,8 +113,13 @@ func test_hollywood_uses_the_real_landmark_layout() -> bool: var banners := root.find_child("EventBanners", true, false) var d := TestUtils.check(banners != null and banners.get_child_count() >= 9, "three red event banners with their trim are present") - var e := TestUtils.check(root.find_child("WalkOfFame", true, false) != null, - "the Hollywood Walk of Fame strip is built") + var walk := root.find_child("WalkOfFame", true, false) + var walk_band := walk.get_child(0) as MeshInstance3D if walk != null else null + var walk_material: Material = walk_band.mesh.surface_get_material(0) if walk_band != null else null + var e := TestUtils.check(walk != null and walk_material != null + and walk_material is StandardMaterial3D + and (walk_material as StandardMaterial3D).albedo_texture != null, + "the Hollywood Walk of Fame strip is built with the terrazzo texture") var f := TestUtils.check(root.get_node_or_null("NeighbourBlocks/ElCapitanTheatre") != null and root.get_node_or_null("NeighbourBlocks/EastRetailBlock") != null, "the El Capitan Theatre and east retail neighbours are built") @@ -260,8 +265,12 @@ func test_skyline_backdrop_uses_alpha() -> bool: var mat: ShaderMaterial = builder._skyline_mat var b := TestUtils.check(mat != null and mat.shader != null, "skyline shader material captured") var code := mat.shader.code if mat != null and mat.shader != null else "" - var c := TestUtils.check(code.contains("ALPHA") and code.contains("fog_disabled"), - "backdrop shader is alpha-scissored and fog-free") + var c := TestUtils.check(code.contains("ALPHA") and code.contains("fog_disabled") + and code.contains("haze_strength"), + "backdrop shader is alpha-scissored, fog-free and haze-blended") + var haze := float(mat.get_shader_parameter("haze_strength")) if mat != null else 0.0 + var c2 := TestUtils.check(haze > 0.0, "skyline haze is enabled") + c = c and c2 var day: Texture2D = mat.get_shader_parameter("sky_texture") builder.set_phase(Rules.Phase.LATE) var night: Texture2D = mat.get_shader_parameter("sky_texture")