Merge branch 'feature/sky-fixes' — sky/weather rendering retail-faithful pass
Six commits on the branch, three retail-decomp investigations (in-house + two external code-review agents) converging on the same root causes:97fc1b5fix(sky): translucency-as-opacity + sky fog floor + additive fog-skip05a8a72fix(sky): retail-faithful sun-vector magnitude for SunColor / AmbientColor034a684fix(sky): partition sky pass on Properties bit 0x01, not bit 0x04375065bfix(meshing): Translucent flag overrides Additive blend per retail SetSurface646cccafeat(sky): load Setup-backed (0x020xxx) sky objects via SetupMesh.Flatten0c82d2cdocs(issues): #28 root-caused (PES particles), #29 filed Net effect: * Sun + ambient colors now use retail's |sunVec| magnitude formula from PrimD3DRender::UpdateLightsInternal at decomp 424118 — fixes blue-white sky tint at most keyframes. * Surface.Translucency is used DIRECTLY as opacity (not 1-x) per D3DPolyRender::SetSurface at decomp 425255 — fixes 3× too-bright cloud + correct rain alpha. * Sky fog re-enabled with SKY_FOG_FLOOR=0.2 mitigation — horizon haze visible without flat-fogging the dome at storm keyframes. * Additive surfaces skip fog per SetFFFogAlphaDisabled at decomp 425295 — sun stays bright at horizon dusk/dawn. * Pre/post-scene partition is bit 0x01 (post-scene placement) instead of bit 0x04 (weather gate), per GameSky::CreateDeletePhysicsObjects at decomp 269036. Fixes double-rendered foreground rain. * Translucent flag forces alpha-blend over Additive when ClipMap is set, matching retail's blend resolution at decomp 425246-425260. Cloud surface 0x08000023 now classified correctly. * Setup-backed sky objects (0x020xxxxx) now load via SetupMesh.Flatten instead of being silently dropped by EnsureMeshUploaded. Tests: 1227 pass. User-visible improvements: foreground rain matches retail's volumetric look, sky tint shifted from blue-white toward retail's warm-gray, additive sun stays bright through horizon haze. Outstanding: * Issue #28 — PES particle rendering ("aurora light play"). Now root-caused with implementation outline; defer to its own Phase. * Issue #29 — residual cloud-density gap; likely rolls into #28. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> # Conflicts: # src/AcDream.App/Rendering/GameWindow.cs
This commit is contained in:
commit
f7c9e88b6a
18 changed files with 1439 additions and 290 deletions
|
|
@ -75,26 +75,56 @@ public sealed class DerethDateTimeTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ToCalendar_PY0Day1_Snowreap()
|
||||
public void ToCalendar_PY10Day1_Morningthaw()
|
||||
{
|
||||
// Tick 0 maps to PY 10 (= relative year 0 + ZeroYear=10),
|
||||
// Morningthaw 1 — matches retail's calendar epoch
|
||||
// (ACE DerethDateTime.cs: dayZeroTicks = 0; // Morningthaw 1, 10 P.Y.).
|
||||
var cal = DerethDateTime.ToCalendar(0);
|
||||
Assert.Equal(0, cal.Year);
|
||||
Assert.Equal(DerethDateTime.MonthName.Snowreap, cal.Month);
|
||||
Assert.Equal(DerethDateTime.ZeroYear, cal.Year);
|
||||
Assert.Equal(DerethDateTime.MonthName.Morningthaw, cal.Month);
|
||||
Assert.Equal(1, cal.Day);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToCalendar_AdvancesCorrectly()
|
||||
{
|
||||
// One year from start → PY 1, Snowreap 1.
|
||||
// One year from start → PY (10 + 1) = 11, Morningthaw 1.
|
||||
var cal = DerethDateTime.ToCalendar(DerethDateTime.YearTicks);
|
||||
Assert.Equal(1, cal.Year);
|
||||
Assert.Equal(DerethDateTime.MonthName.Snowreap, cal.Month);
|
||||
Assert.Equal(DerethDateTime.ZeroYear + 1, cal.Year);
|
||||
Assert.Equal(DerethDateTime.MonthName.Morningthaw, cal.Month);
|
||||
Assert.Equal(1, cal.Day);
|
||||
|
||||
// One month into year 1.
|
||||
// One month into year 11 → Solclaim (next month after Morningthaw).
|
||||
var cal2 = DerethDateTime.ToCalendar(DerethDateTime.YearTicks + DerethDateTime.MonthTicks);
|
||||
Assert.Equal(1, cal2.Year);
|
||||
Assert.Equal(DerethDateTime.MonthName.ColdMeet, cal2.Month);
|
||||
Assert.Equal(DerethDateTime.ZeroYear + 1, cal2.Year);
|
||||
Assert.Equal(DerethDateTime.MonthName.Solclaim, cal2.Month);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToCalendar_TickAtSeedsow24Year106_MatchesRetailFormat()
|
||||
{
|
||||
// Regression guard for the 2026-04-27 dual-client comparison.
|
||||
// Retail @timestamp output format is
|
||||
// "Date: <Month> <Day>, <Year> P.Y."
|
||||
// Pick a tick at the exact start of Seedsow 24 in relative year 106:
|
||||
// shifted = 106 * YearTicks + 2 * MonthTicks + 23 * DayTicks
|
||||
// Derived: 290,779,200 + 457,200 + 175,260 = 291,411,660. Subtract
|
||||
// OriginOffsetTicks (3600 in Dereth dat) to get the input tick:
|
||||
// 291,411,660 - 3600 = 291,408,060
|
||||
// Expected output: PY 116 (= ZeroYear 10 + relative 106), Seedsow,
|
||||
// day 24 1-indexed.
|
||||
DerethDateTime.SetOriginOffsetFromDat(3600.0);
|
||||
try
|
||||
{
|
||||
var cal = DerethDateTime.ToCalendar(291_408_060.0);
|
||||
Assert.Equal(DerethDateTime.ZeroYear + 106, cal.Year);
|
||||
Assert.Equal(DerethDateTime.MonthName.Seedsow, cal.Month);
|
||||
Assert.Equal(24, cal.Day);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DerethDateTime.SetOriginOffsetFromDat(DerethDateTime.DayFractionOriginOffsetTicks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,15 +73,26 @@ public sealed class SkyDescLoaderTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromRegion_SunColor_IsPrepreMultipliedByBrightness()
|
||||
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;
|
||||
// R was 200/255 ≈ 0.784, times dirBright 1.5 = 1.176
|
||||
Assert.InRange(kf.SunColor.X, 1.17f, 1.19f);
|
||||
Assert.InRange(kf.SunColor.X, 1.13f, 1.15f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -25,17 +25,105 @@ public sealed class SkyStateTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Interpolate_BetweenKeyframes_LerpsColors()
|
||||
public void Interpolate_BetweenKeyframes_LerpsRawInputs()
|
||||
{
|
||||
var sky = SkyStateProvider.Default();
|
||||
var dawn = sky.Interpolate(0.25f);
|
||||
var noon = sky.Interpolate(0.5f);
|
||||
var midPt = sky.Interpolate(0.375f);
|
||||
|
||||
// Midpoint should fall between dawn & noon for sun color Y (green channel).
|
||||
float low = System.Math.Min(dawn.SunColor.Y, noon.SunColor.Y);
|
||||
float high = System.Math.Max(dawn.SunColor.Y, noon.SunColor.Y);
|
||||
Assert.InRange(midPt.SunColor.Y, low, high);
|
||||
// The RAW per-channel inputs (DirColor, AmbColor, brightness scalars)
|
||||
// lerp linearly between adjacent keyframes — that's the retail-faithful
|
||||
// separate-channel interpolation. The composite SunColor / AmbientColor
|
||||
// properties intentionally do NOT lerp linearly (their magnitude
|
||||
// depends nonlinearly on heading/pitch/brightness via the retail
|
||||
// sun-vector formula), so we assert on the raw inputs here.
|
||||
float low = System.Math.Min(dawn.DirColor.Y, noon.DirColor.Y);
|
||||
float high = System.Math.Max(dawn.DirColor.Y, noon.DirColor.Y);
|
||||
Assert.InRange(midPt.DirColor.Y, low, high);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetailSunVector_AtZenith_HasMagnitudeEqualToBrightness()
|
||||
{
|
||||
// Sun straight up (P=90°): cos(P)=0, sin(P)=1.
|
||||
// sunVec = (sin(H)×B×0, 0, B×1) = (0, 0, B)
|
||||
// |sunVec| = B
|
||||
var kf = new SkyKeyframe(
|
||||
Begin: 0.5f,
|
||||
SunHeadingDeg: 0f,
|
||||
SunPitchDeg: 90f,
|
||||
DirColor: Vector3.One,
|
||||
DirBright: 1.5f,
|
||||
AmbColor: Vector3.One,
|
||||
AmbBright: 0.3f,
|
||||
FogColor: Vector3.One,
|
||||
FogDensity: 0f);
|
||||
|
||||
var v = SkyStateProvider.RetailSunVector(kf);
|
||||
Assert.InRange(v.Length(), 1.49f, 1.51f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetailSunVector_AtHorizonNorth_MagnitudeIsOne()
|
||||
{
|
||||
// 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);
|
||||
|
||||
var v = SkyStateProvider.RetailSunVector(kf);
|
||||
Assert.InRange(v.Length(), 0.99f, 1.01f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SunColor_UsesRetailMagnitudeNotDirBrightDirectly()
|
||||
{
|
||||
// At sun pitch 90° (zenith) with H=0, B=2: |sunVec| = 2.
|
||||
// SunColor = DirColor × |sunVec| = (0.5, 0.5, 0.5) × 2 = (1, 1, 1).
|
||||
var kf = new SkyKeyframe(
|
||||
Begin: 0.5f,
|
||||
SunHeadingDeg: 0f,
|
||||
SunPitchDeg: 90f,
|
||||
DirColor: new Vector3(0.5f, 0.5f, 0.5f),
|
||||
DirBright: 2.0f,
|
||||
AmbColor: Vector3.One,
|
||||
AmbBright: 0.3f,
|
||||
FogColor: Vector3.One,
|
||||
FogDensity: 0f);
|
||||
|
||||
Assert.InRange(kf.SunColor.X, 0.99f, 1.01f);
|
||||
Assert.InRange(kf.SunColor.Y, 0.99f, 1.01f);
|
||||
Assert.InRange(kf.SunColor.Z, 0.99f, 1.01f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AmbientColor_BoostsByTwentyPercentOfSunVectorLength()
|
||||
{
|
||||
// |sunVec| = 1 (horizon north), AmbBright = 0.4, AmbColor = (1,1,1).
|
||||
// AmbientColor = AmbColor × (AmbBright + 0.2 × |sunVec|)
|
||||
// = (1,1,1) × (0.4 + 0.2) = (0.6, 0.6, 0.6).
|
||||
var kf = new SkyKeyframe(
|
||||
Begin: 0f,
|
||||
SunHeadingDeg: 0f,
|
||||
SunPitchDeg: 0f,
|
||||
DirColor: Vector3.One,
|
||||
DirBright: 1f,
|
||||
AmbColor: Vector3.One,
|
||||
AmbBright: 0.4f,
|
||||
FogColor: Vector3.One,
|
||||
FogDensity: 0f);
|
||||
|
||||
Assert.InRange(kf.AmbientColor.X, 0.59f, 0.61f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -56,8 +144,10 @@ public sealed class SkyStateTests
|
|||
Begin: 0.5f,
|
||||
SunHeadingDeg: 180f, // south
|
||||
SunPitchDeg: 70f,
|
||||
SunColor: Vector3.One,
|
||||
AmbientColor: Vector3.One,
|
||||
DirColor: Vector3.One,
|
||||
DirBright: 1f,
|
||||
AmbColor: Vector3.One,
|
||||
AmbBright: 1f,
|
||||
FogColor: Vector3.One,
|
||||
FogDensity: 0.001f);
|
||||
|
||||
|
|
|
|||
|
|
@ -58,8 +58,10 @@ public sealed class WorldTimeDebugTests
|
|||
Begin: 0f,
|
||||
SunHeadingDeg: 0f,
|
||||
SunPitchDeg: 90f,
|
||||
SunColor: System.Numerics.Vector3.One,
|
||||
AmbientColor: System.Numerics.Vector3.One,
|
||||
DirColor: System.Numerics.Vector3.One,
|
||||
DirBright: 1f,
|
||||
AmbColor: System.Numerics.Vector3.One,
|
||||
AmbBright: 1f,
|
||||
FogColor: System.Numerics.Vector3.Zero,
|
||||
FogDensity: 0f),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue