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:
parent
53608e77e3
commit
8a42750459
2 changed files with 61 additions and 9 deletions
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue