fix(sky): retail-faithful keyframe lerp — separate-channel color/bright
Retail's SkyDesc::GetLighting at 0x00500ac9 (decomp lines 261317-261331)
lerps each color channel and the brightness scalar SEPARATELY, then
multiplies post-lerp:
arg4.r = lerp(k1.amb_color.r, k2.amb_color.r, u)
arg4.g = lerp(k1.amb_color.g, k2.amb_color.g, u)
arg4.b = lerp(k1.amb_color.b, k2.amb_color.b, u)
arg3 = lerp(k1.amb_bright, k2.amb_bright, u)
final = (arg4.rgb * arg3, ...)
acdream pre-multiplied (color × bright) at LOAD time
(`SkyDescLoader.cs:558-559`) and then lerped the product. For any
keyframe pair where both color and brightness change, the two are
mathematically distinct. Example, k1=(white, b=0.5) k2=(black, b=1.0)
at u=0.5:
- retail: color=gray(0.5), bright=0.75 → final = (0.375, 0.375, 0.375)
- acdream: lerp((0.5,0.5,0.5), (0,0,0), 0.5) = (0.25, 0.25, 0.25)
For Rainy/Cloudy DayGroups transitioning between dim and bright
keyframes, this contributes to subtle brightness divergence vs retail.
Refactor:
SkyKeyframe stores DirColor / DirBright / AmbColor / AmbBright
SEPARATELY (raw, not pre-multiplied).
Computed properties SunColor and AmbientColor return the
post-multiplied product, keeping the shader uniform interface
(uSunColor / uAmbientColor) unchanged.
SkyStateProvider.Interpolate lerps each raw channel, then constructs
a new SkyKeyframe whose computed properties yield the correct
post-lerp multiply.
SkyDescLoader now stores raw values without pre-multiplying.
GameWindow comment updated; no functional change there.
Default factory + tests updated to use the new constructor parameters
with DirBright=AmbBright=1.0 (preserving exact existing behavior).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dbe6690a4e
commit
63b50c5291
5 changed files with 97 additions and 31 deletions
|
|
@ -5225,9 +5225,11 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
else
|
||||
{
|
||||
// Outdoor: full keyframe sun + ambient; colors are already
|
||||
// pre-multiplied by DirBright / AmbBright inside
|
||||
// SkyDescLoader so we feed them straight into the UBO.
|
||||
// Outdoor: full keyframe sun + ambient. The SkyKeyframe stores
|
||||
// raw DirColor + DirBright (and AmbColor + AmbBright) for
|
||||
// retail-faithful per-channel keyframe interpolation; the
|
||||
// computed `kf.SunColor` / `kf.AmbientColor` properties return
|
||||
// the post-multiplied product the shader expects.
|
||||
Lighting.Sun = new AcDream.Core.Lighting.LightSource
|
||||
{
|
||||
Kind = AcDream.Core.Lighting.LightKind.Directional,
|
||||
|
|
|
|||
|
|
@ -551,12 +551,23 @@ public static class SkyDescLoader
|
|||
_ => FogMode.Off,
|
||||
};
|
||||
|
||||
// Store DirColor / AmbColor RAW and DirBright / AmbBright SEPARATE
|
||||
// (NOT pre-multiplied) so the keyframe interpolator can lerp each
|
||||
// channel independently — matches retail SkyDesc::GetLighting at
|
||||
// 0x00500ac9 (decomp lines 261317-261331). Multiplying at load
|
||||
// time and lerping the product produces mathematically different
|
||||
// results than retail when both color and brightness change
|
||||
// between adjacent keyframes. The post-multiplied values are
|
||||
// available via `kf.SunColor` / `kf.AmbientColor` computed
|
||||
// properties for shader-uniform plumbing.
|
||||
var kf = new SkyKeyframe(
|
||||
Begin: s.Begin,
|
||||
SunHeadingDeg: s.DirHeading,
|
||||
SunPitchDeg: s.DirPitch,
|
||||
SunColor: ColorToVec3(s.DirColor) * s.DirBright,
|
||||
AmbientColor: ColorToVec3(s.AmbColor) * s.AmbBright,
|
||||
DirColor: ColorToVec3(s.DirColor),
|
||||
DirBright: s.DirBright,
|
||||
AmbColor: ColorToVec3(s.AmbColor),
|
||||
AmbBright: s.AmbBright,
|
||||
FogColor: ColorToVec3(s.WorldFogColor),
|
||||
FogDensity: 0f,
|
||||
FogStart: s.MinWorldFog,
|
||||
|
|
|
|||
|
|
@ -34,24 +34,53 @@ public enum FogMode
|
|||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Colors are in LINEAR RGB, already pre-multiplied by their brightness
|
||||
/// scalar so the shader can plug them straight into the UBO without
|
||||
/// knowing about <c>DirBright</c> / <c>AmbBright</c>. Range is loosely
|
||||
/// [0, N] — retail dusk tints have channels above 1.0 and the frag
|
||||
/// shader clamps after lighting math.
|
||||
/// Colors are stored RAW (NOT pre-multiplied by brightness) in
|
||||
/// <see cref="DirColor"/> / <see cref="AmbColor"/> with the brightness
|
||||
/// scalars in <see cref="DirBright"/> / <see cref="AmbBright"/>. Retail's
|
||||
/// <c>SkyDesc::GetLighting</c> at <c>0x00500ac9</c> (decomp lines
|
||||
/// 261317-261331) lerps each channel separately and lerps brightness
|
||||
/// separately, then multiplies post-lerp. Lerping the pre-multiplied
|
||||
/// product gives mathematically different results when both color and
|
||||
/// brightness change between adjacent keyframes — the cause of subtle
|
||||
/// brightness discrepancies vs retail observed in dual-client
|
||||
/// comparisons (Issue #3 visual sub-bug, 2026-04-27).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The computed properties <see cref="SunColor"/> and
|
||||
/// <see cref="AmbientColor"/> return the post-multiplied product, so
|
||||
/// downstream shader uniform plumbing (sky.vert / mesh.vert /
|
||||
/// SceneLightingUbo) is unchanged.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public readonly record struct SkyKeyframe(
|
||||
float Begin, // [0, 1] day-fraction this keyframe kicks in
|
||||
float SunHeadingDeg, // compass heading (0=N, 90=E, 180=S, 270=W)
|
||||
float SunPitchDeg, // elevation above horizon (-90=below, +90=zenith)
|
||||
Vector3 SunColor, // RGB linear, post-brightness multiply
|
||||
Vector3 AmbientColor, // RGB linear, post-brightness multiply
|
||||
Vector3 DirColor, // RGB linear, RAW (NOT × DirBright)
|
||||
float DirBright, // sun brightness multiplier
|
||||
Vector3 AmbColor, // RGB linear, RAW (NOT × AmbBright)
|
||||
float AmbBright, // ambient brightness multiplier
|
||||
Vector3 FogColor,
|
||||
float FogDensity, // retained for tests; derive from FogStart/End
|
||||
float FogStart = 80f, // meters (retail default ~120 clear, ~40 storm)
|
||||
float FogEnd = 350f, // meters (retail default ~350 clear, ~150 storm)
|
||||
FogMode FogMode = FogMode.Linear);
|
||||
FogMode FogMode = FogMode.Linear)
|
||||
{
|
||||
/// <summary>
|
||||
/// Final directional sun color used by the shader =
|
||||
/// <see cref="DirColor"/> × <see cref="DirBright"/>. Computed property
|
||||
/// so the storage stays as separate channels (for retail-faithful
|
||||
/// keyframe interpolation) while the shader interface stays simple.
|
||||
/// </summary>
|
||||
public Vector3 SunColor => DirColor * DirBright;
|
||||
|
||||
/// <summary>
|
||||
/// Final ambient color used by the shader =
|
||||
/// <see cref="AmbColor"/> × <see cref="AmbBright"/>. See
|
||||
/// <see cref="SunColor"/> for the rationale.
|
||||
/// </summary>
|
||||
public Vector3 AmbientColor => AmbColor * AmbBright;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sky keyframe interpolator — given a day fraction in [0, 1), returns
|
||||
|
|
@ -111,12 +140,18 @@ public sealed class SkyStateProvider
|
|||
// Day fractions: 0.0=midnight, 0.25=dawn, 0.5=noon, 0.75=dusk.
|
||||
return new SkyStateProvider(new[]
|
||||
{
|
||||
// Default factory: brightness scalars are 1.0 here — the colors
|
||||
// ARE the final intended values. Live Dereth keyframes loaded
|
||||
// from the dat have separate non-1.0 DirBright/AmbBright values
|
||||
// and the renderer multiplies them post-lerp.
|
||||
new SkyKeyframe(
|
||||
Begin: 0.0f,
|
||||
SunHeadingDeg: 0f, // below horizon (north)
|
||||
SunPitchDeg: -30f,
|
||||
SunColor: new Vector3(0.02f, 0.02f, 0.08f), // deep blue
|
||||
AmbientColor: new Vector3(0.05f, 0.05f, 0.12f),
|
||||
DirColor: new Vector3(0.02f, 0.02f, 0.08f), // deep blue
|
||||
DirBright: 1.0f,
|
||||
AmbColor: new Vector3(0.05f, 0.05f, 0.12f),
|
||||
AmbBright: 1.0f,
|
||||
FogColor: new Vector3(0.02f, 0.02f, 0.05f),
|
||||
FogDensity: 0.004f,
|
||||
FogStart: 30f,
|
||||
|
|
@ -126,8 +161,10 @@ public sealed class SkyStateProvider
|
|||
Begin: 0.25f,
|
||||
SunHeadingDeg: 90f, // east at dawn
|
||||
SunPitchDeg: 0f,
|
||||
SunColor: new Vector3(1.0f, 0.7f, 0.4f), // sunrise warm
|
||||
AmbientColor: new Vector3(0.4f, 0.35f, 0.3f),
|
||||
DirColor: new Vector3(1.0f, 0.7f, 0.4f), // sunrise warm
|
||||
DirBright: 1.0f,
|
||||
AmbColor: new Vector3(0.4f, 0.35f, 0.3f),
|
||||
AmbBright: 1.0f,
|
||||
FogColor: new Vector3(0.8f, 0.55f, 0.4f),
|
||||
FogDensity: 0.002f,
|
||||
FogStart: 60f,
|
||||
|
|
@ -137,8 +174,10 @@ public sealed class SkyStateProvider
|
|||
Begin: 0.5f,
|
||||
SunHeadingDeg: 180f, // south at noon
|
||||
SunPitchDeg: 70f,
|
||||
SunColor: new Vector3(1.0f, 0.98f, 0.95f), // bright white-ish
|
||||
AmbientColor: new Vector3(0.5f, 0.5f, 0.55f),
|
||||
DirColor: new Vector3(1.0f, 0.98f, 0.95f), // bright white-ish
|
||||
DirBright: 1.0f,
|
||||
AmbColor: new Vector3(0.5f, 0.5f, 0.55f),
|
||||
AmbBright: 1.0f,
|
||||
FogColor: new Vector3(0.7f, 0.75f, 0.85f),
|
||||
FogDensity: 0.0008f,
|
||||
FogStart: 120f,
|
||||
|
|
@ -148,8 +187,10 @@ public sealed class SkyStateProvider
|
|||
Begin: 0.75f,
|
||||
SunHeadingDeg: 270f, // west at dusk
|
||||
SunPitchDeg: 0f,
|
||||
SunColor: new Vector3(0.95f, 0.4f, 0.25f), // sunset red
|
||||
AmbientColor: new Vector3(0.35f, 0.25f, 0.25f),
|
||||
DirColor: new Vector3(0.95f, 0.4f, 0.25f), // sunset red
|
||||
DirBright: 1.0f,
|
||||
AmbColor: new Vector3(0.35f, 0.25f, 0.25f),
|
||||
AmbBright: 1.0f,
|
||||
FogColor: new Vector3(0.85f, 0.45f, 0.35f),
|
||||
FogDensity: 0.002f,
|
||||
FogStart: 60f,
|
||||
|
|
@ -194,17 +235,25 @@ public sealed class SkyStateProvider
|
|||
// Angular lerp for sun heading: pick shortest arc.
|
||||
float heading = ShortestAngleLerp(k1.SunHeadingDeg, k2.SunHeadingDeg, u);
|
||||
|
||||
// Fog mode doesn't interpolate — pick k1's mode (retail uses Linear everywhere).
|
||||
// Retail-faithful interpolation: lerp DirColor / DirBright /
|
||||
// AmbColor / AmbBright as SEPARATE CHANNELS, not as the
|
||||
// pre-multiplied product. Mirrors SkyDesc::GetLighting at
|
||||
// 0x00500ac9 (decomp lines 261317-261331). The post-multiplied
|
||||
// SunColor / AmbientColor are computed properties on the result.
|
||||
// Fog mode doesn't interpolate — pick k1's mode (retail uses
|
||||
// Linear everywhere).
|
||||
return new SkyKeyframe(
|
||||
Begin: t,
|
||||
SunHeadingDeg: heading,
|
||||
SunPitchDeg: Lerp(k1.SunPitchDeg, k2.SunPitchDeg, u),
|
||||
SunColor: Vector3.Lerp(k1.SunColor, k2.SunColor, u),
|
||||
AmbientColor: Vector3.Lerp(k1.AmbientColor, k2.AmbientColor, u),
|
||||
FogColor: Vector3.Lerp(k1.FogColor, k2.FogColor, u),
|
||||
DirColor: Vector3.Lerp(k1.DirColor, k2.DirColor, u),
|
||||
DirBright: Lerp(k1.DirBright, k2.DirBright, u),
|
||||
AmbColor: Vector3.Lerp(k1.AmbColor, k2.AmbColor, u),
|
||||
AmbBright: Lerp(k1.AmbBright, k2.AmbBright, u),
|
||||
FogColor: Vector3.Lerp(k1.FogColor, k2.FogColor, u),
|
||||
FogDensity: Lerp(k1.FogDensity, k2.FogDensity, u),
|
||||
FogStart: Lerp(k1.FogStart, k2.FogStart, u),
|
||||
FogEnd: Lerp(k1.FogEnd, k2.FogEnd, u),
|
||||
FogStart: Lerp(k1.FogStart, k2.FogStart, u),
|
||||
FogEnd: Lerp(k1.FogEnd, k2.FogEnd, u),
|
||||
FogMode: k1.FogMode);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue