Tune the flashlight against measured luminance and refresh the venue gallery
Full build / Import + headless tests (push) Successful in 41s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-06 20:20:22 +01:00
1 parent 2ba40f3578
commit 16dc1dfea4
20 files changed
+77 -15

No files matched your search

+6
View File
@@ -6,6 +6,12 @@ Semantic Versioning.
## [Unreleased]
### Fixed
- The flashlight actually lights what it points at. It was tuned to a beam so weak it changed the
frame by less than one 255th of a level; it now throws a visible pool on the door and the pavement
ahead, in both venues.
### Changed
- Street greenery now reads as leaves instead of green rocks: the planter shrubs and tree crowns
+10
View File
@@ -130,6 +130,16 @@ canonical implemented-features list in `docs/project/development-status.md`.
gear library. The procedural library + MakeHuman head stay in the tree as the tested fallback
(`build --source procedural`), not in the shipped roster. Silhouette variety is the known
regression — three meshes for twelve roles — and the modular part library is the `0.9.10` fix.
- **Tune lights against a measurement, not a look (2026-11-06):** the shipped flashlight changed the
late-night frame by 0.1/255 and nobody noticed. `capture_realism.gd --stages=late --flashlight`
renders the same frame with the lamp on and off; mean luminance of the aimed region and the near
pavement is now the acceptance number. Current: energy 10, range 45 m, cone 24°, tilted 5° down →
13.5 → 29.7 aimed, 3.7 → 11.7 near ground.
- **Docs live in category folders (2026-11-06):** `docs/project/` (architecture, development status),
`docs/design/` (gameplay, venues, ui-copy, accessibility, known-limitations), `docs/assets/`
(asset tooling, blender assets, npc sourcing, 3d-model-review, makehuman head), `docs/process/`
(getting started, release QA, releasing), plus `docs/adr/`, `docs/screens/`, `docs/screenshots/`.
Write to the new paths; `docs/*.md` at the root no longer exists apart from `README.md`.
- **Foliage is leaf cards, not sphere stacks (2026-11-06):** `_shrub()` is one low-poly inner mass
plus 18–53 two-triangle cards scattered on the canopy shell by a jittered Fibonacci sequence, each
rotated onto the canopy normal and tilted off it. No alpha texture, so the flat-colour library and
+12 -4
View File
@@ -957,8 +957,17 @@ scorecard source, this milestone is the actionable list.
- [ ] Joke Shop / Hollywood shape language: fix the ranked remaining items (west rounded corner,
canopy pitch, facade reveal depth) only where a capture shows a readable gain.
- [ ] `lighting`/`lookdev` pass: flashlight cone/energy for the `06-night-flashlight` stage,
marquee/event-banner emissive read under the Compatibility renderer, street-lamp pools.
- [x] **Flashlight tuned by measurement.** The late-night capture proved it did nothing: energy 4.2,
20 m range and a 14° cone changed the frame by **0.1/255**. Now 10 energy, 45 m, 24°, tilted 5°
down — the aimed region goes 13.5 → 29.7 and the near pavement 3.7 → 11.7 (0–255), and the
committed `06-night-flashlight` shot shows the beam on the steps and the NPC in front.
- [ ] Marquee/event-banner emissive read under the Compatibility renderer: measure the banner
luminance against the reference night board and adjust only if it still clips.
- [ ] Street-lamp pools: the night ground sits at 3.8/255 away from the sconces. Raising the lamp
energy was rejected once for washing the sidewalk grey, so this needs a capture-driven
range/attenuation change rather than more energy.
- [x] Refresh `docs/screenshots/` for the whole roster and the new flashlight; `check_screenshots.sh`
reports 21 good / 0 bad.
- [ ] Set dressing: fix the Joke Shop queue story (podium/stanchion line at the door), rope
barrier thickness, the west palm overlap and the empty sidewalk light pool; apply the same
review to Hollywood and the TV Studio.
@@ -966,8 +975,7 @@ scorecard source, this milestone is the actionable list.
### Evidence and release
- [ ] Refresh `sheet_props.jpg`, `sheet_npc_gear.jpg`, the three-up boards, the affected venue
captures and the README asset rows.
- [x] `sheet_props.jpg` and `sheet_npc_gear.jpg` rebuilt; the 21-image venue gallery regenerated.
- [ ] Re-run `tools/check_screenshots.sh` and `python3 tools/check_docs.py`; record both results.
- [ ] Release `0.9.11` with the full procedure.
+19
View File
@@ -302,6 +302,25 @@ renderer are untouched.
| `street_tree` | 2,592 tris | 1,506 |
| Props total (20) | 13,808 | 11,784 |
## Lighting pass — flashlight measured 2026-11-06
`capture_realism.gd --stages=late --flashlight` renders the same frame with the lamp off and on, so
the beam's contribution is a number instead of an impression. Mean luminance, 0–255:
| Region | Lamp off | Old settings | Shipped now |
| --- | --- | --- | --- |
| Aimed centre (facade) | 13.5 | 13.6 | **29.7** |
| Near pavement | 3.7 | 3.7 | **11.7** |
The old flashlight (energy 4.2, 20 m range, 14° cone, aimed dead ahead) moved the frame by 0.1/255 —
it was effectively off. The shipped values are 10 energy, 45 m, 24°, tilted 5° down: the beam lands
on the door or person being checked *and* puts a pool on the pavement in front, without flooding the
facade. Verified in both venues (Hollywood facade and the Joke Shop door).
Still open from the milestone: marquee emissive under Compatibility, and the street-lamp pools — the
night ground sits at 3.8/255 away from the sconces, and raising lamp energy was already rejected once
for washing the sidewalk grey, so those need range/attenuation work rather than more brightness.
## Scorecard skeleton
Verdicts use `qa-review` semantics: **P0** (blocker: wrong scale, broken normals, budget fail),
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 KiB

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 KiB

After

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 757 KiB

After

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 377 KiB

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 607 KiB

After

Width:  |  Height:  |  Size: 607 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 KiB

After

Width:  |  Height:  |  Size: 916 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 529 KiB

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

+11 -6
View File
@@ -224,12 +224,17 @@ func _build_guard() -> GuardController:
flashlight.name = "Flashlight"
flashlight.visible = false
flashlight.position = Vector3(0, -0.15, -0.4)
# Aim straight ahead: tilting it down made the cone hit the ground right in
# front of the guard instead of the person or door being checked.
flashlight.rotation_degrees = Vector3.ZERO
flashlight.spot_range = 30.0 if Materials.modern_light_falloff() else 20.0
flashlight.spot_angle = 14.0
flashlight.light_energy = 4.2
# Aimed a few degrees down: straight ahead left the pavement in front of the guard
# black (night ground measured 3.7/255 with the lamp on), and tilting further hid
# the door or person being checked.
flashlight.rotation_degrees = Vector3(-5.0, 0.0, 0.0)
# Tuned against the late-night capture: the old 4.2 energy / 20 m range / 14 deg
# cone changed the frame by 0.1/255 — effectively an invisible flashlight. These
# values lift the aimed region from 13.5 to 29.7 and the near pavement from 3.7 to
# 11.7 without blowing the facade out.
flashlight.spot_range = 45.0
flashlight.spot_angle = 24.0
flashlight.light_energy = 10.0
flashlight.spot_attenuation = 1.0
flashlight.spot_angle_attenuation = 0.9
# The volumetric shaft is a Forward+ (desktop HQ) effect. Compatibility and
+19 -5
View File
@@ -38,16 +38,30 @@ func test_flashlight_aims_a_tight_forward_spot() -> bool:
var guard: Node3D = gm.call("_build_guard")
var lamp: SpotLight3D = guard.get_node_or_null("Head/Flashlight") as SpotLight3D
var a := TestUtils.check(lamp != null, "flashlight lives on the guard's head")
var b := TestUtils.check(lamp != null and lamp.rotation_degrees.is_equal_approx(Vector3.ZERO),
"flashlight aims straight ahead")
var c := TestUtils.check(lamp != null and lamp.spot_angle <= 20.0,
"flashlight cone is narrow enough to read as a torch")
# The lamp is tilted a few degrees down so the beam also puts a pool on the
# pavement in front; straight ahead left that ground black. Anything past ~8 deg
# drops the door being checked out of the cone.
var b := TestUtils.check(lamp != null
and lamp.rotation_degrees.x <= 0.0 and lamp.rotation_degrees.x >= -8.0
and is_zero_approx(lamp.rotation_degrees.y) and is_zero_approx(lamp.rotation_degrees.z),
"flashlight aims forward with a slight downward tilt (x %.1f)" % (
lamp.rotation_degrees.x if lamp else 0.0))
# Measured: the old 4.2 energy / 20 m / 14 deg beam changed the late-night frame by
# 0.1/255 and read as off. These bounds keep it a torch (not a floodlight) while
# still lighting what the guard looks at.
var c := TestUtils.check(lamp != null and lamp.spot_angle >= 20.0
and lamp.spot_angle <= 30.0,
"flashlight cone reads as a torch (%.0f deg)" % (lamp.spot_angle if lamp else 0.0))
var c2 := TestUtils.check(lamp != null and lamp.light_energy >= 8.0
and lamp.spot_range >= 30.0,
"flashlight is bright enough to see (energy %.1f, range %.0f m)" % [
lamp.light_energy if lamp else 0.0, lamp.spot_range if lamp else 0.0])
var d := TestUtils.check(lamp != null and not lamp.visible, "flashlight starts off")
var e := TestUtils.check(lamp != null
and is_zero_approx(lamp.light_volumetric_fog_energy),
"headless/compatibility builds leave the flashlight non-volumetric")
root.free()
return a and b and c and d and e
return a and b and c and c2 and d and e
func test_flashlight_widens_perception() -> bool:
## Without the flashlight, an NPC at AWARE_RADIUS + 3 m is unaware.