acdream/tests/AcDream.Core.Tests/World/SkyDescLoaderTests.cs
Erik 05a8a7209f fix(sky): retail-faithful sun-vector magnitude for SunColor / AmbientColor
Two independent investigations (in-house decomp re-check + two
external agent reports) converged on the same root cause for the
"too blue-white sky" symptom:

acdream computed SunColor = DirColor × DirBright and AmbientColor =
AmbColor × AmbBright. Retail computes them from the magnitude of a
specially-shaped sun vector instead. Per the named retail decomp:

  SkyDesc::GetLighting at 0x00500ac9 (decomp 261343-261353):
    sunVec.x = sin(H_rad) × DirBright × cos(P_rad)
    sunVec.y = cos(P_rad)                    ← NOT scaled by DirBright
    sunVec.z = DirBright × sin(P_rad)

  PrimD3DRender::UpdateLightsInternal at 0x0059b57c (decomp 424118):
    D3DLIGHT9.Diffuse.r = sunlight_color.r × sqrt(x²+y²+z²)

  SmartBox::SetWorldAmbientLight callsite at 0x0050560b (decomp 267117):
    SetWorldAmbientLight(sqrt(|sunVec|²) × 0.2 + ambient_level, ...)

Y stays unscaled by DirBright on purpose, so |sunVec| ≠ DirBright in
general — the magnitude varies with sun pitch/heading. That's what
gives retail's "sun feels stronger when it's overhead, ambient warms
up at midday" behavior we were missing.

Added SkyStateProvider.RetailSunVector(kf) that builds the vector
verbatim. SkyKeyframe.SunColor / AmbientColor now compose via |sunVec|.
SunDirectionFromKeyframe normalizes the same vector (replaces our
geometrically-clean spherical convention which didn't match retail's
deliberate Y-decoupled-from-heading shape).

Tests:
- Replaced the linear-interp assumption in
  Interpolate_BetweenKeyframes_LerpsColors with a test on the RAW
  inputs (DirColor, AmbBright, etc.) — those still lerp linearly;
  the composite SunColor doesn't, intentionally.
- Added 4 golden-value tests for the new formulas
  (RetailSunVector_AtZenith, _AtHorizonNorth,
  SunColor_UsesRetailMagnitudeNotDirBrightDirectly,
  AmbientColor_BoostsByTwentyPercentOfSunVectorLength).
- Updated stale LoadFromRegion_SunColor_IsPrepreMultipliedByBrightness
  test to LoadFromRegion_SunColor_UsesRetailSunVectorMagnitude
  with the new expected magnitude.

User visually verified — acdream's sky shifted from blue-white toward
the warm tint retail shows at the same keyframe.

1227 tests pass.
2026-04-27 22:42:53 +02:00

147 lines
4.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
using Xunit;
namespace AcDream.Core.Tests.World;
public sealed class SkyDescLoaderTests
{
/// <summary>
/// Hand-build a Region with a minimal sky descriptor to feed the
/// loader without needing real dat bytes. The LoadFromRegion
/// separator exists precisely for this — keeps the parsing logic
/// testable independent of DatCollection.
/// </summary>
private static Region MakeRegion(float dirBright, byte rBgrOrder)
{
var region = new Region();
region.PartsMask = PartsMask.HasSkyInfo;
var sky = new SkyDesc
{
TickSize = 1.0,
LightTickSize = 2.0,
};
var dg = new DayGroup
{
ChanceOfOccur = 1.0f,
};
var time = new SkyTimeOfDay
{
Begin = 0.5f,
DirBright = dirBright,
DirHeading = 180f,
DirPitch = 70f,
DirColor = new ColorARGB { Blue = 0, Green = 0, Red = rBgrOrder, Alpha = 255 },
AmbBright = 0.4f,
AmbColor = new ColorARGB { Blue = 100, Green = 100, Red = 100, Alpha = 255 },
MinWorldFog = 120f,
MaxWorldFog = 400f,
WorldFogColor = new ColorARGB { Blue = 50, Green = 50, Red = 50, Alpha = 255 },
WorldFog = 1, // Linear
};
dg.SkyTime.Add(time);
sky.DayGroups.Add(dg);
region.SkyInfo = sky;
return region;
}
[Fact]
public void LoadFromRegion_ConvertsFogFields()
{
var region = MakeRegion(dirBright: 1.5f, rBgrOrder: 200);
var loaded = SkyDescLoader.LoadFromRegion(region);
Assert.NotNull(loaded);
Assert.Equal(1.0, loaded!.TickSize);
Assert.Single(loaded.DayGroups);
var grp = loaded.DayGroups[0];
Assert.Single(grp.SkyTimes);
var kf = grp.SkyTimes[0].Keyframe;
Assert.Equal(120f, kf.FogStart);
Assert.Equal(400f, kf.FogEnd);
Assert.Equal(FogMode.Linear, kf.FogMode);
}
[Fact]
public void LoadFromRegion_SunColor_UsesRetailSunVectorMagnitude()
{
// 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).
//
// 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
// DirColor.X = 200/255 = 0.7843
// SunColor.X = 0.7843 × 1.4509 = 1.138
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);
}
[Fact]
public void LoadFromRegion_NoSkyInfo_ReturnsNull()
{
var region = new Region { PartsMask = 0 };
Assert.Null(SkyDescLoader.LoadFromRegion(region));
}
[Fact]
public void BuildDefaultProvider_FromDatKeyframes_SupportsInterpolation()
{
var region = MakeRegion(dirBright: 1.0f, rBgrOrder: 255);
var loaded = SkyDescLoader.LoadFromRegion(region)!;
var provider = loaded.BuildDefaultProvider();
// Exactly one keyframe: interpolation at any t returns it.
var s = provider.Interpolate(0.1f);
Assert.InRange(s.SunColor.X, 0.99f, 1.01f);
}
[Fact]
public void SkyObjectData_IsVisible_HandlesWrap()
{
var obj = new SkyObjectData
{
BeginTime = 0.9f, // wraps across midnight
EndTime = 0.1f,
};
Assert.True(obj.IsVisible(0.95f)); // near end of day
Assert.True(obj.IsVisible(0.05f)); // just after midnight
Assert.False(obj.IsVisible(0.5f)); // mid-day (not visible)
}
[Fact]
public void SkyObjectData_CurrentAngle_LerpsAcrossWindow()
{
var obj = new SkyObjectData
{
BeginTime = 0.25f,
EndTime = 0.75f,
BeginAngle = 0f,
EndAngle = 180f,
};
// Middle of the window → 90°.
Assert.Equal(90f, obj.CurrentAngle(0.5f), precision: 2);
// At begin → begin angle.
Assert.Equal(0f, obj.CurrentAngle(0.25f), precision: 2);
}
}