sky(phase-5b): port retail vertex fog onto sky meshes

Retail applies linear vertex fog with 3D range distance
(D3DRS_FOGVERTEXMODE=3=LINEAR, D3DRS_RANGEFOGENABLE=1,
D3DRS_FOGTABLEMODE=0=NONE) to ALL mesh draws including sky. Only
FOGCOLOR / FOGSTART / FOGEND are lerped per keyframe; the mode flags
are init-only.

Verified in `docs/research/2026-04-23-sky-fog.md`:
  - chunk_005A0000.c:3361-3389 device-init sets the modes.
  - Sky meshes render at world origin (translation zeroed, rotation-
    only) with intrinsic mesh radii in the thousands of meters
    (WorldBuilder's SkyboxRenderManager.cs:247 comment confirms).
  - With keyframe MaxWorldFog = 2400m, the dome saturates to
    WorldFogColor at its horizon band. THAT is retail's dusk/dawn
    horizon-glow mechanism.

Port:

`sky.vert` now computes the vertex fog factor:
    worldPos = uModel × aPos         (camera-centered since view translation=0)
    dist = length(worldPos.xyz)
    fogFactor = clamp((fogEnd - dist) / (fogEnd - fogStart), 0, 1)
  — outputs as varying vFogFactor. 1.0 means no fog contribution,
  0.0 means full fog color.

`sky.frag` applies the mix BEFORE the lightning-flash bump:
    rgb = mix(uFogColor.rgb, rgb, vFogFactor)

Uses the existing SceneLighting UBO's uFogParams (x=start, y=end,
z=flash, w=mode) and uFogColor — no new uniforms, no C# change.

Expected visual:
  - Dome at dawn/dusk: horizon band blends toward keyframe fogColor
    (warm orange at sunset, cool blue at dawn), matching retail's
    sky/fog coupling.
  - Close sky objects (sun disk at typical mesh radius): unaffected
    since dist < fogStart.
  - Clouds at intermediate distance: partial fog blend, subtly
    muting their saturation with distance.

Note on lightning: the flash channel (uFogParams.z) stays wired but
is currently always 0 because no code drives it. Agent #5 is
researching retail's real lightning mechanism (PlayScript / SetLight
PhysicsScript / other). This commit does not attempt to port it.

Build + 733 tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-24 11:06:57 +02:00
parent 53608e77e3
commit 8a42750459
2 changed files with 61 additions and 9 deletions

View file

@ -16,16 +16,18 @@
//
// See `docs/research/2026-04-23-sky-material-state.md`.
in vec2 vTex;
in vec3 vTint;
in vec2 vTex;
in vec3 vTint;
in float vFogFactor; // 1 = no fog (near), 0 = full fog color (far)
out vec4 fragColor;
uniform sampler2D uDiffuse;
uniform float uTransparency; // 0 = fully visible, 1 = fully transparent
uniform float uLuminosity; // SkyObjectReplace.Luminosity override (0..1)
// Shared SceneLighting UBO — only need the fog-flash channel for
// client-driven lightning strobes; sun/ambient already baked into vTint.
// Shared SceneLighting UBO — fog params drive the mix, flash channel
// bumps sky brightness during lightning strikes. Matches sky.vert's
// declaration exactly.
struct Light {
vec4 posAndKind;
vec4 dirAndRange;
@ -46,14 +48,24 @@ void main() {
// Composite: texture × per-vertex lit × per-keyframe dim.
vec3 rgb = sampled.rgb * vTint * uLuminosity;
// Lightning additive bump (client-driven during storm keyframes).
// Retail vertex fog: lerp(fogColor, scene, fogFactor). At distant
// horizon dome vertices (distance > FOGEND) the sky saturates to
// the keyframe's WorldFogColor — that's retail's horizon-glow
// mechanism at dusk/dawn. See docs/research/2026-04-23-sky-fog.md.
rgb = mix(uFogColor.rgb, rgb, vFogFactor);
// Lightning additive bump — client-driven during storm flashes.
// NOTE: the exact retail mechanism for lightning visual is still
// under research (agent #5, 2026-04-23). Keeping the uFogParams.z
// channel wired so if it ends up being a per-frame flash uniform
// that's what it becomes; if lightning turns out to be a particle
// system effect instead, this bump becomes a no-op (flash stays 0).
float flash = uFogParams.z;
rgb += flash * vec3(1.5, 1.5, 1.8);
// Normal-frame cap at 1.0 (retail D3D framebuffer clamps at 1.0
// per channel for RGBA8 output; vTint is already vertex-clamped so
// the only path above 1 is lightning flash additive bump). During
// a flash the ceiling relaxes so the strobe blows out visibly.
// Normal-frame cap at 1.0 (retail D3D framebuffer clamps per-channel
// on output). Flash relaxes ceiling to 3.0 so storm strobes blow
// out visibly.
float cap = mix(1.0, 3.0, clamp(flash, 0.0, 1.0));
rgb = min(rgb, vec3(cap));

View file

@ -48,8 +48,25 @@ uniform vec3 uSunDir; // unit vector FROM surface TO sun
// Per-submesh (from Surface.Luminosity float):
uniform float uEmissive;
// Shared SceneLighting UBO — we need uFogParams.xy (fog start/end) to
// compute the vertex fog factor. Must match sky.frag's declaration.
struct Light {
vec4 posAndKind;
vec4 dirAndRange;
vec4 colorAndIntensity;
vec4 coneAngleEtc;
};
layout(std140, binding = 1) uniform SceneLighting {
Light uLights[8];
vec4 uCellAmbient;
vec4 uFogParams; // x=fogStart, y=fogEnd, z=flash, w=fogMode
vec4 uFogColor;
vec4 uCameraAndTime;
};
out vec2 vTex;
out vec3 vTint;
out float vFogFactor; // 1 = no fog (close), 0 = full fog (far)
void main() {
vTex = aTex + uUvScroll;
@ -72,4 +89,27 @@ void main() {
+ uAmbientColor // material.Ambient(1) × light.Ambient
+ uSunColor * diff; // material.Diffuse(1) × light.Diffuse × N·L
vTint = clamp(lit, 0.0, 1.0);
// Retail vertex-fog in 3D-range mode (FOGVERTEXMODE=LINEAR,
// RANGEFOGENABLE=1, FOGTABLEMODE=NONE per device init — never
// toggled per frame). Distance = `|worldPos - cameraPos|`. Since
// our sky view matrix has translation zeroed (sky is camera-
// centered), the post-uModel position IS the camera-relative
// world-space vector, so its length is the 3D range distance.
// See docs/research/2026-04-23-sky-fog.md.
//
// Formula: fogFactor = clamp((fogEnd - dist) / (fogEnd - fogStart), 0, 1)
// 1.0 → no fog contribution (scene color wins)
// 0.0 → full fog color (sky color fades to fog)
//
// Sky meshes have intrinsic radii in the thousands of meters (dome
// / stars / moon are authored at large distances in the dat); at
// typical keyframe FOGEND=2400m, the dome saturates to fogColor at
// its horizon band. THAT is how retail colors the horizon at dusk.
vec4 worldPos = uModel * vec4(aPos, 1.0);
float dist = length(worldPos.xyz);
float fogStart = uFogParams.x;
float fogEnd = uFogParams.y;
float span = max(fogEnd - fogStart, 1e-3);
vFogFactor = clamp((fogEnd - dist) / span, 0.0, 1.0);
}