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