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;