fix(sky): A7 — correct sun-vector magnitude (ambient + sun were ~32% too bright)

Outdoor lighting was ~32% too bright (washed-out, weak shading). Live cdb on
retail (SmartBox::SetWorldAmbientLight + SkyDesc::GetLighting + LScape::sunlight,
binary matches refs/acclient.pdb) pinned it: at the SAME game time + DayGroup,
acdream's ambient COLOR matched retail exactly (the purple is correct, authored
per-time-of-day in the sky dat) but the LEVEL was 0.607 vs retail's 0.459.

level = AmbBright + 0.2·|sunVec|, both AmbBright=0.40, so acdream's |sunVec|≈1.06
vs retail's ≈0.30. Retail's LScape::sunlight read live = (0.2238, ~0, 0.00352),
magnitude 0.224 = DirBright, y≈0.

RetailSunVector had `y = cos(P)` (≈1) — the raw PRE-transform value SkyDesc::
GetLighting writes to arg5 (0x00500ac9), before LScape::set_sky_position's
world transform. acdream ported the un-transformed vector, so the y=cos(P)≈1
term inflated |sunVec| to ~1.06. That magnitude feeds BOTH the ambient boost
(SkyKeyframe.AmbientColor) AND the sun colour (SkyKeyframe.SunColor =
DirColor×|sunVec|), over-brightening the whole scene (terrain, objects, sky)
~30% and also pointing the sun the wrong way.

Fix: RetailSunVector = DirBright × (cos(P)·sin(H), cos(P)·cos(H), sin(P)) — the
world-space spherical form LScape::sunlight actually holds; |sunVec| == DirBright
for all H/P. After: acdream ambient (0.353,0.176,0.449) vs retail (0.360,0.180,
0.459) — within ~2%, user-confirmed "better outside". Sun direction also corrected
(was pointing ~North from the bad y term).

Tests updated to the cdb-verified values (the prior tests pinned the inflated
magnitude). 18/18 sky tests green. reference-retail-ambient-values memory updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-18 15:08:52 +02:00
parent 4345e77d62
commit 57c11358b6
3 changed files with 73 additions and 56 deletions

View file

@ -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]