From f0b1de26aedebff9c83d8ca8c835aea231c9268d Mon Sep 17 00:00:00 2001 From: pkuzic Date: Fri, 25 Sep 2026 08:37:39 +0100 Subject: [PATCH 1/6] MONKEY (p0): WGSL fixes found by the world-scene run naga_oil refuses a composable-module identifier that naga's namer would rewrite (a trailing digit gets a '_'), which failed every pipeline that imports monkey_frame / enhanced_water (models, WMOs, liquid invisible). MonkeyFrame rows are renamed fog_a..fog_d, wind_a/b, wet_a (same order and layout), and enhanced_water's padding member _sh_c16 becomes _sh_quad. Co-Authored-By: Claude Opus 5.5 --- LIGHTING.md | 14 +++++++------- .../src/shaders/enhanced_water.wgsl | 2 +- crates/benilla-assets/src/shaders/fog_hook.wgsl | 6 +++--- .../benilla-assets/src/shaders/monkey_frame.wgsl | 16 +++++++++------- .../benilla-world/src/lighting/monkey_frame.rs | 16 ++++++++-------- 5 files changed, 28 insertions(+), 26 deletions(-) diff --git a/LIGHTING.md b/LIGHTING.md index bb879614..6a3a2d69 100644 --- a/LIGHTING.md +++ b/LIGHTING.md @@ -65,13 +65,13 @@ is its own module, documented in `WATER.md`. | row | x | y | z | w | |---|---|---|---|---| - | `fog0` | height_fog_density | height_fog_height | height_fog_falloff | curve_blend | - | `fog1` | sun_fog r | g | b | sun_fog_strength | - | `fog2` | end_fog r | g | b | end_fog_distance | - | `fog3` | fog_model (0 classic, 1 modern) | sun_fog_angle | 0 | 0 | - | `wind0` | dir_x | dir_y | speed | gust | - | `wind1` | time_s | sway_strength | grass_strength | tree_strength | - | `wet0` | rain_rate | wetness | ripple_time_s | snow | + | `fog_a` | height_fog_density | height_fog_height | height_fog_falloff | curve_blend | + | `fog_b` | sun_fog r | g | b | sun_fog_strength | + | `fog_c` | end_fog r | g | b | end_fog_distance | + | `fog_d` | fog_model (0 classic, 1 modern) | sun_fog_angle | 0 | 0 | + | `wind_a` | dir_x | dir_y | speed | gust | + | `wind_b` | time_s | sway_strength | grass_strength | tree_strength | + | `wet_a` | rain_rate | wetness | ripple_time_s | snow | | `misc` | bender_count | time_of_day 0..1 | night 0..1 | 0 | | `benders[8]` | world x | world y | world z | radius | diff --git a/crates/benilla-assets/src/shaders/enhanced_water.wgsl b/crates/benilla-assets/src/shaders/enhanced_water.wgsl index 80d54670..4d7d2789 100644 --- a/crates/benilla-assets/src/shaders/enhanced_water.wgsl +++ b/crates/benilla-assets/src/shaders/enhanced_water.wgsl @@ -53,7 +53,7 @@ struct WaterLight { fog_color: vec4, // 4 fog_params: vec4, // 5 _sh: array, 6>, // 6-11 - _sh_c16: vec4, // 12 + _sh_quad: vec4, // 12 (MONKEY p0: was `_sh_c16`; a composable module member may not end in a digit once the struct embeds an imported type) water_river: array, 2>, // 13-14 water_ocean: array, 2>, // 15-16 _grade: vec4, // 17 diff --git a/crates/benilla-assets/src/shaders/fog_hook.wgsl b/crates/benilla-assets/src/shaders/fog_hook.wgsl index e931ccf9..6730fbd8 100644 --- a/crates/benilla-assets/src/shaders/fog_hook.wgsl +++ b/crates/benilla-assets/src/shaders/fog_hook.wgsl @@ -13,7 +13,7 @@ // `mix(fog_rgb, rgb, factor)`. The expressions below are the ones the receivers carried, in the // same order, so the classic result is bit-identical. // -// Extension point for the FOG lane: `fog_sample` branches on `mf.fog3.x` (`MonkeyFrame.fog_model`, +// 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. #import benilla::monkey_frame::MonkeyFrame @@ -39,9 +39,9 @@ fn fog_sample( mf: MonkeyFrame, ) -> vec4 { let classic = vec4(fog_rgb, fog_linear(eye_z, span)); - if (mf.fog3.x > 0.5) { + if (mf.fog_d.x > 0.5) { // FOG lane: the modern model (height / sun / end-colour / curve fog) goes here, reading - // mf.fog0..fog3. Until it lands, the modern arm is the classic law. + // mf.fog_a..fog_d. Until it lands, the modern arm is the classic law. return classic; } return classic; diff --git a/crates/benilla-assets/src/shaders/monkey_frame.wgsl b/crates/benilla-assets/src/shaders/monkey_frame.wgsl index ea1780b3..97cbeba8 100644 --- a/crates/benilla-assets/src/shaders/monkey_frame.wgsl +++ b/crates/benilla-assets/src/shaders/monkey_frame.wgsl @@ -8,14 +8,16 @@ // // Coordinates are Bevy world space (Y up, 1 unit = 1 yd), the space of the receivers' // `world_position`: WoW `(x, y, z)` is Bevy `(-y, z, -x)` (`benilla_assets::coords::wow_to_bevy`). +// Member names carry no trailing digit: naga_oil refuses a composable-module identifier that +// naga's namer would rewrite (`fog0` -> `fog0_`), so the rows are `fog_a..fog_d`, `wind_a/b`, `wet_a`. struct MonkeyFrame { - fog0: vec4, // height_fog_density, height_fog_height (world Y, yd), height_fog_falloff, curve_blend - fog1: vec4, // sun_fog_rgb (gamma 0..1), sun_fog_strength - fog2: vec4, // end_fog_rgb (gamma 0..1), end_fog_distance (yd) - fog3: vec4, // fog_model (0 classic, 1 modern), sun_fog_angle (cos), 0, 0 - wind0: vec4, // dir_x, dir_y (unit, world XZ: .x = world x, .y = world z), speed (yd/s), gust 0..1 - wind1: vec4, // time_s, sway_strength, grass_strength, tree_strength - wet0: vec4, // rain_rate 0..1, wetness 0..1, ripple_time_s, snow 0..1 + 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 + 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 misc: vec4, // bender_count, time_of_day 0..1 (packer), night 0..1 (packer), 0 benders: array, 8>, // world xyz (Bevy space) + radius (yd); the first bender_count are live } diff --git a/crates/benilla-world/src/lighting/monkey_frame.rs b/crates/benilla-world/src/lighting/monkey_frame.rs index 3ca56f7d..c583ec98 100644 --- a/crates/benilla-world/src/lighting/monkey_frame.rs +++ b/crates/benilla-world/src/lighting/monkey_frame.rs @@ -14,13 +14,13 @@ //! //! | row | xyz / x,y,z | w | //! |---|---|---| -//! | 0 `fog0` | height_fog_density, height_fog_height, height_fog_falloff | curve_blend | -//! | 1 `fog1` | sun_fog_rgb | sun_fog_strength | -//! | 2 `fog2` | end_fog_rgb | end_fog_distance | -//! | 3 `fog3` | fog_model (0 classic, 1 modern), sun_fog_angle, 0 | 0 | -//! | 4 `wind0` | dir_x, dir_y, speed | gust | -//! | 5 `wind1` | time_s, sway_strength, grass_strength | tree_strength | -//! | 6 `wet0` | rain_rate, wetness, ripple_time_s | snow | +//! | 0 `fog_a` | height_fog_density, height_fog_height, height_fog_falloff | curve_blend | +//! | 1 `fog_b` | sun_fog_rgb | sun_fog_strength | +//! | 2 `fog_c` | end_fog_rgb | end_fog_distance | +//! | 3 `fog_d` | fog_model (0 classic, 1 modern), sun_fog_angle, 0 | 0 | +//! | 4 `wind_a` | dir_x, dir_y, speed | gust | +//! | 5 `wind_b` | time_s, sway_strength, grass_strength | tree_strength | +//! | 6 `wet_a` | rain_rate, wetness, ripple_time_s | snow | //! | 7 `misc` | bender_count, time_of_day 0..1, night 0..1 | 0 | //! | 8-15 `benders` | world x, y, z | radius | //! @@ -38,7 +38,7 @@ pub const MONKEY_FRAME_ROWS: usize = 16; /// How many benders (grass/foliage pushers: the player and nearby units) the block carries. pub const MAX_BENDERS: usize = 8; -/// The fog model selector, `fog3.x`. +/// The fog model selector, `fog_d.x`. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum FogModel { /// Today's linear 1.12 fog, byte-identical. From ee13878f8aa6b78c6cdfb3a53064740e762e8559 Mon Sep 17 00:00:00 2001 From: pkuzic Date: Fri, 25 Sep 2026 09:20:58 +0100 Subject: [PATCH 2/6] MONKEY (fog): LightFogBand.dbc reader + LightCatalog::blend_chain benilla-formats/src/light/fog_band.rs reads our LightFogBand.dbc (the LightFloatBand record, 13 rows per LightParams: height fog, curve blend, sun-fog rgb/strength/angle, end-fog rgb/distance). Absent file = empty catalog, no error. blend_chain exposes the area-light blend order so a side table folds with the same law as the stock bands. Co-Authored-By: Claude Opus 5.5 --- crates/benilla-formats/src/lib.rs | 2 + crates/benilla-formats/src/light.rs | 63 ++++++ crates/benilla-formats/src/light/fog_band.rs | 201 +++++++++++++++++++ crates/benilla-formats/src/light/tables.rs | 10 + 4 files changed, 276 insertions(+) create mode 100644 crates/benilla-formats/src/light/fog_band.rs diff --git a/crates/benilla-formats/src/lib.rs b/crates/benilla-formats/src/lib.rs index ad87a31e..174796de 100644 --- a/crates/benilla-formats/src/lib.rs +++ b/crates/benilla-formats/src/lib.rs @@ -128,6 +128,8 @@ pub use ground_effects::{ }; mod light; pub use light::{Atmosphere, LightCatalog, Submersion, ZERO_KEY_COLOR, ZERO_KEY_SCALAR}; +// MONKEY (fog): `LightFogBand.dbc`. +pub use light::{FogBand, FogBandCatalog, FOG_BANDS_PER_PARAM}; mod loading_screen; pub use loading_screen::{load_loading_screens, LoadingScreenCatalog}; mod liquid; diff --git a/crates/benilla-formats/src/light.rs b/crates/benilla-formats/src/light.rs index 3edc847e..cb19d6de 100644 --- a/crates/benilla-formats/src/light.rs +++ b/crates/benilla-formats/src/light.rs @@ -11,9 +11,12 @@ use crate::dbc::{f32_at, parse, str_at, u32_at}; use crate::Chain; mod atmosphere; +// MONKEY (fog): `LightFogBand.dbc`, the modern fog fields. +mod fog_band; mod tables; pub use atmosphere::Atmosphere; +pub use fog_band::{FogBand, FogBandCatalog, FOG_BANDS_PER_PARAM}; use atmosphere::{ FB_CLOUD_DENSITY, FB_FOG_END, FB_FOG_START_MULT, IB_AMBIENT, IB_CLOUD_GBASE, IB_CLOUD_SLOPE, IB_CLOUD_SUN, IB_DIFFUSE, IB_FOG_COLOR, IB_OCEAN_DEEP, IB_OCEAN_SHALLOW, IB_RIVER_DEEP, @@ -509,6 +512,66 @@ impl LightCatalog { acc } + /// MONKEY (fog): the `LightParams` ids [`Self::sample_blended`] folds, in its order, each with its + /// lerp weight (the base first at 1.0). A side table keyed by `LightParams` (`LightFogBand.dbc`) + /// blends through this with the same law. Magma and slime give their fixed row alone. + pub fn blend_chain( + &self, + map: u32, + pos: [f32; 3], + stormy: bool, + submersion: Submersion, + ghost: bool, + ) -> Vec<(u32, f32)> { + if let Some(p) = submersion.fixed_param() { + if self.has_param(p) { + return vec![(p, 1.0)]; + } + } + let slot = weather_slot(ghost, stormy, submersion.is_water()); + let param_of = |l: &Light| match l.params[slot] { + 0 => l.params[SLOT_CLEAR], + p => p, + }; + let mut chain = Vec::new(); + let map_has_no_light = !self.lights.iter().any(|l| l.map == map); + if let Some(base) = self + .lights + .iter() + .find(|l| l.map == map && l.global) + .or_else(|| { + map_has_no_light + .then(|| self.lights.iter().find(|l| l.id == FALLBACK_LIGHT_ID)) + .flatten() + }) + { + let p = param_of(base); + if p >= 1 { + chain.push((p, 1.0)); + } + } + let mut locals: Vec<(f32, &Light)> = self + .lights + .iter() + .filter(|l| l.map == map && !l.global) + .filter_map(|l| { + let d = (0..3) + .map(|i| (l.pos[i] - pos[i]).powi(2)) + .sum::() + .sqrt(); + (d <= l.falloff_end).then_some((d, l)) + }) + .collect(); + locals.sort_by(|a, b| b.0.total_cmp(&a.0)); + for (dist, l) in locals { + let p = param_of(l); + if p >= 1 { + chain.push((p, blend_alpha(dist, l.falloff_start, l.falloff_end))); + } + } + chain + } + /// Debug: the spheres near `pos`, nearest first (the blend applies them farthest first), then /// the blended result against the `pick_light` sample and the water swatch endpoints. pub fn debug_blend(&self, map: u32, pos: [f32; 3], time: u32) { diff --git a/crates/benilla-formats/src/light/fog_band.rs b/crates/benilla-formats/src/light/fog_band.rs new file mode 100644 index 00000000..56aea8f5 --- /dev/null +++ b/crates/benilla-formats/src/light/fog_band.rs @@ -0,0 +1,201 @@ +//! MONKEY (fog): `LightFogBand.dbc`, the modern fog fields 1.12 has no column for. +//! +//! The file is ours (DESIGN-Benilla-Graphics §2a). It mirrors `LightFloatBand.dbc` exactly: the +//! same 34-field, 136-byte band record (`ID, num, time[16], value[16]`, times in half-minutes on +//! the 2880 day), interpolated the same way across midnight. `LightParams` id `P` keys +//! [`FOG_BANDS_PER_PARAM`] rows, band `b` at id `(P − 1)·13 + b + 1`, the `LightFloatBand` +//! indexing with its own stride, so the stock `paramID·6 + k` tables stay untouched. +//! +//! | band | field | unit | +//! |---|---|---| +//! | 0 | height-fog density | extra extinction below the plane, × the distance fog's (0 = off) | +//! | 1 | height-fog height | WoW world Z, yards | +//! | 2 | height-fog falloff | 1/yd: how fast the extra density fades above the plane | +//! | 3 | curve blend | 0 = exponential (modern), 1 = the 1.12 linear ramp | +//! | 4-6 | sun-fog colour r, g, b | gamma 0..1 | +//! | 7 | sun-fog strength | 0..1 | +//! | 8 | sun-fog angle | cosine threshold of the lobe around the sun | +//! | 9-11 | end-fog colour r, g, b | gamma 0..1 | +//! | 12 | end-fog distance | yards (0 = the scene fog end) | +//! +//! A missing file, or a `LightParams` id without rows, is not an error: the caller falls back to +//! the colours it derives from the 1.12 bands. + +use std::collections::HashMap; + +use benilla_dbc::FieldType; + +use super::tables::{band_schema, load_float_bands, sample_float, Band}; +use crate::Chain; + +const FOG_BAND: &str = "DBFilesClient\\LightFogBand.dbc"; + +/// Rows per `LightParams` id. +pub const FOG_BANDS_PER_PARAM: u32 = 13; + +/// One `LightParams` id's fog fields at one time of day (see the module table). +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FogBand { + pub height_density: f32, + /// WoW world Z, yards. + pub height: f32, + pub height_falloff: f32, + pub curve_blend: f32, + pub sun_rgb: [f32; 3], + pub sun_strength: f32, + pub sun_angle: f32, + pub end_rgb: [f32; 3], + /// Yards; 0 = the scene fog end. + pub end_distance: f32, +} + +impl FogBand { + /// Componentwise `self + (o − self)·t`, the area and storm blend. + pub fn lerp(&self, o: &FogBand, t: f32) -> FogBand { + let l = |a: f32, b: f32| a + (b - a) * t; + let l3 = |a: [f32; 3], b: [f32; 3]| [l(a[0], b[0]), l(a[1], b[1]), l(a[2], b[2])]; + FogBand { + height_density: l(self.height_density, o.height_density), + height: l(self.height, o.height), + height_falloff: l(self.height_falloff, o.height_falloff), + curve_blend: l(self.curve_blend, o.curve_blend), + sun_rgb: l3(self.sun_rgb, o.sun_rgb), + sun_strength: l(self.sun_strength, o.sun_strength), + sun_angle: l(self.sun_angle, o.sun_angle), + end_rgb: l3(self.end_rgb, o.end_rgb), + end_distance: l(self.end_distance, o.end_distance), + } + } +} + +/// The parsed `LightFogBand.dbc`; empty when the file is absent. +#[derive(Default)] +pub struct FogBandCatalog { + bands: HashMap>, +} + +impl FogBandCatalog { + /// Read `LightFogBand.dbc` off the chain. Absent or unreadable → an empty catalog (every + /// lookup `None`), which is the 1.12 data set. + pub fn load(chain: &mut Chain) -> Self { + let bands = load_float_bands( + chain, + FOG_BAND, + band_schema("LightFogBand", FieldType::Float32), + ) + .unwrap_or_default(); + FogBandCatalog { bands } + } + + /// Whether the file carried any row. + pub fn is_empty(&self) -> bool { + self.bands.is_empty() + } + + /// `LightParams` id `p` at `time` (half-minutes), or `None` when `p` has no rows. A single + /// missing band of a present id reads 0. + pub fn sample(&self, p: u32, time: u32) -> Option { + if p < 1 { + return None; + } + let base = (p - 1) * FOG_BANDS_PER_PARAM + 1; + let rows: Vec> = (0..FOG_BANDS_PER_PARAM) + .map(|b| self.bands.get(&(base + b)).and_then(|band| sample_float(band, time))) + .collect(); + if rows.iter().all(Option::is_none) { + return None; + } + let v = |b: usize| rows[b].unwrap_or(0.0); + Some(FogBand { + height_density: v(0), + height: v(1), + height_falloff: v(2), + curve_blend: v(3), + sun_rgb: [v(4), v(5), v(6)], + sun_strength: v(7), + sun_angle: v(8), + end_rgb: [v(9), v(10), v(11)], + end_distance: v(12), + }) + } + + /// The area/storm blend of [`super::LightCatalog::blend_chain`]: each id's sample, or + /// `fallback` for an id without rows, lerped in chain order. `None` when no id in the chain has + /// rows, so the caller keeps its own derivation untouched. + pub fn sample_chain( + &self, + chain: &[(u32, f32)], + time: u32, + fallback: &FogBand, + ) -> Option { + let mut any = false; + let mut acc = *fallback; + for &(p, w) in chain { + let v = match self.sample(p, time) { + Some(v) => { + any = true; + v + } + None => *fallback, + }; + acc = acc.lerp(&v, w); + } + any.then_some(acc) + } + + /// Test/tool constructor: a catalog from `(row id, [(time, value)])` bands. + pub fn from_rows(rows: &[(u32, &[(u32, f32)])]) -> Self { + let mut bands = HashMap::new(); + for (id, keys) in rows { + bands.insert(*id, Band::from_keys(keys)); + } + FogBandCatalog { bands } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn neutral() -> FogBand { + FogBand { + height_density: 0.0, + height: 0.0, + height_falloff: 0.0, + curve_blend: 0.0, + sun_rgb: [0.0; 3], + sun_strength: 0.0, + sun_angle: 1.0, + end_rgb: [0.0; 3], + end_distance: 0.0, + } + } + + #[test] + fn empty_catalog_is_absent() { + let c = FogBandCatalog::default(); + assert!(c.is_empty()); + assert_eq!(c.sample(12, 1440), None); + assert_eq!(c.sample_chain(&[(12, 1.0)], 1440, &neutral()), None); + } + + #[test] + fn rows_land_at_the_param_stride_and_interpolate() { + // Param 12, band 7 (sun strength): 0 at 06:00, 1 at 18:00. + let id = 11 * FOG_BANDS_PER_PARAM + 7 + 1; + let c = FogBandCatalog::from_rows(&[(id, &[(720, 0.0), (2160, 1.0)])]); + let noon = c.sample(12, 1440).unwrap(); + assert!((noon.sun_strength - 0.5).abs() < 1e-6); + assert_eq!(noon.height_density, 0.0); + assert_eq!(c.sample(13, 1440), None); + } + + #[test] + fn chain_blends_rows_against_the_fallback() { + let id = 11 * FOG_BANDS_PER_PARAM + 7 + 1; + let c = FogBandCatalog::from_rows(&[(id, &[(0, 1.0)])]); + // Base 13 (no rows → fallback strength 0), then param 12 at weight 0.25. + let out = c.sample_chain(&[(13, 1.0), (12, 0.25)], 100, &neutral()).unwrap(); + assert!((out.sun_strength - 0.25).abs() < 1e-6); + } +} diff --git a/crates/benilla-formats/src/light/tables.rs b/crates/benilla-formats/src/light/tables.rs index d1eecdab..a963e9b3 100644 --- a/crates/benilla-formats/src/light/tables.rs +++ b/crates/benilla-formats/src/light/tables.rs @@ -18,6 +18,16 @@ pub(super) struct Band { values: Vec, } +// MONKEY (fog): a band from `(time, value)` keys, for `fog_band`'s tests and tools. +impl Band { + pub(super) fn from_keys(keys: &[(u32, T)]) -> Self { + Band { + times: keys.iter().map(|k| k.0).collect(), + values: keys.iter().map(|k| k.1).collect(), + } + } +} + /// The shared band record: `ID, num, time[16], value[16]`, 34 fields, 136 bytes. pub(super) fn band_schema(name: &str, value: FieldType) -> Schema { let mut s = Schema::new(name); From bbea017484eaaab7a7683b7bbf22d9215d821377 Mon Sep 17 00:00:00 2001 From: pkuzic Date: Fri, 25 Sep 2026 09:20:59 +0100 Subject: [PATCH 3/6] 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> = std::sync::OnceLock::new(); + *ENV.get_or_init(|| { + std::env::var("WOW_FOGMODEL").ok().and_then(|v| match v.trim() { + "0" => Some(FogModel::Classic), + "1" => Some(FogModel::Modern), + _ => None, + }) + }) +} + +impl FogModelSetting { + /// Apply a cvar write; the env override, when set, wins for the session. + pub fn set_from_cvar(&mut self, v: f32) { + let want = env_override().unwrap_or(if v >= 0.5 { + FogModel::Modern + } else { + FogModel::Classic + }); + if self.0 != want { + self.0 = want; + } + } + + pub fn modern(&self) -> bool { + self.0 == FogModel::Modern + } +} + +/// `LightFogBand.dbc`, loaded once; empty without the file. +#[derive(Resource, Default)] +pub struct FogBandTable(pub FogBandCatalog); + +/// The reference client's farclip ceiling (`FARCLIP_RANGE` end). Up to it Modern keeps the 1.12 +/// fog pair exactly; beyond it the fog end scales with the view distance. +pub const FOG_REF_FARCLIP: f32 = 777.0; + +/// The Modern scene fog pair: the zone's end, stretched by `farclip / 777` once the view distance +/// passes the reference ceiling, and never past `farclip` (the wall). Classic is +/// `(frac·min(end, farclip), min(end, farclip))`; at `farclip ≤ 777` the two are identical. +pub fn modern_fog_end(fog_end_raw: f32, fog_start_frac: f32, farclip: f32) -> (f32, f32) { + let stretch = (farclip / FOG_REF_FARCLIP).max(1.0); + let end = (fog_end_raw * stretch).min(farclip); + (fog_start_frac * end, end) +} + +/// Sun-fog lobe defaults (no `LightFogBand` row): strength and the cosine the lobe starts at. +pub const DERIVED_SUN_FOG_STRENGTH: f32 = 0.5; +pub const DERIVED_SUN_FOG_ANGLE: f32 = 0.25; +/// How far the sun-fog colour leans from the fog colour toward the sun colour (IntBand 9). +const DERIVED_SUN_FOG_LEAN: f32 = 0.6; +/// How far the end-fog colour leans from the fog colour toward the 1.8° sky ring. +const DERIVED_END_FOG_LEAN: f32 = 0.5; + +fn mix3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] { + [ + a[0] + (b[0] - a[0]) * t, + a[1] + (b[1] - a[1]) * t, + a[2] + (b[2] - a[2]) * t, + ] +} + +/// The Modern fields from 1.12 data alone: no height fog, the exponential curve, a sun-fog lobe in +/// the sun's colour and an end colour leaning to the sky's lowest ring, ending at the scene fog end. +pub fn derived_band(l: &WowLighting) -> FogBand { + FogBand { + height_density: 0.0, + height: 0.0, + height_falloff: 0.0, + curve_blend: 0.0, + sun_rgb: mix3(l.fog_color, l.spec, DERIVED_SUN_FOG_LEAN), + sun_strength: DERIVED_SUN_FOG_STRENGTH, + sun_angle: DERIVED_SUN_FOG_ANGLE, + end_rgb: mix3(l.fog_color, l.sky[4], DERIVED_END_FOG_LEAN), + end_distance: 0.0, + } +} + +/// The sun-fog day factor from the visible sun's height (`sin(elevation)`): full from ~3° up, +/// fading through the horizon to 0 by ~6° below it (the modern client's SunAngleBlend ramp). +pub fn sun_fog_day(sun_y: f32) -> f32 { + let t = ((sun_y + 0.1) / 0.15).clamp(0.0, 1.0); + t * t * (3.0 - 2.0 * t) +} + +/// Octahedral encoding of a unit vector into `[-1, 1]²` (the shader's `oct_decode`). +pub fn oct_encode(v: Vec3) -> [f32; 2] { + let v = v.normalize_or(Vec3::Y); + let n = v / (v.x.abs() + v.y.abs() + v.z.abs()); + if n.y >= 0.0 { + [n.x, n.z] + } else { + let sx = if n.x >= 0.0 { 1.0 } else { -1.0 }; + let sz = if n.z >= 0.0 { 1.0 } else { -1.0 }; + [(1.0 - n.z.abs()) * sx, (1.0 - n.x.abs()) * sz] + } +} + +/// Write the FOG rows of `frame` for `band` under the resolved light. Classic zeroes them. +pub fn fill_frame(frame: &mut MonkeyFrame, model: FogModel, l: &WowLighting, band: &FogBand) { + if model == FogModel::Classic { + frame.fog_model = FogModel::Classic; + frame.fog_scene_end = 0.0; + frame.height_fog_density = 0.0; + frame.height_fog_height = 0.0; + frame.height_fog_falloff = 0.0; + frame.fog_curve_blend = 0.0; + frame.sun_fog_rgb = [0.0; 3]; + frame.sun_fog_strength = 0.0; + frame.sun_fog_angle = 0.0; + frame.sun_fog_dir = [0.0; 2]; + frame.end_fog_rgb = [0.0; 3]; + frame.end_fog_distance = 0.0; + return; + } + let sun = l.celestial_dir; + frame.fog_model = FogModel::Modern; + frame.fog_scene_end = l.fog_end; + frame.height_fog_density = band.height_density.max(0.0); + // WoW Z is Bevy Y. + frame.height_fog_height = band.height; + frame.height_fog_falloff = band.height_falloff.max(0.0); + frame.fog_curve_blend = band.curve_blend.clamp(0.0, 1.0); + frame.sun_fog_rgb = band.sun_rgb; + frame.sun_fog_strength = band.sun_strength.clamp(0.0, 1.0) * sun_fog_day(sun.y); + frame.sun_fog_angle = band.sun_angle.clamp(-1.0, 0.999); + frame.sun_fog_dir = oct_encode(sun); + frame.end_fog_rgb = band.end_rgb; + frame.end_fog_distance = if band.end_distance > 0.0 { + band.end_distance + } else { + l.fog_end + }; +} + +/// Startup: read `LightFogBand.dbc` (absent → empty). +pub(super) fn load_fog_bands(mut commands: Commands, world_assets: Option>) { + let Some(world_assets) = world_assets else { + return; + }; + let mut chain = world_assets.chain.lock_recover(); + let cat = FogBandCatalog::load(&mut chain); + if !cat.is_empty() { + info!("LightFogBand.dbc loaded: Modern fog uses its authored fields"); + } + commands.insert_resource(FogBandTable(cat)); +} + +/// Per frame: the Modern fog fields into [`MonkeyFrame`] (after the light resolve). +#[allow(clippy::too_many_arguments)] +pub(super) fn update_fog_model( + setting: Res, + lighting: Res, + table: Option>, + sampler: Option>, + clock: Res, + cam: Query<&GlobalTransform, With>, + current_map: Option>, + eye_liquid: crate::liquid::EyeLiquid, + viewer: Res, + weather: Option>, + mut frame: ResMut, +) { + let derived = derived_band(&lighting); + let band = match (&table, &sampler, setting.modern()) { + (Some(t), Some(s), true) if !t.0.is_empty() => { + let pos = match cam.single() { + Ok(t) => bevy_to_wow(t.translation()), + Err(_) => [SPAWN_XY.0, SPAWN_XY.1, 83.5], + }; + let map = current_map.as_ref().map_or(0, |m| m.0); + let sub = eye_liquid.submersion(); + let time = clock.minute * 2; + let storm = weather + .as_ref() + .map_or(0.0, |w| crate::weather::storm_blend(w.sky_density)); + let chain = |stormy| s.0.blend_chain(map, pos, stormy, sub, viewer.ghost); + let clear = t.0.sample_chain(&chain(false), time, &derived); + let stormy = (storm > 0.0) + .then(|| t.0.sample_chain(&chain(true), time, &derived)) + .flatten(); + match (clear, stormy) { + (None, None) => derived, + (c, s) => c.unwrap_or(derived).lerp(&s.unwrap_or(derived), storm), + } + } + _ => derived, + }; + let mut next = *frame; + fill_frame(&mut next, setting.0, &lighting, &band); + if *frame != next { + *frame = next; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn modern_end_matches_classic_up_to_the_reference_farclip() { + for far in [177.0, 350.0, 500.0, 777.0] { + for end in [120.0, 444.0, 900.0] { + let classic_end: f32 = f32::min(end, far); + let (s, e) = modern_fog_end(end, 0.25, far); + assert_eq!(e, classic_end); + assert_eq!(s, 0.25 * classic_end); + } + } + } + + #[test] + fn modern_end_grows_past_the_reference_farclip() { + let (_, e) = modern_fog_end(444.0, 0.25, 1554.0); + assert!((e - 888.0).abs() < 1e-3); + // Never past the wall. + let (_, e) = modern_fog_end(900.0, 0.25, 1000.0); + assert_eq!(e, 1000.0); + } + + #[test] + fn classic_leaves_the_fog_rows_zero() { + let mut f = MonkeyFrame::default(); + let l = WowLighting::default(); + fill_frame(&mut f, FogModel::Classic, &l, &derived_band(&l)); + assert_eq!(f, MonkeyFrame::default()); + assert!(f.pack(0.0, 0.0)[..4].iter().flatten().all(|v| *v == 0.0)); + } + + #[test] + fn modern_marks_the_scene_end_and_fades_the_sun_at_night() { + let mut l = WowLighting::default(); + l.fog_end = 444.0; + l.celestial_dir = Vec3::new(0.0, 0.8, 0.6); + let mut f = MonkeyFrame::default(); + fill_frame(&mut f, FogModel::Modern, &l, &derived_band(&l)); + assert_eq!(f.fog_scene_end, 444.0); + assert_eq!(f.end_fog_distance, 444.0); + assert!((f.sun_fog_strength - DERIVED_SUN_FOG_STRENGTH).abs() < 1e-6); + l.celestial_dir = Vec3::new(0.0, -0.5, 0.86); + fill_frame(&mut f, FogModel::Modern, &l, &derived_band(&l)); + assert_eq!(f.sun_fog_strength, 0.0); + } + + #[test] + fn oct_encoding_round_trips() { + fn decode(e: [f32; 2]) -> Vec3 { + let mut v = Vec3::new(e[0], 1.0 - e[0].abs() - e[1].abs(), e[1]); + let t = (-v.y).max(0.0); + v.x += if v.x >= 0.0 { -t } else { t }; + v.z += if v.z >= 0.0 { -t } else { t }; + v.normalize() + } + for d in [ + Vec3::Y, + -Vec3::Y, + Vec3::new(0.3, 0.2, -0.9), + Vec3::new(-0.7, -0.1, 0.7), + Vec3::new(0.1, -0.9, -0.4), + ] { + let d = d.normalize(); + assert!(decode(oct_encode(d)).dot(d) > 0.9999, "{d:?}"); + } + } +} diff --git a/crates/benilla-world/src/lighting/mod.rs b/crates/benilla-world/src/lighting/mod.rs index 81f80ad3..9fe4d4bd 100644 --- a/crates/benilla-world/src/lighting/mod.rs +++ b/crates/benilla-world/src/lighting/mod.rs @@ -13,6 +13,8 @@ mod flicker; // MONKEY (flame flicker): the per-light fire wobble folded in at p mod global_light; // the one shared global-light storage buffer (replaces the per-material push) mod monkey_frame; // MONKEY (p0 MonkeyFrame): the programme's per-frame block after the point table pub use monkey_frame::{FogModel, MonkeyFrame, MAX_BENDERS, MONKEY_FRAME_ROWS}; +pub mod fog_model; // MONKEY (fog): the Modern fog model's CPU half (MonkeyFrame fog rows) +pub use fog_model::FogModelSetting; mod lava_light; // MONKEY (lava light): magma surface fixtures and their independent gain pub use lava_light::{LavaLight, LavaLightGain}; mod prop_probes; // the per-instance interior-prop SH probe table (slot ↔ MeshTag payload) @@ -318,6 +320,16 @@ impl Plugin for LightingPlugin { // This frame's submersion verdict, which the sky-pass suppression also reads. .after(crate::liquid::SubmersionVerdict), ); + // MONKEY (fog): the fog-model setting, LightFogBand.dbc and the per-frame fog rows. + app.init_resource::() + .init_resource::() + .add_systems(Startup, fog_model::load_fog_bands.after(AssetSet::Open)) + .add_systems( + Update, + fog_model::update_fog_model + .after(update_time_lighting) + .in_set(LightingResolveSet), + ); // The shared light buffer, packed after the resolve and uploaded in the render world. global_light::register(app); // MONKEY (daylight fixtures): the per-frame re-aim, ordered before the packer's own set. diff --git a/crates/benilla-world/src/lighting/monkey_frame.rs b/crates/benilla-world/src/lighting/monkey_frame.rs index c583ec98..363058ef 100644 --- a/crates/benilla-world/src/lighting/monkey_frame.rs +++ b/crates/benilla-world/src/lighting/monkey_frame.rs @@ -17,7 +17,7 @@ //! | 0 `fog_a` | height_fog_density, height_fog_height, height_fog_falloff | curve_blend | //! | 1 `fog_b` | sun_fog_rgb | sun_fog_strength | //! | 2 `fog_c` | end_fog_rgb | end_fog_distance | -//! | 3 `fog_d` | fog_model (0 classic, 1 modern), sun_fog_angle, 0 | 0 | +//! | 3 `fog_d` | fog_model (0 classic; Modern = the scene fog end, yd, ≥ 1), sun_fog_angle, sun dir oct.x | sun dir oct.y | //! | 4 `wind_a` | dir_x, dir_y, speed | gust | //! | 5 `wind_b` | time_s, sway_strength, grass_strength | tree_strength | //! | 6 `wet_a` | rain_rate, wetness, ripple_time_s | snow | @@ -64,6 +64,11 @@ pub struct MonkeyFrame { pub sun_fog_strength: f32, /// Cosine threshold of the sun-fog lobe. pub sun_fog_angle: f32, + /// MONKEY (fog): the sun direction (Bevy space), octahedral-encoded (`fog_model::oct_encode`). + pub sun_fog_dir: [f32; 2], + /// MONKEY (fog): the scene fog end (yd) the Modern law applies to; packed as `fog_d.x`, so an + /// interior WMO span (a different end) stays classic. + pub fog_scene_end: f32, /// Gamma 0..1. pub end_fog_rgb: [f32; 3], pub end_fog_distance: f32, @@ -94,9 +99,10 @@ impl MonkeyFrame { /// The 16 packed rows. `time_of_day` (0..1) and `night` (0..1) are the packer's own inputs. pub fn pack(&self, time_of_day: f32, night: f32) -> [[f32; 4]; MONKEY_FRAME_ROWS] { let mut rows = [[0.0f32; 4]; MONKEY_FRAME_ROWS]; + // MONKEY (fog): Modern packs the scene fog end (never below 1) as its flag. let model = match self.fog_model { FogModel::Classic => 0.0, - FogModel::Modern => 1.0, + FogModel::Modern => self.fog_scene_end.max(1.0), }; rows[0] = [ self.height_fog_density, @@ -108,7 +114,7 @@ impl MonkeyFrame { rows[1] = [s[0], s[1], s[2], self.sun_fog_strength]; let e = self.end_fog_rgb; rows[2] = [e[0], e[1], e[2], self.end_fog_distance]; - rows[3] = [model, self.sun_fog_angle, 0.0, 0.0]; + rows[3] = [model, self.sun_fog_angle, self.sun_fog_dir[0], self.sun_fog_dir[1]]; rows[4] = [self.wind_dir[0], self.wind_dir[1], self.wind_speed, self.wind_gust]; rows[5] = [self.wind_time_s, self.sway_strength, self.grass_strength, self.tree_strength]; rows[6] = [self.rain_rate, self.wetness, self.ripple_time_s, self.snow]; @@ -144,6 +150,8 @@ mod tests { end_fog_rgb: [9.0, 10.0, 11.0], end_fog_distance: 12.0, sun_fog_angle: 13.0, + sun_fog_dir: [0.5, -0.5], + fog_scene_end: 444.0, wind_dir: [14.0, 15.0], wind_speed: 16.0, wind_gust: 17.0, @@ -162,7 +170,7 @@ mod tests { assert_eq!(r[0], [1.0, 2.0, 3.0, 4.0]); assert_eq!(r[1], [5.0, 6.0, 7.0, 8.0]); assert_eq!(r[2], [9.0, 10.0, 11.0, 12.0]); - assert_eq!(r[3], [1.0, 13.0, 0.0, 0.0]); + assert_eq!(r[3], [444.0, 13.0, 0.5, -0.5]); assert_eq!(r[4], [14.0, 15.0, 16.0, 17.0]); assert_eq!(r[5], [18.0, 19.0, 20.0, 21.0]); assert_eq!(r[6], [22.0, 23.0, 24.0, 25.0]); diff --git a/crates/benilla-world/src/lighting/resolve.rs b/crates/benilla-world/src/lighting/resolve.rs index 72e95217..e1749d54 100644 --- a/crates/benilla-world/src/lighting/resolve.rs +++ b/crates/benilla-world/src/lighting/resolve.rs @@ -177,7 +177,9 @@ pub(super) fn update_time_lighting( eye_liquid: crate::liquid::EyeLiquid, viewer: Res, weather: Option>, - view: Res, + // MONKEY (fog): paired with the fog-model setting (Modern decouples the fog end from farclip + // past the reference ceiling); a tuple keeps the system under Bevy's 16-parameter limit. + (view, fog_setting): (Res, Res), time: Res