From 57c11358b62d82ca72579b4d9bc5ed1eeab138ec Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 18 Jun 2026 15:08:52 +0200 Subject: [PATCH] =?UTF-8?q?fix(sky):=20A7=20=E2=80=94=20correct=20sun-vect?= =?UTF-8?q?or=20magnitude=20(ambient=20+=20sun=20were=20~32%=20too=20brigh?= =?UTF-8?q?t)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/AcDream.Core/World/SkyState.cs | 71 ++++++++++--------- .../World/SkyDescLoaderTests.cs | 17 ++--- .../AcDream.Core.Tests/World/SkyStateTests.cs | 41 ++++++----- 3 files changed, 73 insertions(+), 56 deletions(-) diff --git a/src/AcDream.Core/World/SkyState.cs b/src/AcDream.Core/World/SkyState.cs index 5acf2d39..0120e84a 100644 --- a/src/AcDream.Core/World/SkyState.cs +++ b/src/AcDream.Core/World/SkyState.cs @@ -74,22 +74,15 @@ public readonly record struct SkyKeyframe( /// (see ). /// /// - /// Why |sunVec| instead of DirBright directly: retail's - /// PrimD3DRender::UpdateLightsInternal at 0x0059b57c - /// (decomp line 424118-424119) computes - /// D3DLIGHT9.Diffuse.r = sunlight_color.r × sqrt(x²+y²+z²) - /// from the sun vector SkyDesc::GetLighting built at - /// 0x00500ac9 (decomp lines 261343-261353): - /// - /// sunVec.x = sin(H) × DirBright × cos(P) - /// sunVec.y = cos(P) // NOT scaled by DirBright - /// sunVec.z = DirBright × sin(P) - /// - /// Because Y is unscaled by DirBright, |sunVec| ≠ - /// DirBright in general — it varies with sun pitch and heading. - /// Using DirBright alone underweighted the warm directional - /// term, letting the cool ambient/fog dominate ⇒ acdream rendered - /// blue-white at keyframes where retail looked warm-gray. + /// |sunVec| is retail's D3DLIGHT9.Diffuse = DirColor × sqrt(x²+y²+z²) + /// scaling (PrimD3DRender::UpdateLightsInternal 0x0059b57c, decomp + /// 424118-424119) of the WORLD-space sun vector (LScape::sunlight). + /// Because is now the + /// DirBright-scaled spherical vector (magnitude == DirBright, cdb-verified — + /// see that method), |sunVec| == DirBright, so this is effectively + /// SunColor = DirColor × DirBright. (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]].) /// /// public Vector3 SunColor => DirColor * SkyStateProvider.RetailSunVector(this).Length(); @@ -301,21 +294,35 @@ public sealed class SkyStateProvider } /// - /// Retail's raw sun vector (NOT normalized) — the same vector - /// SkyDesc::GetLighting writes at 0x00500ac9 - /// (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 + /// DirBright: /// - /// 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) /// - /// Y is unscaled by brightness on purpose — that's what makes - /// |sunVec|DirBright 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 - /// uSunDir uniform uses the NORMALIZED vector for N·L; the - /// magnitude feeds intensity and - /// the ambient brightness boost in . + /// so |sunVec| == DirBright exactly (cos²P·(sin²H+cos²H)+sin²P = 1). + /// + /// + /// GROUNDED IN A LIVE cdb CAPTURE (2026-06-18, [[reference-retail-ambient-values]]): + /// retail's LScape::sunlight read at a dawn keyframe (H=90°, P=0.9°, + /// DirBright≈0.224) = (0.2238, ~0, 0.00352) — y≈0, magnitude 0.224 = + /// DirBright. That fed level = 0.2·|sunlight| + ambient_level = 0.2·0.224 + + /// 0.40 = 0.445, matching the captured SetWorldAmbientLight level. + /// + /// + /// PRIOR BUG: an earlier version returned y = cos(P) (≈1) — the raw + /// PRE-transform value the decomp's SkyDesc::GetLighting writes to its + /// arg5 (0x00500ac9, before LScape::set_sky_position's world + /// transform). Porting that un-transformed vector inflated |sunVec| to + /// ~1.06 instead of ~0.22, over-brightening BOTH the ambient boost + /// () AND the sun colour + /// () by ~30% vs retail. The world-space + /// form above is what LScape::sunlight actually holds at runtime. + /// + /// The shader uses the NORMALIZED vector for N·L; the magnitude (= DirBright) + /// feeds the sun-colour intensity and the ambient brightness boost. /// 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) } /// diff --git a/tests/AcDream.Core.Tests/World/SkyDescLoaderTests.cs b/tests/AcDream.Core.Tests/World/SkyDescLoaderTests.cs index d07d0a64..4ceeddba 100644 --- a/tests/AcDream.Core.Tests/World/SkyDescLoaderTests.cs +++ b/tests/AcDream.Core.Tests/World/SkyDescLoaderTests.cs @@ -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] diff --git a/tests/AcDream.Core.Tests/World/SkyStateTests.cs b/tests/AcDream.Core.Tests/World/SkyStateTests.cs index 1c677204..3d87da00 100644 --- a/tests/AcDream.Core.Tests/World/SkyStateTests.cs +++ b/tests/AcDream.Core.Tests/World/SkyStateTests.cs @@ -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]