docs(lighting): A7 Fix D investigation RESOLVED + implementation spec (#140)
Resolve the Fix D contradiction with decomp (workflow wf_f660eb88 + adversarial verify) + 4 live cdb captures. The D3D-FF model was the WRONG oracle: retail has TWO light systems — STATIC torches BAKE into wall vertices (calc_point_light, triple-clamped: range gate + per-channel min(scale*color,color) + per-vertex [0,1] from black), DYNAMIC lights go D3D hardware. The captured intensity=100 is the purple PORTAL (magenta, dynamic), not a wall torch. Ground truth: 38 static warm torches (orange (1,0.588,0.314)/cream, intensity=100, falloff 3-5) + 2 dynamic. acdream over-brightness = two confirmed bugs: D-1 mesh_modern.vert folds ambient+sun+torches into one UNCLAMPED accumulator (single frag clamp) -> warm blowout; D-2 EnvCellRenderer never binds SSBO 4/5 so the cell shell reads a leaked light set. Spec: D-1 in-shader clamp-split (clamp the torch sum on its own before ambient/sun); D-2 bind the shell's own per-cell light set (mirror WbDrawDispatcher); LightBake.cs is the C# conformance oracle. Adds the 4 reusable cdb capture scripts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f384d036a3
commit
c407104ab9
7 changed files with 419 additions and 46 deletions
|
|
@ -45,57 +45,83 @@ was wrong (Fix C). Don't re-investigate the purple.
|
|||
|
||||
---
|
||||
|
||||
## OPEN — Fix D: outdoor OBJECTS too bright near torches
|
||||
## RESOLVED — Fix D: outdoor walls too bright near torches (contradiction settled 2026-06-18)
|
||||
|
||||
**Symptom (user, 2026-06-18):** the Holtburg meeting-hall walls blow out warm/bright
|
||||
in acdream vs dim in retail. Fix A/B/C did NOT touch this. It's the per-object
|
||||
point-light **contribution on objects**.
|
||||
**Symptom (user):** Holtburg meeting-hall walls blow out **warm**/bright in acdream
|
||||
vs dim in retail. The contradiction ("D3D-FF math says color×100 should blow WHITE,
|
||||
yet retail is DIM") is **resolved**: the D3D-FF model was the WRONG ORACLE for these
|
||||
walls. Settled by a 5-thread decomp workflow (`wf_f660eb88`) + adversarial verify +
|
||||
4 live cdb captures. **⚠ The "DO NOT port the D3D-FF model" warning still stands** —
|
||||
not because it'd be too bright, but because it's the wrong path entirely.
|
||||
|
||||
### Grounded (cdb + decomp) — retail's object point-light path
|
||||
`Render::config_hardware_light` (0x0059ad30) builds the `D3DLIGHT9`:
|
||||
- `Diffuse = color × intensity`
|
||||
- `Attenuation = (0, 1, 0)` ⇒ **1/d** (inverse-LINEAR; acdream's `calc_point_light`
|
||||
is `~1/d²` via norm = distsq·d)
|
||||
- `Range = falloff × rangeAdjust`, **`rangeAdjust = 1.5`** (0x00820cc4) ⇒ torch Range
|
||||
= 6×1.5 = **9 m** (LARGER than acdream's falloff×1.3 = 7.8 m — range is NOT why
|
||||
we're brighter)
|
||||
- live `LIGHTINFO` captured: torch `type=0 intensity=100 falloff=6`; a 2nd light
|
||||
`intensity=2.25 falloff=10`
|
||||
- `d3d_material.Diffuse = (1,1,1)` white (decomp 0x00539774)
|
||||
### Render path (Ghidra xrefs — unambiguous, two SEPARATE light systems)
|
||||
- **STATIC lights → CPU vertex BAKE.** `RenderDeviceD3D::DrawEnvCell` (0x0059F170) →
|
||||
`D3DPolyRender::SetStaticLightingVertexColors` (0x0059CFE0) → `calc_point_light`
|
||||
(0x0059C8B0, its SOLE caller). Wall torches are STATIC objects → baked into vertex
|
||||
colours. AC town buildings are EnvCell structures, so their walls take this path.
|
||||
- **DYNAMIC lights → D3D hardware FF.** `add_dynamic_light` → `insert_light` (0x0054D1B0)
|
||||
→ `config_hardware_light` (0x0059AD30); `minimize_envcell_lighting` (0x0054C170)
|
||||
enables ONLY the dynamic subset (class 2) for the cell — statics are NEVER hardware-
|
||||
enabled for the cell. (`minimize_object_lighting` 0x0054D480 enables both, for free
|
||||
GfxObjs.) So `config_hardware_light` — where last session's `intensity=100` was seen —
|
||||
carries DYNAMIC lights for cells, not the wall torches.
|
||||
|
||||
### THE CONTRADICTION (resolve this FIRST next session)
|
||||
By `mat(1)×color×100×(N·L)×(1/d)`, a torch 3 m away = `color×33` ⇒ retail's walls
|
||||
SHOULD blow to **WHITE** — but they're **DIM**. Material diffuse, range, and
|
||||
intensity are all captured and ruled out. So the scaling lives in the building's
|
||||
**RENDER PATH**, which is unknown. **⚠ DO NOT port the D3D-FF model — by this math it
|
||||
would make objects BRIGHTER (white), the opposite of the fix.**
|
||||
### Why retail stays warm-but-DIM (the bake is triple-clamped — `calc_point_light`)
|
||||
Per light: `range = falloff×1.3` hard gate; half-Lambert wrap `(1/1.5)(N·D + 0.5·d)`;
|
||||
`norm = (distsq>1)? distsq·d : d` (~1/d²); `scale = (1−d/range)·intensity·(wrap/norm)`;
|
||||
then the **decisive per-channel cap `result = min(scale·color, color)`** — one light adds
|
||||
**at most its own (sub-1.0, warm) colour**, however large intensity is. Caller sums from
|
||||
**BLACK** (no ambient/sun in the accumulator) over all static lights, then **clamps the
|
||||
sum to [0,1]** per channel before packing `vertex.diffuse`. White needs many in-range
|
||||
lights stacking past 1.0; a hall has a handful, each warm-capped.
|
||||
|
||||
### The decisive next capture
|
||||
Determine the static building's ACTUAL render path:
|
||||
- **Hypothesis (a) — MOST LIKELY:** static buildings DON'T use D3D hardware lighting.
|
||||
They use the `D3DPolyRender::SetStaticLightingVertexColors` BAKE (0x0059cfe0 →
|
||||
`calc_point_light`), like EnvCells. The `config_hardware_light` lights I captured
|
||||
were for a DIFFERENT object (player / creature / the purple PORTAL — note the
|
||||
`intensity=100` could be the portal, not the wall torch). If (a) holds, acdream's
|
||||
`calc_point_light` is the RIGHT model and the over-brightness is the **per-channel
|
||||
cap** (`min(scale×col,col)` lets several torches each reach full colour and sum to
|
||||
white) and/or **too many torches selected** per object and/or a missing clamp step.
|
||||
- **Hypothesis (b):** `D3DRS_LIGHTING` off / lights not `LightEnable`'d for the
|
||||
building draw.
|
||||
- **How to capture:** break at `SetStaticLightingVertexColors` (0x0059cfe0) and see
|
||||
whether it's called for the building's mesh (confirms the bake path); and/or
|
||||
inspect the render state around the static-object `DrawIndexedPrimitive`
|
||||
(`D3DRS_LIGHTING`, which lights are enabled). Also: at `config_hardware_light`,
|
||||
dump WHICH object/owner the light is being configured for to identify whether the
|
||||
`intensity=100` light is the torch or the portal.
|
||||
### Live cdb ground truth (4 captures; scripts in `tools/cdb/a7-fixd-*.cdb`)
|
||||
`Render::world_lights` @ **0x008672a0** (LightParms): `num_static_lights` @ +0x104,
|
||||
`sorted_static_lights[]` (RenderLight*, info @ RL+0x70) @ +0x3498, `num_dynamic_lights`
|
||||
@ +0x3588. Captured standing in Holtburg:
|
||||
- **num_static_lights = 38**, **num_dynamic_lights = 2.**
|
||||
- **2 DYNAMIC** (`add_dynamic_light`, d3dIdx 1–2): viewer light `intensity=2.25 falloff=10
|
||||
color=(1,1,1)` white; **PORTAL** `intensity=100 falloff=6 color=(0.784,0,0.784)` MAGENTA.
|
||||
→ **the `intensity=100` light is the purple PORTAL (dynamic/hardware), NOT a wall torch.**
|
||||
- **38 STATIC** wall torches, all `type=0 intensity=100`, **WARM**: orange
|
||||
`(1.0, 0.588, 0.314)` falloff 4, and cream `(0.980, 0.843, 0.612)` falloff 3–5
|
||||
(→ bake range ~3.9–6.5 m). Torches DO carry intensity=100, but the per-channel cap
|
||||
pins each to its warm colour ⇒ retail walls go warm, not white.
|
||||
|
||||
### acdream side — where the fix lands
|
||||
- acdream runs `calc_point_light` (wrap/norm + per-channel cap) for ALL meshes via
|
||||
`mesh_modern.vert` `pointContribution` (objects AND cells — Fix A).
|
||||
- If buildings use the bake, the likely fix is in the **cap / sum / count**, not the
|
||||
attenuation model. Files: `src/AcDream.App/Rendering/Shaders/mesh_modern.vert`
|
||||
(`pointContribution` + `accumulateLights`), `src/AcDream.Core/Lighting/LightManager.cs`
|
||||
(`SelectForObject`), `LightBake.cs` (verbatim calc_point_light, still unwired).
|
||||
### acdream's actual bug — TWO real causes (both verified in source)
|
||||
- **D-1 (math, primary): unclamped accumulator folding ambient+sun+torches.**
|
||||
`mesh_modern.vert` `accumulateLights` starts `lit = uCellAmbient.xyz` (:184), ADDS
|
||||
sun (:196), ADDS each capped torch (:206), returns UNCLAMPED (:208); the ONLY clamp is
|
||||
one `min(lit,1.0)` in `mesh_modern.frag:92` AFTER a lightning bump (:89). The per-light
|
||||
cap (:180) IS faithful. But pouring ambient + sun + up-to-8 intensity-100 WARM torches
|
||||
into ONE bucket and trimming only at the end overflows to warm-white. Retail clamps the
|
||||
torch sum on its OWN (from black); ambient/sun are a separate term.
|
||||
- **D-2 (state, compounding): EnvCell shell SSBO binding leak.**
|
||||
`EnvCellRenderer.cs:1225-1230` (RenderModernMDIInternal) binds SSBO 0/1/2/3 only, NEVER
|
||||
4 (`gLights`) or 5 (`instanceLightIdx`) — which the shared `mesh_modern.vert` reads at
|
||||
:204-206. Only `WbDrawDispatcher` binds 4/5. Indoor DrawInside interleaves the two, so a
|
||||
cell shell reads whatever LEAKED light set the last WbDrawDispatcher draw left bound —
|
||||
a different entity's torches, wrong per-instance indices ⇒ wrong/over-bright walls.
|
||||
- `LightBake.cs` (verbatim CPU port) exists but is UNWIRED (zero callers); the live path is
|
||||
the in-shader version missing the clamp shape.
|
||||
|
||||
### Fix plan (REPORT-ONLY — implement in a separate session, with the no-workaround rule)
|
||||
- **D-1:** accumulate point/spot into a LOCAL `pointAcc`, `saturate` it to [0,1] BEFORE
|
||||
adding ambient + sun — mirrors `SetStaticLightingVertexColors` (sum-from-black, clamp the
|
||||
point sum). Keep the per-light `min(scale·baseCol, baseCol)` (vert:180). Files:
|
||||
`mesh_modern.vert` (split accumulator + clamp), `mesh_modern.frag` (reorder/drop the
|
||||
single clamp). Conformance golden: a wall ≤~5 m from an orange `(1,0.588,0.314)` torch
|
||||
bakes warm-but-≤[0,1], NOT white.
|
||||
- **D-2:** EnvCell shell must bind binding 4 (global lights) + 5 (per-instance light set)
|
||||
for ITS OWN instances — compute a per-shell set like `WbDrawDispatcher.ComputeEntityLightSet`
|
||||
(LightManager.SelectForObject); option (b) all-(-1) fallback = NO point lights is a STOPGAP
|
||||
(needs approval + a divergence-register row). File: `EnvCellRenderer.cs` RenderModernMDIInternal.
|
||||
- **Stale doc to fix in the D-1 commit:** divergence-register `AP-35` still describes the
|
||||
point-light path as per-pixel `mesh_modern.frag:52` with the wrap "NOT ported"; Fix A
|
||||
(`aa94ced`) moved it to per-vertex `mesh_modern.vert:163` WITH the wrap.
|
||||
- **Do NOT port the D3D-FF hardware model for the walls** (config_hardware_light's
|
||||
color×intensity / (0,1,0)=1/d / Range=falloff×1.5) — it lights GfxObjs/dynamics, not the
|
||||
baked walls.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,211 @@
|
|||
# A7 Fix D — warm torch over-brightness on indoor walls (#140)
|
||||
|
||||
**Date:** 2026-06-18 **Milestone:** M1.5 (Indoor world feels right) → A7 lighting
|
||||
**Status:** design approved (user pre-approved 2026-06-18); ready for implementation plan.
|
||||
**Investigation source of truth:**
|
||||
[`docs/research/2026-06-18-lighting-a7-fixABC-shipped-fixD-handoff.md`](../../research/2026-06-18-lighting-a7-fixABC-shipped-fixD-handoff.md)
|
||||
(RESOLVED section) + `claude-memory/reference_retail_ambient_values.md`.
|
||||
|
||||
## Problem
|
||||
|
||||
The Holtburg meeting-hall walls (and outdoor objects near torches) blow out
|
||||
**warm/bright** in acdream vs **dim** in retail. Fix A/B/C (shipped) did not touch this.
|
||||
|
||||
The handoff "contradiction" (D3D-FF math `color×100×N·L/d` says walls should go WHITE,
|
||||
yet retail is DIM) is **resolved**: the D3D-FF hardware model is the **wrong oracle**
|
||||
for these walls. Two SEPARATE retail light systems (Ghidra xrefs, unambiguous):
|
||||
|
||||
- **STATIC lights → CPU vertex BAKE**: `DrawEnvCell` (0x0059F170) →
|
||||
`SetStaticLightingVertexColors` (0x0059CFE0) → `calc_point_light` (0x0059C8B0, its
|
||||
SOLE caller). Wall torches are STATIC objects → baked into vertex colours.
|
||||
- **DYNAMIC lights → D3D hardware FF**: `add_dynamic_light` → `config_hardware_light`
|
||||
(0x0059AD30); `minimize_envcell_lighting` (0x0054C170) enables ONLY the dynamic
|
||||
subset for a cell. The previously-captured `intensity=100` light is on THIS path.
|
||||
|
||||
`calc_point_light` is mathematically **bounded**: range gate `d < falloff×1.3`; the
|
||||
decisive **per-channel cap `min(scale·color, color)`** (a torch adds at most its own
|
||||
sub-1.0 colour, any intensity); caller sums from **BLACK** then clamps the sum to
|
||||
`[0,1]` (no ambient/sun in the bake accumulator). White needs many in-range lights;
|
||||
a hall has a handful, each warm-capped.
|
||||
|
||||
### Ground truth (live cdb, `tools/cdb/a7-fixd-*.cdb`; `Render::world_lights` @ 0x008672a0)
|
||||
|
||||
Holtburg: **38 static + 2 dynamic** lights.
|
||||
|
||||
| Light | path | type | intensity | falloff | colour (r,g,b) |
|
||||
|---|---|---|---|---|---|
|
||||
| viewer light | dynamic / HW | point | 2.25 | 10 | (1, 1, 1) white |
|
||||
| **portal** | dynamic / HW | point | **100** | 6 | **(0.784, 0, 0.784) magenta** ← the captured "intensity=100"; NOT a wall torch |
|
||||
| 38× wall torch | static / **bake** | point | 100 | 3–5 | **(1.0, 0.588, 0.314) orange** / (0.980, 0.843, 0.612) cream |
|
||||
|
||||
Torches carry `intensity=100` too, but the per-channel cap pins each to its warm
|
||||
colour ⇒ retail walls go warm, never white.
|
||||
|
||||
## Root cause in acdream (both verified in source)
|
||||
|
||||
Two independent bugs, both touching the meeting-hall walls; this spec fixes both.
|
||||
|
||||
**D-1 (math, primary): unclamped accumulator folding ambient + sun + torches.**
|
||||
[`mesh_modern.vert`](../../../src/AcDream.App/Rendering/Shaders/mesh_modern.vert)
|
||||
`accumulateLights` starts `lit = uCellAmbient.xyz` (:184), adds sun (:196), adds each
|
||||
capped torch (:206), returns UNCLAMPED (:208); the only clamp is `min(lit,1.0)` in
|
||||
`mesh_modern.frag:92` after a lightning bump. The per-light cap (vert:180) is faithful.
|
||||
But pouring ambient + sun + up-to-8 intensity-100 WARM torches into ONE bucket and
|
||||
trimming only at the end overflows to warm-white. Retail clamps the torch sum on its
|
||||
OWN (from black); ambient/sun are a separate material-lit term.
|
||||
|
||||
**D-2 (state, compounding): EnvCell shell SSBO binding leak.**
|
||||
[`EnvCellRenderer.RenderModernMDIInternal`](../../../src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs)
|
||||
binds SSBO 0/1/2/3 only, NEVER **4** (`gLights`) or **5** (`instanceLightIdx`) — which
|
||||
the shared `mesh_modern.vert` reads unconditionally (:204-206). Only `WbDrawDispatcher`
|
||||
binds 4/5. Indoor `DrawInside` interleaves the two, so a cell shell reads whatever
|
||||
LEAKED light set the last `WbDrawDispatcher` draw left bound (a different entity's
|
||||
torches, wrong per-instance indices) ⇒ wrong/over-bright walls.
|
||||
|
||||
`LightBake.cs` (verbatim CPU port of the bake) exists but is UNWIRED (zero callers).
|
||||
|
||||
## Design
|
||||
|
||||
Decisions (user, 2026-06-18): **D-1 = small in-shader clamp split** (not a CPU bake);
|
||||
**D-1 + D-2 land together**, single visual verification.
|
||||
|
||||
### D-1 — clamp the torch sum on its own (mirrors `SetStaticLightingVertexColors`)
|
||||
|
||||
In `mesh_modern.vert` `accumulateLights`, give point/spot lights their own accumulator,
|
||||
saturate it to `[0,1]` BEFORE it joins ambient + sun. The per-light cap and
|
||||
`pointContribution` are unchanged; the only new operation is one `min(pointAcc, 1.0)`.
|
||||
|
||||
```glsl
|
||||
vec3 accumulateLights(vec3 N, vec3 worldPos, int instanceIndex) {
|
||||
// ambient + sun = retail's material-lit term
|
||||
vec3 lit = uCellAmbient.xyz;
|
||||
int activeLights = int(uCellAmbient.w);
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (i >= activeLights) break;
|
||||
if (int(uLights[i].posAndKind.w) != 0) continue; // directional only
|
||||
vec3 Ldir = -uLights[i].dirAndRange.xyz;
|
||||
float ndl = max(0.0, dot(N, Ldir));
|
||||
lit += uLights[i].colorAndIntensity.xyz * uLights[i].colorAndIntensity.w * ndl;
|
||||
}
|
||||
// point/spot torches: their OWN accumulator, clamped to [0,1] (retail baked emissive)
|
||||
vec3 pointAcc = vec3(0.0);
|
||||
int base = instanceIndex * 8;
|
||||
for (int k = 0; k < 8; ++k) {
|
||||
int gi = instanceLightIdx[base + k];
|
||||
if (gi < 0) continue;
|
||||
pointAcc += pointContribution(N, worldPos, gLights[gi]); // per-light cap unchanged
|
||||
}
|
||||
lit += min(pointAcc, vec3(1.0)); // <-- THE FIX
|
||||
return lit; // frag still does final min(lit, 1.0)
|
||||
}
|
||||
```
|
||||
|
||||
Behaviour change is confined to surfaces whose torch sum currently exceeds 1.0 —
|
||||
normally-lit surfaces are byte-identical (no regression). Shared by every mesh using
|
||||
this shader (outdoor objects AND cell walls), matching the issue's scope.
|
||||
`mesh_modern.frag:92`'s final `min(lit, 1.0)` stays as-is (it clamps the total to the
|
||||
retail FF pixel clamp). The lightning bump (frag:89) is unaffected.
|
||||
|
||||
### D-2 — the EnvCell shell binds its OWN light set
|
||||
|
||||
`EnvCellRenderer` must own its lighting like `WbDrawDispatcher` does, instead of reading
|
||||
leaked SSBO state. Mirror `WbDrawDispatcher`'s proven pattern
|
||||
(`ComputeEntityLightSet`/`AppendCurrentLightSet`/`UploadGlobalLights`):
|
||||
|
||||
1. **Wire `LightManager` in** via `Initialize(...)` (alongside `_shader`). Self-contained
|
||||
pass — per `feedback_render_self_contained_gl_state`, EnvCellRenderer already
|
||||
re-uploads its own `uViewProjection`; it now also uploads/binds its own lights.
|
||||
2. **Binding 4 (global lights):** upload `LightManager.PointSnapshot` itself, packed
|
||||
identically to `WbDrawDispatcher.UploadGlobalLights` (the `GlobalLight` SSBO layout:
|
||||
`posAndKind`, `dirAndRange`, `colorAndIntensity`, `coneAngleEtc`). Same snapshot →
|
||||
same indices both renderers reference. `BuildPointLightSnapshot` is already called
|
||||
once per frame before rendering. **Extract the packing into a shared helper** so the
|
||||
two renderers cannot drift (a `GlobalLightPacker` in `AcDream.App/Rendering/Wb/` or a
|
||||
static on the snapshot type) — do not copy-paste the struct layout.
|
||||
3. **Binding 5 (per-instance light set):** per **cell** (keyed on `allInstances[i].CellId`),
|
||||
compute the set ONCE with `LightManager.SelectForObject(snapshot, cellCenter,
|
||||
cellRadius, set)` (camera-independent; cache per CellId, reuse for all that cell's
|
||||
part-instances — like `WbDrawDispatcher` reuses one set per entity). Write the 8-int
|
||||
set per instance into a new buffer parallel to `_gpuInstanceTransforms` (same shape
|
||||
as `_clipSlotData`); bind at binding 5. On a no-lights frame, fill -1 (shader adds no
|
||||
point light) and still bind a ≥1-element buffer so the SSBO is never unbound.
|
||||
4. **Cell centre/radius:** world-space bounding sphere of the cell geometry — reuse the
|
||||
cell's existing visibility bound (the BSP/AABB sphere already computed for culling).
|
||||
The exact field is pinned during planning by reading the cell-storage structs in
|
||||
`EnvCellRenderer` / `EnvCellLandblock`; fallback = centre from the cell-part transform
|
||||
translation, radius from the cell vertex AABB. **This is the one detail to confirm
|
||||
against code in the plan.**
|
||||
|
||||
Order independence: D-1 and D-2 are orthogonal (shader math vs buffer binding) and can
|
||||
be implemented in either order, but ship together.
|
||||
|
||||
## Testing (TDD)
|
||||
|
||||
`LightBake.cs` already encodes the correct math: `PointContribution` = per-light capped
|
||||
(matches `mesh_modern.vert` pointContribution line-for-line), `ComputeVertexColor` = sum
|
||||
reaching point lights → clamp `[0,1]`, skip directional. The new shader `pointAcc` clamp
|
||||
mirrors `ComputeVertexColor`'s final clamp exactly.
|
||||
|
||||
New conformance test in `tests/AcDream.Core.Tests/` (e.g. `LightBakeConformanceTests`):
|
||||
|
||||
- **Golden warm torch, bounded:** an orange `(1, 0.588, 0.314)` `intensity=100`
|
||||
`falloff=4` (Range = 4×1.3 = 5.2 m) torch lighting a wall vertex (facing it) at
|
||||
d = 1, 2, 3, 4, 5 m → result is warm (R ≥ G ≥ B, hue preserved) and **every channel
|
||||
≤ 1.0** (never white); at d ≥ Range the contribution is 0 (range gate).
|
||||
- **No-blowout under stacking:** 8 overlapping `intensity=100` near-white torches summed
|
||||
via `ComputeVertexColor` → each channel clamps to ≤ 1.0 (the `[0,1]` saturate holds).
|
||||
- **Hue preserved:** a single orange torch's bounded result keeps B < G < R (warm), not
|
||||
desaturated toward white.
|
||||
|
||||
These pin the contract the shader must match. GLSL is not unit-testable in-process
|
||||
(standard for this project per the render digest); the shader `pointContribution` +
|
||||
`pointAcc` clamp are matched to `LightBake` by **line-for-line review** with the C#
|
||||
oracle as the pinned reference (call it out in the implementation commit).
|
||||
|
||||
## Bookkeeping — divergence register
|
||||
|
||||
- **Correct stale row AP-35** (`docs/architecture/retail-divergence-register.md`): it
|
||||
describes the point-light path as per-pixel `mesh_modern.frag:52` with the half-Lambert
|
||||
wrap "NOT ported". Reality since Fix A (`aa94ced`): per-vertex Gouraud in
|
||||
`mesh_modern.vert:163` WITH the wrap ported. Update the row to match; the D-1 clamp
|
||||
makes the accumulator MORE faithful (no new deviation introduced).
|
||||
- **EnvCell shell per-cell 8-light selection** (D-2) inherits Fix B's existing
|
||||
per-object approximation (retail bakes per-VERTEX over the full static list; acdream
|
||||
selects up to 8 per cell-sphere then gates per-vertex in-shader). Confirm Fix B's
|
||||
register row covers EnvCell shells; extend that row if needed — do NOT add a
|
||||
contradicting row.
|
||||
|
||||
## Files
|
||||
|
||||
- `src/AcDream.App/Rendering/Shaders/mesh_modern.vert` — D-1 clamp split.
|
||||
- `src/AcDream.App/Rendering/Shaders/mesh_modern.frag` — verify final clamp stays correct
|
||||
(expected no change).
|
||||
- `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs` — D-2: `LightManager` ref, per-cell
|
||||
light sets, bind SSBO 4 + 5, per-instance light-set buffer.
|
||||
- `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (+ a shared `GlobalLightPacker`) —
|
||||
extract the binding-4 global-lights packing so both renderers share it.
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs` — wire `LightManager` into
|
||||
`EnvCellRenderer.Initialize` (minimal).
|
||||
- `tests/AcDream.Core.Tests/Lighting/LightBakeConformanceTests.cs` — new.
|
||||
- `docs/architecture/retail-divergence-register.md` — AP-35 update.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `dotnet build` green; `dotnet test` green including the new conformance test.
|
||||
- Conformance test passes on the captured golden torch values (warm, bounded, hue-preserved).
|
||||
- Shader `pointContribution` + new `pointAcc` clamp reviewed line-for-line against
|
||||
`LightBake` (cited in the commit).
|
||||
- AP-35 corrected; any D-2 register note reconciled with Fix B's row.
|
||||
- **Visual (user):** outdoor objects near torches no longer blow out warm-white, and the
|
||||
Holtburg meeting-hall walls render warm-but-dim like retail.
|
||||
|
||||
## Out of scope (explicit)
|
||||
|
||||
- **Do NOT port the D3D-FF hardware model** (`config_hardware_light`'s
|
||||
`color×intensity`, `(0,1,0)=1/d`, `Range=falloff×1.5`) — it lights GfxObjs/dynamics,
|
||||
not the baked walls. Wrong oracle (handoff warning stands).
|
||||
- **Do NOT** wire the CPU vertex bake (`LightBake.cs` as the runtime path) — chosen
|
||||
approach is the in-shader clamp split. `LightBake.cs` stays the test oracle.
|
||||
- Sun handling on indoor walls is unchanged (kept in the material-lit term as today);
|
||||
any "should indoor walls receive sun at all" refinement is a separate question.
|
||||
- The purple portal is correct — do not touch it.
|
||||
Loading…
Add table
Add a link
Reference in a new issue