From bbea017484eaaab7a7683b7bbf22d9215d821377 Mon Sep 17 00:00:00 2001
From: pkuzic
Date: Fri, 25 Sep 2026 09:20:59 +0100
Subject: [PATCH] MONKEY (fog): Modern fog model (fogModel 1) in fog_hook +
fog_model.rs
fog_hook.wgsl: radial exponential fog fitted to the zone's 1.12 start/end
(60 % survival mid-span, closed over the last stretch so the zone end
holds), optional height fog, end-fog colour shift, sun-fog lobe; interior
WMO spans stay classic; the WDL hull pair takes the far colour.
lighting/fog_model.rs fills the MonkeyFrame fog rows each frame (derived
from the 1.12 bands, or LightFogBand.dbc rows); fog_d.x carries the scene
fog end as the Modern flag, fog_d.zw the octahedral sun direction.
resolve.rs: Modern stretches the fog end with farclip past 777 only.
sky.wgsl/sky.rs: the dome's low band eases into the same far colour.
Classic packs zeros and every new path branches out.
Includes the cherry-picked p0 fix 05d6b1ad (MonkeyFrame rows fog_a..).
Co-Authored-By: Claude Opus 5.5
---
.../benilla-assets/src/shaders/fog_hook.wgsl | 114 ++++++-
.../src/shaders/monkey_frame.wgsl | 2 +-
.../benilla-world/src/lighting/fog_model.rs | 310 ++++++++++++++++++
crates/benilla-world/src/lighting/mod.rs | 12 +
.../src/lighting/monkey_frame.rs | 16 +-
crates/benilla-world/src/lighting/resolve.rs | 11 +-
crates/benilla-world/src/shaders/sky.wgsl | 9 +-
crates/benilla-world/src/sky.rs | 30 +-
8 files changed, 492 insertions(+), 12 deletions(-)
create mode 100644 crates/benilla-world/src/lighting/fog_model.rs
diff --git a/crates/benilla-assets/src/shaders/fog_hook.wgsl b/crates/benilla-assets/src/shaders/fog_hook.wgsl
index 6730fbd8..c9d66bac 100644
--- a/crates/benilla-assets/src/shaders/fog_hook.wgsl
+++ b/crates/benilla-assets/src/shaders/fog_hook.wgsl
@@ -15,6 +15,21 @@
//
// Extension point for the FOG lane: `fog_sample` branches on `mf.fog_d.x` (`MonkeyFrame.fog_model`,
// 0 classic / 1 modern). The modern arm currently returns the classic result.
+//
+// MONKEY (fog): the Modern model (cvar `fogModel` 1; CPU half `lighting/fog_model.rs`). `fog_d.x`
+// holds the scene fog end when Modern is on (0 = Classic, every path below is then untouched):
+// - survival: radial distance, exponential from the zone's start, fitted so 60 % survives at the
+// middle of the 1.12 span, closed linearly over the last stretch (from max(start, 0.3·end), the
+// modern client's 1/0.7 end fade) so the fog still ends where the zone authored it; optional
+// height fog below `fog_a.y` (Bevy Y); `fog_a.w` blends back toward the 1.12 linear ramp.
+// - colour (scene colour only, never the black/white/grey blend policies): shifts to the end-fog
+// colour `fog_c` by (d / end)³, then leans to the sun-fog colour `fog_b` inside a cubic lobe around
+// the sun (cosine threshold `fog_d.y`, direction octahedral in `fog_d.zw`; strength already faded
+// for night on the CPU).
+// - only spans ending at the scene fog end take it: an interior WMO fog pair stays classic.
+// - the WDL hull's own (0, 1) pair asks for the horizon: fully fogged, in the far colour.
+// The sky dome (`fog_sky_horizon`) and the volumetric haze (`fog_modern_colour`) call the same
+// colour function, so the fogged world, the far hull and the sky's horizon meet in one colour.
#import benilla::monkey_frame::MonkeyFrame
@@ -24,6 +39,85 @@ fn fog_linear(eye_z: f32, span: vec2) -> f32 {
return clamp((span.y - eye_z) / denom, 0.0, 1.0);
}
+// MONKEY (fog): exp survival at the span midpoint = 0.6, i.e. k·(end − start) = −2·ln 0.6.
+const FOG_EXP_MID: f32 = 1.0216512;
+
+// MONKEY (fog): the unit vector packed by `fog_model::oct_encode`.
+fn fog_oct_decode(e: vec2) -> vec3 {
+ var v = vec3(e.x, 1.0 - abs(e.x) - abs(e.y), e.y);
+ let t = max(-v.y, 0.0);
+ v.x += select(t, -t, v.x >= 0.0);
+ v.z += select(t, -t, v.z >= 0.0);
+ return normalize(v);
+}
+
+// MONKEY (fog): whether the Modern model is on for this frame.
+fn fog_is_modern(sel_row: vec4) -> bool {
+ return sel_row.x > 0.5;
+}
+
+// MONKEY (fog): the Modern fog colour seen along unit `dir` at radial distance `d` (yd), from the
+// scene fog colour `base`: end-fog shift, then the sun lobe. `d` may be huge (the sky).
+fn fog_modern_colour(
+ base: vec3,
+ dir: vec3,
+ d: f32,
+ sun_row: vec4,
+ end_row: vec4,
+ sel_row: vec4,
+) -> vec3 {
+ var c = base;
+ if (end_row.w > 0.0) {
+ let t = clamp(d / end_row.w, 0.0, 1.0);
+ c = mix(c, end_row.rgb, t * t * t);
+ }
+ if (sun_row.w > 0.0) {
+ let sun = fog_oct_decode(sel_row.zw);
+ let a = sel_row.y;
+ let s = clamp((dot(dir, sun) - a) / max(1.0 - a, 0.001), 0.0, 1.0);
+ c = mix(c, sun_row.rgb, sun_row.w * s * s * s);
+ }
+ return c;
+}
+
+// MONKEY (fog): the Modern survival (1 = no fog) at radial distance `d` for the pair `span`.
+fn fog_modern_survival(span: vec2, d: f32, world_y: f32, height_row: vec4) -> f32 {
+ let len = max(span.y - span.x, 0.001);
+ let k = FOG_EXP_MID / len;
+ let x = max(d - span.x, 0.0);
+ var expo = exp(-k * x);
+ if (height_row.x > 0.0) {
+ // Extra density at and below the plane, fading out `1 / falloff` yd above it.
+ let hf = clamp(1.0 - (world_y - height_row.y) * height_row.z, 0.0, 1.0);
+ expo = mix(expo, exp(-k * (1.0 + height_row.x) * x), hf);
+ }
+ let fade_from = max(span.x, 0.3 * span.y);
+ let end_fade = clamp((span.y - d) / max(span.y - fade_from, 0.001), 0.0, 1.0);
+ let linear = clamp((span.y - d) / len, 0.0, 1.0);
+ return mix(min(expo, end_fade), linear, clamp(height_row.w, 0.0, 1.0));
+}
+
+// MONKEY (fog): the sky dome's low band under Modern. `col` is the dome's own colour at `dir`,
+// `horizon_rgb` the colour it uses at 0° (the scene fog colour); the far fog colour along `dir`
+// replaces the horizon and eases into the dome's gradient over the first 12° of elevation.
+// Classic returns `col` untouched.
+fn fog_sky_horizon(
+ col: vec3,
+ horizon_rgb: vec3,
+ dir: vec3,
+ sun_row: vec4,
+ end_row: vec4,
+ sel_row: vec4,
+) -> vec3 {
+ if (!fog_is_modern(sel_row)) {
+ return col;
+ }
+ let far = fog_modern_colour(horizon_rgb, dir, 1.0e9, sun_row, end_row, sel_row);
+ let elev = asin(clamp(dir.y, -1.0, 1.0));
+ let w = 1.0 - smoothstep(0.0, 0.20943951, elev);
+ return clamp(col + (far - horizon_rgb) * w, vec3(0.0), vec3(1.0));
+}
+
// The fog a surface stands in: rgb = the fog colour, w = how much of the surface survives (1 = no
// fog). `fog_rgb` is the colour the caller's policy picked; `scene_colour` is true when that is the
// scene's own fog colour (false for the forced black/white/grey policies of additive, Mod and Mod2x
@@ -40,9 +134,23 @@ fn fog_sample(
) -> vec4 {
let classic = vec4(fog_rgb, fog_linear(eye_z, span));
if (mf.fog_d.x > 0.5) {
- // FOG lane: the modern model (height / sun / end-colour / curve fog) goes here, reading
- // mf.fog_a..fog_d. Until it lands, the modern arm is the classic law.
- return classic;
+ // MONKEY (fog): the Modern model (header).
+ let delta = world_pos - camera_pos;
+ let d = length(delta);
+ let dir = delta / max(d, 0.0001);
+ if (span.x == 0.0 && span.y == 1.0) {
+ // The WDL hull's own pair: the horizon, fully fogged in the far colour.
+ return vec4(fog_modern_colour(fog_rgb, dir, d, mf.fog_b, mf.fog_c, mf.fog_d), 0.0);
+ }
+ if (abs(span.y - mf.fog_d.x) > 0.01) {
+ // Not the scene fog (an interior WMO pair): classic.
+ return classic;
+ }
+ var rgb = fog_rgb;
+ if (scene_colour) {
+ rgb = fog_modern_colour(fog_rgb, dir, d, mf.fog_b, mf.fog_c, mf.fog_d);
+ }
+ return vec4(rgb, fog_modern_survival(span, d, world_pos.y, mf.fog_a));
}
return classic;
}
diff --git a/crates/benilla-assets/src/shaders/monkey_frame.wgsl b/crates/benilla-assets/src/shaders/monkey_frame.wgsl
index 97cbeba8..8d0ca4ce 100644
--- a/crates/benilla-assets/src/shaders/monkey_frame.wgsl
+++ b/crates/benilla-assets/src/shaders/monkey_frame.wgsl
@@ -14,7 +14,7 @@ struct MonkeyFrame {
fog_a: vec4, // height_fog_density, height_fog_height (world Y, yd), height_fog_falloff, curve_blend
fog_b: vec4, // sun_fog_rgb (gamma 0..1), sun_fog_strength
fog_c: vec4, // end_fog_rgb (gamma 0..1), end_fog_distance (yd)
- fog_d: vec4, // fog_model (0 classic, 1 modern), sun_fog_angle (cos), 0, 0
+ fog_d: vec4, // fog_model (0 classic; modern = scene fog end yd >= 1), sun_fog_angle (cos), sun dir (octahedral xy)
wind_a: vec4, // dir_x, dir_y (unit, world XZ: .x = world x, .y = world z), speed (yd/s), gust 0..1
wind_b: vec4, // time_s, sway_strength, grass_strength, tree_strength
wet_a: vec4, // rain_rate 0..1, wetness 0..1, ripple_time_s, snow 0..1
diff --git a/crates/benilla-world/src/lighting/fog_model.rs b/crates/benilla-world/src/lighting/fog_model.rs
new file mode 100644
index 00000000..a700116f
--- /dev/null
+++ b/crates/benilla-world/src/lighting/fog_model.rs
@@ -0,0 +1,310 @@
+//! MONKEY (fog): the Modern fog model's CPU half — fills the FOG rows of [`MonkeyFrame`] every
+//! frame. The law itself lives in `benilla-assets/src/shaders/fog_hook.wgsl`.
+//!
+//! Setting: cvar `fogModel` (0 Classic = the 1.12 linear fog, byte-identical; 1 Modern), bridged by
+//! `benilla-app/src/monkey_gfx.rs` into [`FogModelSetting`]; `WOW_FOGMODEL=0|1` overrides it for a
+//! session (captures).
+//!
+//! Modern, per frame:
+//! - **Distances** stay the zone's: the shader's exponential curve is fitted to the 1.12 start/end
+//! pair it already receives. Only past the reference's farclip ceiling does the end grow with the
+//! view distance ([`modern_fog_end`], called from `resolve.rs`), so a larger `farclip` shows more.
+//! - **Colours** without `LightFogBand.dbc` rows are derived from the 1.12 bands
+//! ([`derived_band`]): the sun-fog lobe leans the fog toward the sun colour (IntBand 9) around the
+//! sun and fades out at night; the far fog shifts toward the sky's lowest ring (1.8°) so the
+//! fogged world, the WDL hull and the sky dome's horizon meet in one colour. Height fog is off.
+//! - With `LightFogBand.dbc` rows, those override the derivation per `LightParams`, blended over
+//! the same area-light chain as the stock bands.
+//!
+//! Packed rows (see `monkey_frame.rs`): `fog_a` height density / height (Bevy Y) / falloff / curve
+//! blend; `fog_b` sun rgb / strength × day; `fog_c` end rgb / end distance; `fog_d` the scene fog end
+//! (> 0 = Modern; the shader applies the model only to spans ending there, so an interior WMO fog
+//! stays classic) / sun-fog cosine / the sun direction, octahedral-encoded.
+
+use bevy::prelude::*;
+
+use benilla_assets::{LockRecover, WorldAssets};
+use benilla_formats::{FogBand, FogBandCatalog};
+
+use super::{FogModel, LightSampler, MonkeyFrame, WowLighting};
+use crate::terrain_stream::SPAWN_XY;
+use crate::view::WorldCamera;
+use crate::world_map::CurrentMap;
+use benilla_assets::coords::bevy_to_wow;
+
+/// The live fog-model choice (cvar `fogModel`). Default Classic; `WOW_FOGMODEL` overrides.
+#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq)]
+pub struct FogModelSetting(pub FogModel);
+
+impl Default for FogModelSetting {
+ fn default() -> Self {
+ FogModelSetting(env_override().unwrap_or(FogModel::Classic))
+ }
+}
+
+/// `WOW_FOGMODEL=0|1`, read once.
+fn env_override() -> Option {
+ static ENV: std::sync::OnceLock