From f62a9eae5b16b518621fcd1452eef4c9b9b140d4 Mon Sep 17 00:00:00 2001
From: pkuzic
Date: Fri, 25 Sep 2026 01:21:19 +0100
Subject: [PATCH] MONKEY (p0): MonkeyFrame block appended to the shared light
buffer
16 vec4 rows (256 B) after the point-light table, so no earlier offset moves
(per-frame blob 8528 -> 8784 B, size test updated). Rust side: the
`lighting::MonkeyFrame` resource any system may write, packed each frame by
`global_light::pack_monkey_frame` (which also fills time_of_day / night).
WGSL side: `benilla::monkey_frame::MonkeyFrame`, declared as `monkey` after
`points` in every WowLight mirror (terrain, wow_model, static_gx, liquid,
enhanced_water, wow_effect, wdl). All zero = no visual change. Layout in
LIGHTING.md "Invariants".
Co-Authored-By: Claude Opus 5.5
---
LIGHTING.md | 24 ++-
crates/benilla-assets/src/materials.rs | 3 +
.../src/shaders/enhanced_water.wgsl | 4 +
crates/benilla-assets/src/shaders/liquid.wgsl | 6 +
.../src/shaders/monkey_frame.wgsl | 21 ++
.../benilla-assets/src/shaders/terrain.wgsl | 4 +
crates/benilla-assets/src/shaders/wdl.wgsl | 8 +
.../benilla-assets/src/shaders/wow_model.wgsl | 4 +
.../src/lighting/global_light.rs | 28 ++-
crates/benilla-world/src/lighting/mod.rs | 2 +
.../src/lighting/monkey_frame.rs | 190 ++++++++++++++++++
.../benilla-world/src/shaders/static_gx.wgsl | 4 +
.../benilla-world/src/shaders/wow_effect.wgsl | 7 +
13 files changed, 301 insertions(+), 4 deletions(-)
create mode 100644 crates/benilla-assets/src/shaders/monkey_frame.wgsl
create mode 100644 crates/benilla-world/src/lighting/monkey_frame.rs
diff --git a/LIGHTING.md b/LIGHTING.md
index 2122d3df..59e0f2f1 100644
--- a/LIGHTING.md
+++ b/LIGHTING.md
@@ -52,8 +52,28 @@ is its own module, documented in `WATER.md`.
## Invariants worth knowing before editing
-- The shared light buffer stays 8528 bytes and the torch table 6416 bytes; both are mirrored in
- three shaders and pinned by tests.
+- The shared light buffer's per-frame blob is 8784 bytes and the torch table 6416 bytes; both are
+ mirrored in the shaders and pinned by tests. The blob is 21 header rows (336 B), the 256-slot
+ point-light table (8192 B), then the MONKEY **MonkeyFrame** block (16 rows, 256 B; nothing before
+ it moves). Every `WowLight` mirror (terrain, wow_model, static_gx, liquid, enhanced_water,
+ wow_effect, wdl) declares it as `monkey: monkey_frame::MonkeyFrame` after `points`:
+
+ | 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 |
+ | `misc` | bender_count | time_of_day 0..1 | night 0..1 | 0 |
+ | `benders[8]` | world x | world y | world z | radius |
+
+ Positions are Bevy world space (Y up, yards; WoW `(x, y, z)` = Bevy `(-y, z, -x)`), the wind
+ direction is a unit vector in world XZ. Lanes write the `MonkeyFrame` resource
+ (`benilla-world/src/lighting/monkey_frame.rs`); `global_light::pack_monkey_frame` packs it after
+ the point table and fills `time_of_day` / `night` itself. All zero = no visual change.
- World lights use upstream's `WorldPointLight`, never Bevy's `PointLight`.
- WGSL only fails at pipeline creation, so a shader edit is verified by running a world scene, not
by `cargo check`.
diff --git a/crates/benilla-assets/src/materials.rs b/crates/benilla-assets/src/materials.rs
index 18aa1682..08878c86 100644
--- a/crates/benilla-assets/src/materials.rs
+++ b/crates/benilla-assets/src/materials.rs
@@ -29,6 +29,9 @@ pub fn register_shaders(app: &mut App) {
// (loaded eagerly so `#import benilla::shadow_hook` resolves in terrain/model/static_gx). This is
// the ONE place the realtime directional-shadow term lives; the receivers just call into it.
load_shader_library!(app, "shaders/shadow_hook.wgsl");
+ // MONKEY (p0 MonkeyFrame): `benilla::monkey_frame`, the struct every `WowLight` mirror declares
+ // after its point table (LIGHTING.md "Invariants").
+ load_shader_library!(app, "shaders/monkey_frame.wgsl");
// MONKEY (enhanced water): the optional water module, `benilla::enhanced_water` (WATER.md).
load_shader_library!(app, "shaders/enhanced_water.wgsl");
}
diff --git a/crates/benilla-assets/src/shaders/enhanced_water.wgsl b/crates/benilla-assets/src/shaders/enhanced_water.wgsl
index 667e7459..ac4a27c2 100644
--- a/crates/benilla-assets/src/shaders/enhanced_water.wgsl
+++ b/crates/benilla-assets/src/shaders/enhanced_water.wgsl
@@ -20,6 +20,8 @@
// 106 the opaque scene COLOUR, copied beside the depth: what the refraction looks through
#import bevy_pbr::mesh_view_bindings::{view, globals}
+// MONKEY (p0 MonkeyFrame): the programme block's struct, mirrored after the point table.
+#import benilla::monkey_frame
@group(#{MATERIAL_BIND_GROUP}) @binding(103) var scene_depth: texture_2d;
@group(#{MATERIAL_BIND_GROUP}) @binding(106) var scene_colour: texture_2d;
@@ -55,6 +57,8 @@ struct WaterLight {
wmo_fog_params: vec4, // 19
point_count: vec4, // 20 live count + point-light controls
points: array, 512>, // 21+ position/range, colour/lane pairs
+ // MONKEY (p0 MonkeyFrame): the programme block after the point table (monkey_frame.wgsl).
+ monkey: monkey_frame::MonkeyFrame,
};
@group(#{MATERIAL_BIND_GROUP}) @binding(105) var water_light: WaterLight;
diff --git a/crates/benilla-assets/src/shaders/liquid.wgsl b/crates/benilla-assets/src/shaders/liquid.wgsl
index 1a9d72e7..293cd96f 100644
--- a/crates/benilla-assets/src/shaders/liquid.wgsl
+++ b/crates/benilla-assets/src/shaders/liquid.wgsl
@@ -30,6 +30,8 @@
}
// MONKEY (enhanced water): the optional water module - see enhanced_water.wgsl and WATER.md.
#import benilla::enhanced_water::{water_active, water_swell, enhanced_water, WaterFragment}
+// MONKEY (p0 MonkeyFrame): the programme block's struct, mirrored after the point table.
+#import benilla::monkey_frame
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var frames: texture_2d_array;
@group(#{MATERIAL_BIND_GROUP}) @binding(101) var frames_samp: sampler;
@@ -60,6 +62,10 @@ struct WowLight {
_grade: vec4, // 17
wmo_fog_color: vec4, // 18 rgb = interior fog (block 2); w = enable
wmo_fog_params: vec4, // 19 x = start yd; y = end yd
+ _point_count: vec4, // 20 point-light count, unread here
+ _points: array, 512>, // 21+ point-light table, unread here
+ // MONKEY (p0 MonkeyFrame): the programme block after the point table (monkey_frame.wgsl).
+ monkey: monkey_frame::MonkeyFrame,
};
@group(#{MATERIAL_BIND_GROUP}) @binding(90) var wow_light: WowLight;
diff --git a/crates/benilla-assets/src/shaders/monkey_frame.wgsl b/crates/benilla-assets/src/shaders/monkey_frame.wgsl
new file mode 100644
index 00000000..ea1780b3
--- /dev/null
+++ b/crates/benilla-assets/src/shaders/monkey_frame.wgsl
@@ -0,0 +1,21 @@
+#define_import_path benilla::monkey_frame
+
+// MONKEY (p0 MonkeyFrame): the graphics programme's per-frame block, 16 rows (256 B) appended to
+// the shared light buffer right after the point-light table (`lighting::global_light`, packed from
+// the `MonkeyFrame` resource in `lighting/monkey_frame.rs`). Every receiver's `WowLight` mirror
+// declares it as `monkey: monkey_frame::MonkeyFrame` after `points`, so the offsets of everything
+// before it never move. All zero = no visual change: every reader treats zero as "feature off".
+//
+// 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`).
+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
+ 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-assets/src/shaders/terrain.wgsl b/crates/benilla-assets/src/shaders/terrain.wgsl
index dbbd71d5..c5394a0b 100644
--- a/crates/benilla-assets/src/shaders/terrain.wgsl
+++ b/crates/benilla-assets/src/shaders/terrain.wgsl
@@ -18,6 +18,8 @@
}
// MONKEY (shadow hook): the realtime directional-shadow term (fetch + edge/night fade) lives here.
#import benilla::shadow_hook
+// MONKEY (p0 MonkeyFrame): the programme block's struct, mirrored after the point table.
+#import benilla::monkey_frame
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var layer_array: texture_2d_array;
@group(#{MATERIAL_BIND_GROUP}) @binding(104) var alpha_array: texture_2d_array;
@@ -56,6 +58,8 @@ struct WowLight {
// reach in yards) for an INTERIOR one. Terrain consumes only the exterior half.
point_count: vec4,
points: array, 512>,
+ // MONKEY (p0 MonkeyFrame): the programme block after the point table (monkey_frame.wgsl).
+ monkey: monkey_frame::MonkeyFrame,
};
@group(#{MATERIAL_BIND_GROUP}) @binding(90) var wow_light: WowLight;
diff --git a/crates/benilla-assets/src/shaders/wdl.wgsl b/crates/benilla-assets/src/shaders/wdl.wgsl
index 69d4e93b..ea85aa3e 100644
--- a/crates/benilla-assets/src/shaders/wdl.wgsl
+++ b/crates/benilla-assets/src/shaders/wdl.wgsl
@@ -17,6 +17,9 @@
mesh_view_bindings::view,
}
+// MONKEY (p0 MonkeyFrame): the programme block's struct, mirrored after the point table.
+#import benilla::monkey_frame
+
/// How far the band reaches inside the far-clip wall: the reference's far-band near plane
/// `farclip - 33.0` (`[0x8101b0]`, about one WDL outer cell).
const WDL_OVERLAP: f32 = 33.0;
@@ -34,6 +37,11 @@ struct WowLight {
_light_spec: vec4, // 3
fog_color: vec4, // 4 rgb = Light.dbc row 7 (gamma 0..1); w = enable (>0.5 ⇒ blend)
fog_params: vec4, // 5 x/y = the SCENE fog start/end yd (unread here — the hull has its own pair); z = the signed directional-shadow weight (MONKEY moon shadows: +sun / -moon; unread here); w = farclip wall (0 ⇒ off)
+ // MONKEY (p0 MonkeyFrame): rows 6-20 and the point table, unread here, so the block lines up.
+ _rows_6_20: array, 15>,
+ _points: array, 512>,
+ // MONKEY (p0 MonkeyFrame): the programme block after the point table (monkey_frame.wgsl).
+ monkey: monkey_frame::MonkeyFrame,
};
@group(#{MATERIAL_BIND_GROUP}) @binding(90) var w: WowLight;
diff --git a/crates/benilla-assets/src/shaders/wow_model.wgsl b/crates/benilla-assets/src/shaders/wow_model.wgsl
index 07705ba5..0f211ca6 100644
--- a/crates/benilla-assets/src/shaders/wow_model.wgsl
+++ b/crates/benilla-assets/src/shaders/wow_model.wgsl
@@ -20,6 +20,8 @@
}
// MONKEY (shadow hook): the realtime directional-shadow term (fetch + edge/night fade) lives here.
#import benilla::shadow_hook
+// MONKEY (p0 MonkeyFrame): the programme block's struct, mirrored after the point table.
+#import benilla::monkey_frame
// bevy_pbr 0.18.1's `forward_io::FragmentOutput`. No depth output: a fragment depth write costs
// the pipeline early-Z, so the sky lane pins its depth in the vertex stage.
@@ -112,6 +114,8 @@ struct WowLight {
// packed reach is always ≥ 1 yd and can never be mistaken for the exterior 0.
point_count: vec4,
points: array, 512>,
+ // MONKEY (p0 MonkeyFrame): the programme block after the point table (monkey_frame.wgsl).
+ monkey: monkey_frame::MonkeyFrame,
// The interior-prop SH probes, 7 rows per slot (`MAX_PROP_PROBES` = 8192 slots). Only this
// shader declares this tail; the other shaders bind the same buffer by its prefix.
prop_probes: array, 57344>,
diff --git a/crates/benilla-world/src/lighting/global_light.rs b/crates/benilla-world/src/lighting/global_light.rs
index b3373de0..06d5d90b 100644
--- a/crates/benilla-world/src/lighting/global_light.rs
+++ b/crates/benilla-world/src/lighting/global_light.rs
@@ -66,6 +66,9 @@ use crate::view::WorldCamera;
struct LightStd430 {
rows: [[f32; 4]; LIGHT_HEADER_ROWS],
points: [[f32; 4]; 2 * MAX_POINT_LIGHTS],
+ // MONKEY (p0 MonkeyFrame): the programme block, appended AFTER the point table so no earlier
+ // offset moves (8528 -> 8784 B); packed by [`pack_monkey_frame`] from [`super::MonkeyFrame`].
+ monkey: [[f32; 4]; super::monkey_frame::MONKEY_FRAME_ROWS],
}
/// Header row count of the layout above (rows 0..=20), which every light blob sizes against.
@@ -615,6 +618,7 @@ impl Default for WowLightData {
Self(LightStd430 {
rows: [[0.0; 4]; 21],
points: [[0.0; 4]; 2 * MAX_POINT_LIGHTS],
+ monkey: [[0.0; 4]; super::monkey_frame::MONKEY_FRAME_ROWS],
})
}
}
@@ -644,6 +648,8 @@ pub(super) fn register(app: &mut App) {
// MONKEY (spellLightGain): and the one on spell-effect lights, which overrides it.
.init_resource::()
.init_resource::()
+ // MONKEY (p0 MonkeyFrame): the per-frame programme parameters any system may write.
+ .init_resource::()
.add_plugins(ExtractResourcePlugin::::default())
.add_plugins(ExtractResourcePlugin::::default())
.add_plugins(ExtractResourcePlugin::::default())
@@ -656,7 +662,8 @@ pub(super) fn register(app: &mut App) {
// a light that has stood for a frame is packed on this frame's verdict. Its writes go
// through `Commands`, so a NEWLY spawned light is still packed on the fail-safe
// fallback for one frame — see that fallback's note in `build_light_data`.
- (classify_light_lanes, build_light_data)
+ // MONKEY (p0 MonkeyFrame): the programme block is packed right after the table.
+ (classify_light_lanes, build_light_data, pack_monkey_frame)
.chain()
.after(bevy::transform::TransformSystems::Propagate)
.after(super::update_time_lighting),
@@ -699,6 +706,22 @@ pub(super) fn per_frame_blob_bytes() -> u64 {
std::mem::size_of::() as u64
}
+/// MONKEY (p0 MonkeyFrame): copies [`super::MonkeyFrame`] into the block after the point table,
+/// plus the two clock fields the packer owns (`misc.y` time of day 0..1, `misc.z` night 0..1, the
+/// same dusk ramp `nightGain` uses). Writes through `ResMut` only when a row moved.
+fn pack_monkey_frame(
+ frame: Res,
+ clock: Res,
+ light: Res,
+ mut data: ResMut,
+) {
+ let night = 1.0 - sun_shadow_strength(light.celestial_dir.y);
+ let packed = frame.pack(clock.minute as f32 / 1440.0, night);
+ if data.0.monkey != packed {
+ data.0.monkey = packed;
+ }
+}
+
/// Pack the resolved [`WowLighting`] (+ the global fog-disable toggle and the view farclip) into the
/// std430 blob. The `.w` lanes carry the faithful invariants the shaders expect (Mod2x 1.0, clamp on,
/// terrain shininess 20, fog-enable, farclip wall); the model SH coeffs and both water swatches are
@@ -2626,7 +2649,8 @@ mod tests {
assert_eq!(MAX_LIVE_POINT_LIGHTS, 255, "255 is EXT_SEL_EMPTY in the three shaders");
assert_eq!(MAX_LIVE_POINT_LIGHTS, MAX_POINT_LIGHTS - 1);
// 21 header rows + 2 x 256 point rows, 16 B each.
- assert_eq!(per_frame_blob_bytes(), 8528, "the mirrored blob must not change size");
+ // MONKEY (p0 MonkeyFrame): 8528 + the 256-byte programme block.
+ assert_eq!(per_frame_blob_bytes(), 8784, "the mirrored blob must not change size");
}
/// MONKEY (enclosed day floor): `interiorDaylight` rides the FRACTION of the interior lane's
diff --git a/crates/benilla-world/src/lighting/mod.rs b/crates/benilla-world/src/lighting/mod.rs
index 027a4ce0..81f80ad3 100644
--- a/crates/benilla-world/src/lighting/mod.rs
+++ b/crates/benilla-world/src/lighting/mod.rs
@@ -11,6 +11,8 @@ mod daylight; // MONKEY (daylight fixtures): the sun as an interior-lane light i
mod daynight; // the two sun directions + day/night interp + the dawn/dusk warp curve
mod flicker; // MONKEY (flame flicker): the per-light fire wobble folded in at pack time
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};
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)
diff --git a/crates/benilla-world/src/lighting/monkey_frame.rs b/crates/benilla-world/src/lighting/monkey_frame.rs
new file mode 100644
index 00000000..3ca56f7d
--- /dev/null
+++ b/crates/benilla-world/src/lighting/monkey_frame.rs
@@ -0,0 +1,190 @@
+//! MONKEY (p0 MonkeyFrame): the graphics programme's per-frame parameter block.
+//!
+//! One resource, [`MonkeyFrame`], that any system may write (fog model, wind, wetness, benders).
+//! The light packer (`global_light::pack_monkey_frame`) copies it every frame into 16 rows
+//! appended to the shared light buffer right after the point-light table, so the offsets of every
+//! row before it never move. The WGSL mirror is `benilla::monkey_frame::MonkeyFrame`
+//! (`benilla-assets/src/shaders/monkey_frame.wgsl`); every receiver's `WowLight` struct declares
+//! it as its `monkey` member.
+//!
+//! All zero (the default) is "every feature off": nothing reads a zero field as a change, so the
+//! image is the same as before the block existed.
+//!
+//! Layout (16 × `vec4`, 256 B):
+//!
+//! | 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 |
+//! | 7 `misc` | bender_count, time_of_day 0..1, night 0..1 | 0 |
+//! | 8-15 `benders` | world x, y, z | radius |
+//!
+//! Coordinates are Bevy world space (Y up, 1 unit = 1 yd), the space the receivers'
+//! `world_position` is in: WoW `(x, y, z)` → Bevy `(-y, z, -x)` ([`benilla_assets::coords`]).
+//! The wind direction is a unit vector in the world XZ plane: `dir_x` = world x, `dir_y` = world z.
+//! `time_of_day` and `night` are filled by the packer from the game clock and the celestial sun;
+//! every other field belongs to the lane that owns the feature.
+
+use bevy::prelude::*;
+
+/// Rows in the appended block.
+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`.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub enum FogModel {
+ /// Today's linear 1.12 fog, byte-identical.
+ #[default]
+ Classic,
+ /// The FOG lane's modern model (height / sun / end-colour / curve fog).
+ Modern,
+}
+
+/// The per-frame programme parameters. Write the fields your lane owns through
+/// `ResMut`; leave the rest alone. See the module docs for the packed layout.
+#[derive(Resource, Clone, Copy, Debug, Default, PartialEq)]
+pub struct MonkeyFrame {
+ // ── FOG lane (rows 0-3) ──
+ pub fog_model: FogModel,
+ pub height_fog_density: f32,
+ /// World Y (Bevy space, yd) of the height-fog plane.
+ pub height_fog_height: f32,
+ pub height_fog_falloff: f32,
+ pub fog_curve_blend: f32,
+ /// Gamma 0..1.
+ pub sun_fog_rgb: [f32; 3],
+ pub sun_fog_strength: f32,
+ /// Cosine threshold of the sun-fog lobe.
+ pub sun_fog_angle: f32,
+ /// Gamma 0..1.
+ pub end_fog_rgb: [f32; 3],
+ pub end_fog_distance: f32,
+ // ── WIND lane (rows 4-5) ──
+ /// Unit vector in the world XZ plane (`[x, z]`).
+ pub wind_dir: [f32; 2],
+ /// Yards per second.
+ pub wind_speed: f32,
+ pub wind_gust: f32,
+ /// The wind clock in seconds (the lane owns it, so a capture can pin it).
+ pub wind_time_s: f32,
+ pub sway_strength: f32,
+ pub grass_strength: f32,
+ pub tree_strength: f32,
+ // ── WET lane (row 6) ──
+ pub rain_rate: f32,
+ pub wetness: f32,
+ pub ripple_time_s: f32,
+ pub snow: f32,
+ // ── benders (rows 7.x, 8-15) ──
+ /// Bender slots; only the first `min(bender_count, MAX_BENDERS)` are packed. `[x, y, z, radius]` in
+ /// Bevy world space.
+ pub benders: [[f32; 4]; MAX_BENDERS],
+ pub bender_count: u32,
+}
+
+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];
+ let model = match self.fog_model {
+ FogModel::Classic => 0.0,
+ FogModel::Modern => 1.0,
+ };
+ rows[0] = [
+ self.height_fog_density,
+ self.height_fog_height,
+ self.height_fog_falloff,
+ self.fog_curve_blend,
+ ];
+ let s = self.sun_fog_rgb;
+ 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[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];
+ let n = (self.bender_count as usize).min(MAX_BENDERS);
+ rows[7] = [n as f32, time_of_day, night, 0.0];
+ rows[8..8 + n].copy_from_slice(&self.benders[..n]);
+ rows
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn the_default_frame_packs_to_zero_but_the_clock() {
+ let rows = MonkeyFrame::default().pack(0.0, 0.0);
+ assert!(rows.iter().flatten().all(|v| *v == 0.0));
+ let rows = MonkeyFrame::default().pack(0.5, 1.0);
+ assert_eq!(rows[7], [0.0, 0.5, 1.0, 0.0]);
+ }
+
+ #[test]
+ fn fields_land_in_their_documented_rows() {
+ let f = MonkeyFrame {
+ fog_model: FogModel::Modern,
+ height_fog_density: 1.0,
+ height_fog_height: 2.0,
+ height_fog_falloff: 3.0,
+ fog_curve_blend: 4.0,
+ sun_fog_rgb: [5.0, 6.0, 7.0],
+ sun_fog_strength: 8.0,
+ end_fog_rgb: [9.0, 10.0, 11.0],
+ end_fog_distance: 12.0,
+ sun_fog_angle: 13.0,
+ wind_dir: [14.0, 15.0],
+ wind_speed: 16.0,
+ wind_gust: 17.0,
+ wind_time_s: 18.0,
+ sway_strength: 19.0,
+ grass_strength: 20.0,
+ tree_strength: 21.0,
+ rain_rate: 22.0,
+ wetness: 23.0,
+ ripple_time_s: 24.0,
+ snow: 25.0,
+ benders: [[26.0, 27.0, 28.0, 29.0]; MAX_BENDERS],
+ bender_count: 20, // clamped to MAX_BENDERS
+ };
+ let r = f.pack(0.25, 0.75);
+ 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[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]);
+ assert_eq!(r[7], [8.0, 0.25, 0.75, 0.0]);
+ assert_eq!(r[15], [26.0, 27.0, 28.0, 29.0]);
+ }
+
+ /// The WGSL mirror declares the same 16 rows; a member added on one side only would shift
+ /// every bender.
+ #[test]
+ fn the_wgsl_mirror_has_sixteen_rows() {
+ let src = include_str!("../../../benilla-assets/src/shaders/monkey_frame.wgsl");
+ let body = src.split_once("struct MonkeyFrame {").unwrap().1;
+ let body = body.split_once("\n}").unwrap().0;
+ let mut rows = 0;
+ for line in body.lines().map(str::trim).filter(|l| !l.is_empty() && !l.starts_with("//")) {
+ if line.contains("array, 8>") {
+ rows += 8;
+ } else if line.contains("vec4") {
+ rows += 1;
+ }
+ }
+ assert_eq!(rows, MONKEY_FRAME_ROWS);
+ }
+}
diff --git a/crates/benilla-world/src/shaders/static_gx.wgsl b/crates/benilla-world/src/shaders/static_gx.wgsl
index ebe78d5a..0850dc75 100644
--- a/crates/benilla-world/src/shaders/static_gx.wgsl
+++ b/crates/benilla-world/src/shaders/static_gx.wgsl
@@ -10,6 +10,8 @@
}
// MONKEY (shadow hook): the realtime directional-shadow term (fetch + edge/night fade) lives here.
#import benilla::shadow_hook
+// MONKEY (p0 MonkeyFrame): the programme block's struct, mirrored after the point table.
+#import benilla::monkey_frame
// Group 0 is Bevy's standard mesh-view bind group (view matrices, directional-light records and
// the shadow textures the retained pass reads).
@@ -40,6 +42,8 @@ struct WowLight {
// yards. wow_model.wgsl owns the full note.
point_count: vec4,
points: array, 512>,
+ // MONKEY (p0 MonkeyFrame): the programme block after the point table (monkey_frame.wgsl).
+ monkey: monkey_frame::MonkeyFrame,
// lighting::prop_probes: 8192 slots of 7 rows; the buffer's later regions are not mirrored.
prop_probes: array, 57344>,
}
diff --git a/crates/benilla-world/src/shaders/wow_effect.wgsl b/crates/benilla-world/src/shaders/wow_effect.wgsl
index f6654dd9..e3921b8f 100644
--- a/crates/benilla-world/src/shaders/wow_effect.wgsl
+++ b/crates/benilla-world/src/shaders/wow_effect.wgsl
@@ -12,6 +12,8 @@
// - BLEND_MOD2X: (rgb, 1) under (Dst, Src) = `2·src·dst`, rain's state; reads no alpha.
#import bevy_render::view::View
+// MONKEY (p0 MonkeyFrame): the programme block's struct, mirrored after the point table.
+#import benilla::monkey_frame
// Prefix of `lighting::global_light`'s buffer; keep in sync with wow_model.wgsl's copy.
struct WowLight {
@@ -33,6 +35,11 @@ struct WowLight {
sh_c16: vec4,
_water: array, 4>,
grade: vec4,
+ // MONKEY (p0 MonkeyFrame): rows 18-20 and the point table, unread here, so the block lines up.
+ _rows_18_20: array, 3>,
+ _points: array, 512>,
+ // MONKEY (p0 MonkeyFrame): the programme block after the point table (monkey_frame.wgsl).
+ monkey: monkey_frame::MonkeyFrame,
};
@group(0) @binding(0) var view: View;