From 8a42750459e6bf1ffe36aae52953d3e508948fdd Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 24 Apr 2026 11:06:57 +0200 Subject: [PATCH] sky(phase-5b): port retail vertex fog onto sky meshes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/AcDream.App/Rendering/Shaders/sky.frag | 30 +++++++++++----- src/AcDream.App/Rendering/Shaders/sky.vert | 40 ++++++++++++++++++++++ 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/AcDream.App/Rendering/Shaders/sky.frag b/src/AcDream.App/Rendering/Shaders/sky.frag index 7015957..95eaaeb 100644 --- a/src/AcDream.App/Rendering/Shaders/sky.frag +++ b/src/AcDream.App/Rendering/Shaders/sky.frag @@ -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)); diff --git a/src/AcDream.App/Rendering/Shaders/sky.vert b/src/AcDream.App/Rendering/Shaders/sky.vert index 48e5987..1a2427f 100644 --- a/src/AcDream.App/Rendering/Shaders/sky.vert +++ b/src/AcDream.App/Rendering/Shaders/sky.vert @@ -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); }