From dff6556867bd4e1342bccbc5edd73f7567525d7c Mon Sep 17 00:00:00 2001 From: pkuzic Date: Fri, 25 Sep 2026 08:37:39 +0100 Subject: [PATCH 1/4] 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 | 14 +++++++------- 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, 34 insertions(+), 32 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 924d5f88..39d774e1 100644 --- a/crates/benilla-assets/src/shaders/enhanced_water.wgsl +++ b/crates/benilla-assets/src/shaders/enhanced_water.wgsl @@ -54,13 +54,13 @@ struct WaterParams { const MONKEY_ROW: u32 = 533u; fn water_monkey() -> monkey_frame::MonkeyFrame { var m: monkey_frame::MonkeyFrame; - m.fog0 = water_light[MONKEY_ROW + 0u]; - m.fog1 = water_light[MONKEY_ROW + 1u]; - m.fog2 = water_light[MONKEY_ROW + 2u]; - m.fog3 = water_light[MONKEY_ROW + 3u]; - m.wind0 = water_light[MONKEY_ROW + 4u]; - m.wind1 = water_light[MONKEY_ROW + 5u]; - m.wet0 = water_light[MONKEY_ROW + 6u]; + m.fog_a = water_light[MONKEY_ROW + 0u]; + m.fog_b = water_light[MONKEY_ROW + 1u]; + m.fog_c = water_light[MONKEY_ROW + 2u]; + m.fog_d = water_light[MONKEY_ROW + 3u]; + m.wind_a = water_light[MONKEY_ROW + 4u]; + m.wind_b = water_light[MONKEY_ROW + 5u]; + m.wet_a = water_light[MONKEY_ROW + 6u]; m.misc = water_light[MONKEY_ROW + 7u]; for (var i = 0u; i < 8u; i++) { m.benders[i] = water_light[MONKEY_ROW + 8u + i]; } return m; 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 88ad3cdad54c40dc8244f0ee4656b7d28e244bf3 Mon Sep 17 00:00:00 2001 From: pkuzic Date: Fri, 25 Sep 2026 10:20:40 +0100 Subject: [PATCH 2/4] MONKEY (wet): weather/wetness.rs -> MonkeyFrame wet row + wet_hook.wgsl library Wetness ramps up while it rains (~90 s to soaked) and dries over ~4 min; ripple clock; rainSurfaces resource; env WOW_RAIN_SURFACES / WOW_WETNESS / WOW_WET_T capture overrides. Co-Authored-By: Claude Opus 5.5 --- crates/benilla-assets/src/materials.rs | 2 + .../benilla-assets/src/shaders/wet_hook.wgsl | 93 ++++++++++ crates/benilla-world/src/weather/mod.rs | 3 + crates/benilla-world/src/weather/wetness.rs | 168 ++++++++++++++++++ 4 files changed, 266 insertions(+) create mode 100644 crates/benilla-assets/src/shaders/wet_hook.wgsl create mode 100644 crates/benilla-world/src/weather/wetness.rs diff --git a/crates/benilla-assets/src/materials.rs b/crates/benilla-assets/src/materials.rs index a2254aa8..0e7b04d8 100644 --- a/crates/benilla-assets/src/materials.rs +++ b/crates/benilla-assets/src/materials.rs @@ -34,6 +34,8 @@ pub fn register_shaders(app: &mut App) { load_shader_library!(app, "shaders/monkey_frame.wgsl"); // MONKEY (p0 fog hook): `benilla::fog_hook`, the one distance-fog law all seven receivers call. load_shader_library!(app, "shaders/fog_hook.wgsl"); + // MONKEY (wet): `benilla::wet_hook`, the rain-on-surfaces look terrain/model/static_gx call. + load_shader_library!(app, "shaders/wet_hook.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/wet_hook.wgsl b/crates/benilla-assets/src/shaders/wet_hook.wgsl new file mode 100644 index 00000000..9a4032b2 --- /dev/null +++ b/crates/benilla-assets/src/shaders/wet_hook.wgsl @@ -0,0 +1,93 @@ +#define_import_path benilla::wet_hook + +// MONKEY (wet): rain on surfaces — the ONE place the wet look is computed (the `shadow_hook.wgsl` +// pattern). Receivers (terrain, static_gx, wow_model) call it with one-line hooks and pass the +// MonkeyFrame (its `wet_a` row: `rain_rate, wetness, ripple_time_s, snow`, written by +// `benilla-world/src/weather/wetness.rs`). A zero wetness (dry weather, or `rainSurfaces 0`) returns +// the inputs untouched, so the dry image is bit-identical. +// +// Sky exposure, phase 1: the receiver passes `exposure` = 0 for interior-class / unlit / emissive +// surfaces and 1 otherwise; the normal then takes undersides out (dry), walls half wet, tops fully +// wet and glossy. There is no rain-occlusion map yet, so an exterior surface under a porch or a +// bridge still reads wet. + +#import benilla::monkey_frame::MonkeyFrame + +struct WetSurface { + albedo: vec3, + // Gloss weight for `wet_sheen`: 0 dry, ~1 a soaked top face, up to ~1.6 in a terrain puddle. + boost: f32, +} + +// How wet a surface is from its facing: none below, half on walls, full on tops. +fn wet_amount(n: vec3, exposure: f32, wetness: f32) -> f32 { + let facing = smoothstep(-0.2, 0.25, n.y); + return clamp(wetness, 0.0, 1.0) * clamp(exposure, 0.0, 1.0) * facing + * mix(0.5, 1.0, smoothstep(0.3, 0.8, n.y)); +} + +// Wet albedo: darker (water fills the micro-pores) and a touch more saturated; the gloss weight +// is kept for up-facing faces only. +fn wet_surface(albedo: vec3, n: vec3, world_pos: vec3, exposure: f32, + mf: MonkeyFrame) -> WetSurface { + let wet0 = mf.wet_a; + var o: WetSurface; + o.albedo = albedo; + o.boost = 0.0; + if (wet0.y <= 0.0 || exposure <= 0.0) { + return o; + } + let w = wet_amount(n, exposure, wet0.y); + let luma = dot(albedo, vec3(0.299, 0.587, 0.114)); + let sat = max(mix(vec3(luma), albedo, 1.0 + 0.3 * w), vec3(0.0)); + o.albedo = sat * (1.0 - 0.36 * w); + o.boost = w * smoothstep(0.35, 0.9, n.y); + return o; +} + +fn wet_hash(p: vec2) -> f32 { + let q = fract(p * vec2(0.1031, 0.1030)); + let r = q + dot(q, q.yx + 33.33); + return fract((r.x + r.y) * r.x); +} + +fn wet_noise(p: vec2) -> f32 { + let i = floor(p); + let f = fract(p); + let u = f * f * (3.0 - 2.0 * f); + return mix(mix(wet_hash(i), wet_hash(i + vec2(1.0, 0.0)), u.x), + mix(wet_hash(i + vec2(0.0, 1.0)), wet_hash(i + vec2(1.0, 1.0)), u.x), u.y); +} + +// Terrain bonus: puddle patches on flat ground once it is well soaked — darker, and a stronger +// gloss weight so they read as standing water catching the sky. +fn wet_puddles(s: WetSurface, n: vec3, world_pos: vec3, mf: MonkeyFrame) -> WetSurface { + let wet0 = mf.wet_a; + var o = s; + if (wet0.y <= 0.0) { + return o; + } + let xz = world_pos.xz; + let field = 0.65 * wet_noise(xz * 0.11) + 0.35 * wet_noise(xz * 0.43 + 17.0); + let level = smoothstep(0.94, 0.99, n.y); + // Puddles grow with the wetness: the threshold falls as the ground soaks. + let edge = mix(0.8, 0.6, smoothstep(0.4, 1.0, wet0.y)); + let puddle = smoothstep(edge, edge + 0.06, field) * level * smoothstep(0.35, 0.8, wet0.y); + o.albedo = o.albedo * (1.0 - 0.4 * puddle); + o.boost = max(o.boost, 1.6 * puddle); + return o; +} + +// The wet sheen, added to the lit gamma colour: a tight sun highlight (shadowed) and a Fresnel +// reflection of the sky/fog colour. Zero when `boost` is zero. +fn wet_sheen(boost: f32, n: vec3, to_view: vec3, to_light: vec3, + sun_rgb: vec3, sky_rgb: vec3, shadow: f32) -> vec3 { + if (boost <= 0.0) { + return vec3(0.0); + } + let h = normalize(to_light + to_view); + let nl = max(dot(n, to_light), 0.0); + let spec = pow(max(dot(n, h), 0.0), 80.0) * smoothstep(0.0, 0.15, nl); + let fres = 0.03 + 0.97 * pow(1.0 - clamp(dot(n, to_view), 0.0, 1.0), 5.0); + return boost * (sun_rgb * (0.9 * spec * shadow) + sky_rgb * (0.28 * fres)); +} diff --git a/crates/benilla-world/src/weather/mod.rs b/crates/benilla-world/src/weather/mod.rs index d2a1e144..09548546 100644 --- a/crates/benilla-world/src/weather/mod.rs +++ b/crates/benilla-world/src/weather/mod.rs @@ -16,6 +16,8 @@ use bevy::prelude::*; use crate::dev_state::DebugState; mod precip; +mod wetness; // MONKEY (wet): surface wetness + the rain-ripple clock -> MonkeyFrame wet0 +pub use wetness::{RainSurfaces, Wetness}; /// Rain's forced-fog window, read by the effect lane's `EffectFog::Rain` row. pub(crate) use precip::{RAIN_FOG_END, RAIN_FOG_START}; @@ -309,6 +311,7 @@ impl Plugin for WeatherPlugin { .add_message::() .add_systems(Update, weather_tick.in_set(WeatherTick)); precip::register(app); + wetness::register(app); // MONKEY (wet) } } diff --git a/crates/benilla-world/src/weather/wetness.rs b/crates/benilla-world/src/weather/wetness.rs new file mode 100644 index 00000000..caa9c4e2 --- /dev/null +++ b/crates/benilla-world/src/weather/wetness.rs @@ -0,0 +1,168 @@ +//! MONKEY (wet): surface wetness from the zone weather, published in the MonkeyFrame `wet_a` row +//! (`rain_rate, wetness, ripple_time_s, snow`). +//! +//! - `rain_rate` is channel A's effect density while the effect is rain (already ramped ~10 s by +//! the weather channel), else 0. +//! - `wetness` rises while it rains (full in about 90 s at full rain, slower in a drizzle) and +//! dries after it stops (about 4 min from soaked). It runs on real time, like the channels. +//! - `ripple_time_s` is the rain-ripple clock (wraps every [`RIPPLE_WRAP_S`] s; the shader's ring +//! rates are chosen so the wrap is seamless). +//! - `snow` is reserved (0). +//! +//! The `rainSurfaces` cvar ([`RainSurfaces`]) gates the whole row: off writes zeros, and every +//! shader reader treats a zero row as "dry", so the image is unchanged. +//! +//! Capture knobs: `WOW_RAIN_SURFACES=0|1` overrides the cvar; `WOW_WETNESS=<0..1>` pins the +//! wetness (a capture cannot wait minutes for the ramp); `WOW_WET_T=` pins the ripple clock. + +use bevy::prelude::*; + +use super::{WeatherKind, WeatherState, WeatherTick}; +use crate::lighting::MonkeyFrame; + +/// The `rainSurfaces` cvar: 0 Off (no wet surfaces, no rain ripples), 1 On. benilla-app's +/// settings bridge (`monkey_gfx.rs`) writes it. +#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq)] +pub struct RainSurfaces(pub bool); + +impl Default for RainSurfaces { + fn default() -> Self { + Self(true) + } +} + +/// Seconds from dry to soaked under full rain. +const WET_UP_S: f32 = 90.0; +/// Seconds from soaked to dry once the rain stops. +const DRY_S: f32 = 240.0; +/// The ripple clock's wrap. The shader's ring rates are multiples of 1/`RIPPLE_WRAP_S`, so every +/// ring's phase is continuous across the wrap. +pub const RIPPLE_WRAP_S: f32 = 1000.0; + +/// The wetness integrator's state. +#[derive(Resource, Default, Debug, Clone, Copy)] +pub struct Wetness { + pub wetness: f32, + pub ripple_time_s: f32, +} + +impl Wetness { + /// One real-time step. `rain` is the rain rate 0..1 (0 when not raining). + fn step(&mut self, rain: f32, dt: f32) { + if rain > 0.0 { + // A drizzle wets slower and never quite soaks. + let target = 0.55 + 0.45 * rain.min(1.0); + let rate = (0.35 + 0.65 * rain.min(1.0)) / WET_UP_S; + if self.wetness < target { + self.wetness = (self.wetness + rate * dt).min(target); + } else { + self.wetness = (self.wetness - dt / DRY_S).max(target); + } + } else { + self.wetness = (self.wetness - dt / DRY_S).max(0.0); + } + self.ripple_time_s = (self.ripple_time_s + dt) % RIPPLE_WRAP_S; + } +} + +fn env_f32(key: &str) -> Option { + std::env::var(key).ok()?.trim().parse().ok() +} + +/// The session-only overrides, read once. +struct EnvOverrides { + on: Option, + wetness: Option, + ripple_t: Option, +} + +fn env_overrides() -> &'static EnvOverrides { + static ENV: std::sync::OnceLock = std::sync::OnceLock::new(); + ENV.get_or_init(|| EnvOverrides { + on: env_f32("WOW_RAIN_SURFACES").map(|v| v >= 0.5), + wetness: env_f32("WOW_WETNESS").map(|v| v.clamp(0.0, 1.0)), + ripple_t: env_f32("WOW_WET_T"), + }) +} + +fn wetness_tick( + time: Res>, + state: Res, + cvar: Res, + mut wet: ResMut, + mut frame: ResMut, +) { + let env = env_overrides(); + let rain = if state.effect_kind == WeatherKind::Rain { + state.effect_density.clamp(0.0, 1.0) + } else { + 0.0 + }; + wet.step(rain, time.delta_secs()); + let on = env.on.unwrap_or(cvar.0); + let (rate, wetness, ripple) = if on { + ( + rain, + env.wetness.unwrap_or(wet.wetness), + env.ripple_t.unwrap_or(wet.ripple_time_s), + ) + } else { + (0.0, 0.0, 0.0) + }; + // Write only on change, so change detection on the frame stays meaningful. + if frame.rain_rate != rate || frame.wetness != wetness || frame.ripple_time_s != ripple { + frame.rain_rate = rate; + frame.wetness = wetness; + frame.ripple_time_s = ripple; + frame.snow = 0.0; + } +} + +pub(super) fn register(app: &mut App) { + app.init_resource::() + .init_resource::() + .add_systems(Update, wetness_tick.after(WeatherTick)); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn full_rain_soaks_in_about_ninety_seconds_and_dries_in_four_minutes() { + let mut w = Wetness::default(); + for _ in 0..(89 * 10) { + w.step(1.0, 0.1); + } + assert!(w.wetness > 0.95 && w.wetness < 1.0, "{}", w.wetness); + for _ in 0..(20 * 10) { + w.step(1.0, 0.1); + } + assert_eq!(w.wetness, 1.0); + for _ in 0..(120 * 10) { + w.step(0.0, 0.1); + } + assert!((w.wetness - 0.5).abs() < 0.01, "half dry at 2 min: {}", w.wetness); + for _ in 0..(121 * 10) { + w.step(0.0, 0.1); + } + assert_eq!(w.wetness, 0.0); + } + + #[test] + fn a_drizzle_never_quite_soaks() { + let mut w = Wetness::default(); + for _ in 0..(600 * 10) { + w.step(0.2, 0.1); + } + assert!((w.wetness - 0.64).abs() < 1e-4, "{}", w.wetness); + } + + #[test] + fn the_ripple_clock_wraps() { + let mut w = Wetness::default(); + w.ripple_time_s = RIPPLE_WRAP_S - 0.05; + w.step(0.0, 0.1); + assert!(w.ripple_time_s < 0.1); + } +} From 5b8306dd6f4359f40bc602f5e8c75c3fa1efce1c Mon Sep 17 00:00:00 2001 From: pkuzic Date: Fri, 25 Sep 2026 10:32:05 +0100 Subject: [PATCH 3/4] MONKEY (wet): wet hooks in terrain/static_gx/wow_model, rain rings on water, rainSurfaces cvar + row, wet- scenarios Co-Authored-By: Claude Opus 5.5 --- LIGHTING.md | 1 + crates/benilla-app/assets/ui/OptionsFrame.xml | 30 ++++++++++ crates/benilla-app/src/capture/scenarios.rs | 17 ++++++ crates/benilla-app/src/cvars.rs | 7 +++ crates/benilla-app/src/monkey_gfx.rs | 30 ++++++++-- .../src/shaders/enhanced_water.wgsl | 57 ++++++++++++++++++- .../benilla-assets/src/shaders/terrain.wgsl | 12 ++++ .../benilla-assets/src/shaders/wet_hook.wgsl | 14 +++-- .../benilla-assets/src/shaders/wow_model.wgsl | 14 +++++ .../benilla-world/src/shaders/static_gx.wgsl | 10 ++++ 10 files changed, 178 insertions(+), 14 deletions(-) diff --git a/LIGHTING.md b/LIGHTING.md index 6a3a2d69..394ec0fa 100644 --- a/LIGHTING.md +++ b/LIGHTING.md @@ -25,6 +25,7 @@ behind each constant. | Ground-effect spells | Flamestrike, Rain of Fire, Consecration, Flare and fire traps light the ground for their duration; frost and nature areas stay dark | `spellLightGain` | | Volumetric fog | near-field haze that converges on the zone fog colour (clear within 10 yd, full by 150 yd; mistier at dawn and in bad weather, faint indoors) and sun/moon light shafts through gaps, sampled from the shadow map; own fullscreen pass after the main pass (`benilla-app/src/volumetric_fog.rs`) | Advanced Graphics → Volumetric Fog (Off/Low/High), cvar `volumetricFog`, env `WOW_VOLFOG=0\|1\|2` | | Sky dithering | a faint screen-space dither in the FFXGlow combine so smooth sky and fog gradients do not band (MONKEY p0; was env-only) | Advanced Graphics → Sky Dithering, cvar `skyDither` 0/1 (default 0, Graphics preset High = 1), env `WOW_DITHER=1` still forces it on; bridge in `benilla-app/src/monkey_gfx.rs` | +| Rain on surfaces | MONKEY (wet): while it rains, sky-exposed ground, stone and wood soak (dry→soaked in ~90 s, dry again over ~4 min): darker, slightly more saturated albedo, puddles on flat terrain, a sun highlight + sky Fresnel sheen on up-facing faces; rain rings on exterior Enhanced water. Wetness lives in `benilla-world/src/weather/wetness.rs` → MonkeyFrame `wet_a`; the look is `benilla-assets/src/shaders/wet_hook.wgsl` (one-line hooks in terrain/static_gx/wow_model), rings are `rain_ripple_normal` in `enhanced_water.wgsl`. Phase 1 sky exposure = not interior/unlit/emissive and not downward-facing; no rain-occlusion map yet, so porches and bridges still wet what is under them | Advanced Graphics → Wet Surfaces in Rain, cvar `rainSurfaces` 0/1 (default 1, Graphics preset High = 1); env `WOW_RAIN_SURFACES=0\|1`, `WOW_WETNESS=<0..1>` (pin wetness), `WOW_WET_T=` (pin ripple clock) | | Night and interior level | global dimming of the night sky term and of interior ambient | `nightGain`, `interiorGain`, `interiorBakeFloor` | Players reach all of it from **Options -> Advanced Graphics** (a Lighting Quality preset Off / Low / Medium / High plus the individual rows; Off is the original client look). The dev build has a panel for all of it: **Ctrl+Shift+D → Lighting & shadows**, with Dim / Default / diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml index a54f95cb..28f3b6ad 100644 --- a/crates/benilla-app/assets/ui/OptionsFrame.xml +++ b/crates/benilla-app/assets/ui/OptionsFrame.xml @@ -521,6 +521,8 @@ interiorDarkness = "Interior Darkness", -- MONKEY (p0 skyDither) skyDither = "Sky Dithering", + -- MONKEY (wet) + rainSurfaces = "Wet Surfaces in Rain", yards = "%d yd", tips = { LIGHTING_QUALITY = @@ -617,6 +619,12 @@ SKY_DITHER = "Adds a faint noise to the final image so smooth sky and fog gradients do not " .. "break into visible bands. Off is the original client's look. Practically free.", + -- MONKEY (wet) + RAIN_SURFACES = + "Rain slowly soaks the ground, stone and wood under the open sky: they darken, " + .. "puddles gather on flat ground and wet tops catch the light; rain drops ring " + .. "the water. It dries over a few minutes after the rain stops. Off is the " + .. "original client's look. Nearly free, and only active while things are wet.", }, }, ruRU = { @@ -651,6 +659,8 @@ interiorDarkness = "Темнота помещений", -- MONKEY (p0 skyDither) skyDither = "Дизеринг неба", + -- MONKEY (wet) + rainSurfaces = "Мокрые поверхности в дождь", yards = "%d ярд.", tips = { LIGHTING_QUALITY = @@ -756,6 +766,12 @@ "Добавляет едва заметный шум в итоговое изображение, чтобы плавные переходы цвета " .. "неба и тумана не распадались на полосы. Выключено — вид оригинального клиента. " .. "Почти ничего не стоит.", + -- MONKEY (wet) + RAIN_SURFACES = + "Дождь постепенно намачивает землю, камень и дерево под открытым небом: они темнеют, " + .. "на ровной земле собираются лужи, мокрые поверхности блестят, а капли расходятся " + .. "кругами по воде. После дождя всё высыхает за несколько минут. Выключено — вид " + .. "оригинального клиента. Почти ничего не стоит и работает, только пока всё мокрое.", }, }, } @@ -816,6 +832,8 @@ "RowNightDarkness", "RowInteriorDarkness", -- MONKEY (p0 skyDither) "RowSkyDither", + -- MONKEY (wet) + "RowRainSurfaces", }, -- Player Guild Names is a child of Player Names, as in 1.12 (l.702-708). Nameplates = { @@ -2854,6 +2872,18 @@ "BENILLA_TOOLTIP_SKY_DITHER") + + + + + + + + OptionsRow_OnLoad(self, "rainSurfaces", + BENILLA_ADVGFX.rainSurfaces, + "BENILLA_TOOLTIP_RAIN_SURFACES") + +