merge: A7 lighting Fix C (sun-vector brightness) + handoff into main
Brings Fix C (57c1135, sun-vector magnitude / ~32% over-bright) + the A7 lighting handoff doc onto main. Auto-merged clean against the D.2b line. Merged tree builds green; 18/18 sky tests pass. Fix A/B already on main (37911ed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
4795a6c7f3
4 changed files with 187 additions and 56 deletions
|
|
@ -0,0 +1,114 @@
|
|||
# A7 Lighting — Fix A/B/C SHIPPED, Fix D (object torch over-brightness) HANDOFF
|
||||
|
||||
**Date:** 2026-06-18 **Branch:** claude/thirsty-goldberg-51bb9b (merged to main)
|
||||
**Companion memory:** `claude-memory/reference_retail_ambient_values.md` (all captured
|
||||
values + cdb recipes) and `reference_retail_chat_colors.md` (cdb method).
|
||||
|
||||
This session made acdream's outdoor + ambient lighting retail-faithful by grounding
|
||||
everything in **live cdb on the retail client** (no guessing). Three fixes shipped;
|
||||
a fourth (Fix D — outdoor objects too bright near torches) is fully grounded but
|
||||
**deliberately NOT implemented** because the math contradicts the observed result —
|
||||
one more capture is needed first.
|
||||
|
||||
## SHIPPED this session (all on `main`)
|
||||
|
||||
| Fix | Commit | What | Result |
|
||||
|---|---|---|---|
|
||||
| **A** | `aa94ced` | point-light SHAPE: per-vertex Gouraud + faithful `calc_point_light` (wrap + norm), per-channel cap | killed the "spotlight" disc — user "way better" |
|
||||
| **B** | `4345e77` | per-OBJECT light selection (`minimize_object_lighting`): each object picks its own ≤8 lights by its AABB sphere, camera-independent | killed "building lights up as you approach"; a Holtburg view has **129** point lights vs the old global cap of 8 |
|
||||
| **C** | `57c1135` | sun-vector magnitude: ambient + sun were **~32% too bright** | ambient now matches retail within ~2%; user "general ambient better outside" |
|
||||
|
||||
**Fix B mechanism** (for context): two new SSBOs in `mesh_modern.vert` — binding=4
|
||||
GLOBAL light array (`LightManager.PointSnapshot`), binding=5 per-instance 8-int
|
||||
light set (mirrors the U.3 clip-slot SSBO). `LightManager.SelectForObject` +
|
||||
`BuildPointLightSnapshot` (pure, TDD). `WbDrawDispatcher` computes each entity's
|
||||
light set once per entity (like `_currentEntitySlot`), threads it parallel to the
|
||||
matrices.
|
||||
|
||||
**Fix C mechanism:** `SkyStateProvider.RetailSunVector` had `y = cos(P)` (≈1) — the
|
||||
PRE-transform value `SkyDesc::GetLighting` writes to its arg5 (0x00500ac9), before
|
||||
`LScape::set_sky_position`'s world transform. cdb read retail's actual
|
||||
`LScape::sunlight = (0.2238, ~0, 0.00352)`, magnitude = DirBright. Corrected to the
|
||||
world-space spherical form `DirBright × (cos P·sin H, cos P·cos H, sin P)`,
|
||||
`|sunVec| == DirBright`. Feeds BOTH the ambient boost AND the sun colour, so it
|
||||
dims **terrain + objects + sky** (all read the shared SceneLighting UBO). 18/18 sky
|
||||
tests green (old tests pinned the inflated magnitude — updated to cdb-verified).
|
||||
|
||||
## KEY LESSON: the "too purple" was NEVER a bug
|
||||
|
||||
The user's side-by-side ("acdream too purple, retail neutral") was a comparison
|
||||
**across different times of day**. Live cdb at the SAME game time + DayGroup proved
|
||||
acdream's time, weather (DayGroup selection), AND ambient COLOR all match retail
|
||||
exactly — the purple `AmbColor=(200,100,255)` is authored per-time-of-day in the
|
||||
sky dat (twilight = purple, midday = neutral `(230,230,255)`). Only the *brightness*
|
||||
was wrong (Fix C). Don't re-investigate the purple.
|
||||
|
||||
---
|
||||
|
||||
## OPEN — Fix D: outdoor OBJECTS too bright near torches
|
||||
|
||||
**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**.
|
||||
|
||||
### 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)
|
||||
|
||||
### 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.**
|
||||
|
||||
### 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.
|
||||
|
||||
### 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).
|
||||
|
||||
---
|
||||
|
||||
## cdb cheat-sheet (all verified this session; binary MATCHES refs/acclient.pdb)
|
||||
- `bp acclient!SmartBox::SetWorldAmbientLight` (0x004530a0) — arg2=level `[esp+4]`, arg3=color32 `[esp+8]`
|
||||
- `bp acclient!SkyDesc::GetLighting` (0x00500a80) — arg2=dayFraction `[esp+4]`; `dt acclient!SkyDesc @ecx present_day_group`
|
||||
- `LScape::sunlight` global @ **0x00841940** (Vector3); `LScape::ambient_level` @ 0x00841770
|
||||
- `bp acclient!PrimD3DRender::config_hardware_light` (0x0059ad30) — arg4=LIGHTINFO `[esp+0x10]`; `dt acclient!LIGHTINFO dwo(@esp+0x10) type intensity falloff color`
|
||||
- `rangeAdjust = 1.5` @ 0x00820cc4; `D3DPolyRender::SetStaticLightingVertexColors` @ 0x0059cfe0
|
||||
- Pattern: `.formats poi(<addr>)` for floats, `dwo(<addr>)` for dwords, `qd` after N hits to auto-detach (keeps retail alive). User must have retail in-world first.
|
||||
- acdream probes: `ACDREAM_PROBE_LIGHT=1` (`[light]` ambient+sun line), `ACDREAM_DUMP_SKY=1` (keyframes + dayFraction + DayGroup).
|
||||
|
||||
## Build / run
|
||||
`dotnet build src/AcDream.App/AcDream.App.csproj -c Debug` (green). Standard
|
||||
`ACDREAM_LIVE` launch env in CLAUDE.md. Close the client before rebuilding (it locks
|
||||
the DLLs). 18/18 sky tests + 17/17 LightManager + 36/36 dispatcher clip-slot green.
|
||||
|
|
@ -74,22 +74,15 @@ public readonly record struct SkyKeyframe(
|
|||
/// (see <see cref="SkyStateProvider.RetailSunVector"/>).
|
||||
///
|
||||
/// <para>
|
||||
/// Why <c>|sunVec|</c> instead of <c>DirBright</c> directly: retail's
|
||||
/// <c>PrimD3DRender::UpdateLightsInternal</c> at <c>0x0059b57c</c>
|
||||
/// (decomp line 424118-424119) computes
|
||||
/// <code>D3DLIGHT9.Diffuse.r = sunlight_color.r × sqrt(x²+y²+z²)</code>
|
||||
/// from the sun vector <c>SkyDesc::GetLighting</c> built at
|
||||
/// <c>0x00500ac9</c> (decomp lines 261343-261353):
|
||||
/// <code>
|
||||
/// sunVec.x = sin(H) × DirBright × cos(P)
|
||||
/// sunVec.y = cos(P) // NOT scaled by DirBright
|
||||
/// sunVec.z = DirBright × sin(P)
|
||||
/// </code>
|
||||
/// Because Y is unscaled by <c>DirBright</c>, <c>|sunVec|</c> ≠
|
||||
/// <c>DirBright</c> in general — it varies with sun pitch and heading.
|
||||
/// Using <c>DirBright</c> alone underweighted the warm directional
|
||||
/// term, letting the cool ambient/fog dominate ⇒ acdream rendered
|
||||
/// blue-white at keyframes where retail looked warm-gray.
|
||||
/// <c>|sunVec|</c> is retail's <c>D3DLIGHT9.Diffuse = DirColor × sqrt(x²+y²+z²)</c>
|
||||
/// scaling (<c>PrimD3DRender::UpdateLightsInternal</c> 0x0059b57c, decomp
|
||||
/// 424118-424119) of the WORLD-space sun vector (<c>LScape::sunlight</c>).
|
||||
/// Because <see cref="SkyStateProvider.RetailSunVector"/> is now the
|
||||
/// DirBright-scaled spherical vector (magnitude == DirBright, cdb-verified —
|
||||
/// see that method), <c>|sunVec| == DirBright</c>, so this is effectively
|
||||
/// <c>SunColor = DirColor × DirBright</c>. (A prior bug used the un-transformed
|
||||
/// y=cos(P) vector ⇒ |sunVec|≈1.06 ⇒ the sun was ~4–5× too bright at dawn/dusk;
|
||||
/// [[reference-retail-ambient-values]].)
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public Vector3 SunColor => DirColor * SkyStateProvider.RetailSunVector(this).Length();
|
||||
|
|
@ -301,21 +294,35 @@ public sealed class SkyStateProvider
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail's raw sun vector (NOT normalized) — the same vector
|
||||
/// <c>SkyDesc::GetLighting</c> writes at <c>0x00500ac9</c>
|
||||
/// (decomp lines 261343, 261352, 261353):
|
||||
/// Retail's world-space sun vector (NOT normalized): the standard
|
||||
/// spherical-to-cartesian direction (East=x, North=y, Up=z) scaled by
|
||||
/// <c>DirBright</c>:
|
||||
/// <code>
|
||||
/// sunVec.x = sin(H_rad) × DirBright × cos(P_rad)
|
||||
/// sunVec.y = cos(P_rad) // NOT scaled by DirBright
|
||||
/// sunVec.z = DirBright × sin(P_rad)
|
||||
/// sunVec.x = DirBright × cos(P) × sin(H)
|
||||
/// sunVec.y = DirBright × cos(P) × cos(H)
|
||||
/// sunVec.z = DirBright × sin(P)
|
||||
/// </code>
|
||||
/// Y is unscaled by brightness on purpose — that's what makes
|
||||
/// <c>|sunVec|</c> ≠ <c>DirBright</c> in general (the magnitude varies
|
||||
/// with pitch/heading, which is the basis for retail's "sun is brighter
|
||||
/// in some configurations than others" lighting behavior). The shader's
|
||||
/// <c>uSunDir</c> uniform uses the NORMALIZED vector for N·L; the
|
||||
/// magnitude feeds <see cref="SkyKeyframe.SunColor"/> intensity and
|
||||
/// the ambient brightness boost in <see cref="SkyKeyframe.AmbientColor"/>.
|
||||
/// so <c>|sunVec| == DirBright</c> exactly (cos²P·(sin²H+cos²H)+sin²P = 1).
|
||||
///
|
||||
/// <para>
|
||||
/// GROUNDED IN A LIVE cdb CAPTURE (2026-06-18, [[reference-retail-ambient-values]]):
|
||||
/// retail's <c>LScape::sunlight</c> read at a dawn keyframe (H=90°, P=0.9°,
|
||||
/// DirBright≈0.224) = <c>(0.2238, ~0, 0.00352)</c> — y≈0, magnitude 0.224 =
|
||||
/// DirBright. That fed <c>level = 0.2·|sunlight| + ambient_level = 0.2·0.224 +
|
||||
/// 0.40 = 0.445</c>, matching the captured <c>SetWorldAmbientLight</c> level.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// PRIOR BUG: an earlier version returned <c>y = cos(P)</c> (≈1) — the raw
|
||||
/// PRE-transform value the decomp's <c>SkyDesc::GetLighting</c> writes to its
|
||||
/// <c>arg5</c> (0x00500ac9, before <c>LScape::set_sky_position</c>'s world
|
||||
/// transform). Porting that un-transformed vector inflated <c>|sunVec|</c> to
|
||||
/// ~1.06 instead of ~0.22, over-brightening BOTH the ambient boost
|
||||
/// (<see cref="SkyKeyframe.AmbientColor"/>) AND the sun colour
|
||||
/// (<see cref="SkyKeyframe.SunColor"/>) by ~30% vs retail. The world-space
|
||||
/// form above is what <c>LScape::sunlight</c> actually holds at runtime.
|
||||
/// </para>
|
||||
/// The shader uses the NORMALIZED vector for N·L; the magnitude (= DirBright)
|
||||
/// feeds the sun-colour intensity and the ambient brightness boost.
|
||||
/// </summary>
|
||||
public static Vector3 RetailSunVector(SkyKeyframe kf)
|
||||
{
|
||||
|
|
@ -325,9 +332,9 @@ public sealed class SkyStateProvider
|
|||
float sinP = MathF.Sin(p);
|
||||
float B = kf.DirBright;
|
||||
return new Vector3(
|
||||
MathF.Sin(h) * B * cosP, // x = sin(H) × B × cos(P)
|
||||
cosP, // y = cos(P) ← unscaled by B
|
||||
B * sinP); // z = B × sin(P)
|
||||
B * cosP * MathF.Sin(h), // x = DirBright × cos(P) × sin(H)
|
||||
B * cosP * MathF.Cos(h), // y = DirBright × cos(P) × cos(H)
|
||||
B * sinP); // z = DirBright × sin(P)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -100,22 +100,23 @@ public sealed class SkyDescLoaderTests
|
|||
{
|
||||
// The loader stores DirColor and DirBright RAW. The SunColor property
|
||||
// composes them via |sunVec| per retail's UpdateLightsInternal at
|
||||
// 0x59b57c (decomp 424118) — the diffuse magnitude is sqrt(x²+y²+z²)
|
||||
// where the sun vector is built from heading/pitch/brightness with
|
||||
// Y unscaled by brightness (decomp 261352).
|
||||
// 0x59b57c (decomp 424118) — diffuse = DirColor × |LScape::sunlight|.
|
||||
// cdb-verified (reference-retail-ambient-values): |LScape::sunlight| ==
|
||||
// DirBright for every keyframe (world-space spherical vector, magnitude
|
||||
// DirBright·sqrt(cos²P+sin²P) = DirBright).
|
||||
//
|
||||
// For this region: H=180°, P=70°, B=1.5
|
||||
// sunVec = (sin(180)*1.5*cos(70), cos(70), 1.5*sin(70))
|
||||
// = (0, 0.342, 1.410)
|
||||
// |sunVec| = sqrt(0 + 0.117 + 1.988) = 1.4509
|
||||
// sunVec = 1.5 × (cos(70)·sin(180), cos(70)·cos(180), sin(70))
|
||||
// = (0, -0.513, 1.410)
|
||||
// |sunVec| = sqrt(0 + 0.263 + 1.988) = 1.500 (= DirBright)
|
||||
// DirColor.X = 200/255 = 0.7843
|
||||
// SunColor.X = 0.7843 × 1.4509 = 1.138
|
||||
// SunColor.X = 0.7843 × 1.500 = 1.1765
|
||||
var region = MakeRegion(dirBright: 1.5f, rBgrOrder: 200);
|
||||
var loaded = SkyDescLoader.LoadFromRegion(region);
|
||||
Assert.NotNull(loaded);
|
||||
|
||||
var kf = loaded!.DayGroups[0].SkyTimes[0].Keyframe;
|
||||
Assert.InRange(kf.SunColor.X, 1.13f, 1.15f);
|
||||
Assert.InRange(kf.SunColor.X, 1.17f, 1.18f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -66,24 +66,33 @@ public sealed class SkyStateTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void RetailSunVector_AtHorizonNorth_MagnitudeIsOne()
|
||||
public void RetailSunVector_MagnitudeAlwaysEqualsDirBright()
|
||||
{
|
||||
// Sun on horizon to the north (H=0°, P=0°): cos(P)=1, sin(P)=0.
|
||||
// sunVec = (sin(0)×B×1, 1, B×0) = (0, 1, 0)
|
||||
// |sunVec| = 1 regardless of B (because Y is unscaled by B)
|
||||
var kf = new SkyKeyframe(
|
||||
Begin: 0f,
|
||||
SunHeadingDeg: 0f,
|
||||
SunPitchDeg: 0f,
|
||||
DirColor: Vector3.One,
|
||||
DirBright: 2.0f, // anything
|
||||
AmbColor: Vector3.One,
|
||||
AmbBright: 1f,
|
||||
FogColor: Vector3.One,
|
||||
FogDensity: 0f);
|
||||
// cdb-verified (2026-06-18, reference-retail-ambient-values): retail's
|
||||
// world-space LScape::sunlight = DirBright × (cosP·sinH, cosP·cosH, sinP),
|
||||
// whose magnitude is DirBright·sqrt(cos²P·(sin²H+cos²H)+sin²P) = DirBright
|
||||
// for ALL headings/pitches. (The prior y=cos(P) port gave |sunVec|≈1 at the
|
||||
// horizon — that was the ~30% over-bright bug.)
|
||||
// Horizon north (H=0°, P=0°): (0, B, 0), |.| = B.
|
||||
var horizon = new SkyKeyframe(
|
||||
Begin: 0f, SunHeadingDeg: 0f, SunPitchDeg: 0f,
|
||||
DirColor: Vector3.One, DirBright: 2.0f,
|
||||
AmbColor: Vector3.One, AmbBright: 1f,
|
||||
FogColor: Vector3.One, FogDensity: 0f);
|
||||
Assert.InRange(SkyStateProvider.RetailSunVector(horizon).Length(), 1.99f, 2.01f);
|
||||
|
||||
var v = SkyStateProvider.RetailSunVector(kf);
|
||||
Assert.InRange(v.Length(), 0.99f, 1.01f);
|
||||
// Reproduce the live cdb capture: dawn keyframe H=90°, P=0.9°, DirBright=0.224
|
||||
// → LScape::sunlight = (0.2238, ~0, 0.00352), magnitude 0.224 = DirBright.
|
||||
var dawn = new SkyKeyframe(
|
||||
Begin: 0f, SunHeadingDeg: 90f, SunPitchDeg: 0.9f,
|
||||
DirColor: Vector3.One, DirBright: 0.224f,
|
||||
AmbColor: Vector3.One, AmbBright: 0.40f,
|
||||
FogColor: Vector3.One, FogDensity: 0f);
|
||||
var v = SkyStateProvider.RetailSunVector(dawn);
|
||||
Assert.InRange(v.X, 0.223f, 0.225f); // DirBright·cosP·sin(90°) ≈ 0.224
|
||||
Assert.InRange(v.Y, -0.001f, 0.001f); // DirBright·cosP·cos(90°) ≈ 0 (was the bug: ≈1)
|
||||
Assert.InRange(v.Z, 0.003f, 0.004f); // DirBright·sin(0.9°) ≈ 0.0035
|
||||
Assert.InRange(v.Length(), 0.223f, 0.225f); // = DirBright
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue