From 3e59cc126c8134c52b92ecf98d15f2b7b573f896 Mon Sep 17 00:00:00 2001 From: pkuzic Date: Sat, 26 Sep 2026 17:53:32 +0100 Subject: [PATCH] MONKEY (fix-wind): visible foliage wind + Foliage Wind Strength slider The owner's "wind does not work" was the motion being too small to see (WXL's 0.06 yd grass tips on a 6 s wave, 0.1 yd tree crowns). Retuned at the 9 yd/s base speed: - grass: tips about +-0.2-0.3 yd, 2.5 s main wave (182 cycles/wrap), 1.1 s cross flutter (410); 55 yd gust bands roll downwind (68), swelling the sway to 1.2x and lulling it to 0.55x; a bent tip dips (no stretched blades). Parting around units unchanged. - trees/bushes: 4 s crown sway (114), 5.5 s side wave (82), 0.8 s leaf flutter (569); crown weight grows with height (0.43x at 1.5 yd, 1x at 6 yd, 1.3x cap), so canopies sway about +-0.3 yd while bushes nod and bases stay planted. - new cvar foliageWindStrength 0.25..3 (default 1, not on the preset ladder): Advanced Graphics slider under Foliage Wind (EN+RU label/tooltip), greyed while Foliage Wind is Off; carried in sway_strength, so the parting ignores it; a storm adds up to +60 % sway. Co-Authored-By: Claude Opus 5.5 --- LIGHTING.md | 2 +- crates/benilla-app/assets/ui/OptionsFrame.xml | 34 ++++++++++- crates/benilla-app/src/cvars.rs | 7 +++ crates/benilla-app/src/monkey_gfx.rs | 12 +++- .../benilla-assets/src/shaders/wind_hook.wgsl | 51 ++++++++++------ crates/benilla-world/src/wind/mod.rs | 60 ++++++++++++++++++- 6 files changed, 142 insertions(+), 24 deletions(-) diff --git a/LIGHTING.md b/LIGHTING.md index 651e4aa7..03ec8577 100644 --- a/LIGHTING.md +++ b/LIGHTING.md @@ -31,7 +31,7 @@ behind each constant. | Sky quality | Enhanced: the five Light.dbc sky stops through a smooth monotone curve in linear light (no bands at the rings), a soft sun glow tinted by sun and fog colour (fades at night and under cloud), a procedural star field with twinkle and a faint Milky Way over the stock `Stars.m2`. High adds domain-warped cloud detail and sun-lit clouds (self-shadow, silver lining; technique from WarcraftXL, see `THIRD-PARTY.md`). Classic is the reference sky unchanged | Advanced Graphics → Sky Quality, cvar `skyQuality` 0 Classic / 1 Enhanced / 2 High, env `WOW_SKY_QUALITY` | | Modern fog | MONKEY (fog): radial distance fog with a gradual exponential curve, a daylight sun lobe, and an end colour shared by the world, WDL hull, sky horizon and volumetric haze. Each zone keeps its authored fog end up to the reference view-distance limit; optional `LightFogBand.dbc` rows provide height, sun and end-fog controls. Interior WMO fog stays classic | Advanced Graphics → Modern Fog, cvar `fogModel` 0 Classic (default) / 1 Modern (High = 1), env `WOW_FOGMODEL=0\|1`; `fog_hook.wgsl`, `lighting/fog_model.rs`, `light/fog_band.rs` | | Rain on surfaces | MONKEY (wet): rain gradually darkens and saturates exposed terrain and models, adds puddles and a sky/sun sheen, and produces rings on exterior Enhanced water. Wetness rises over roughly 90 seconds and dries over roughly four minutes; MONKEY (rainshelter): a camera-centred rain-occlusion map (128² cells of 0.75 yd, filled by vertical collision rays) keeps ground, walls and water under roofs, porches and bridges dry, with a ~1.5 yd drip margin and a soft edge; tree canopies without colliders do not shelter | Advanced Graphics → Wet Surfaces in Rain, cvar `rainSurfaces` 0/1 (default and High 1), env `WOW_RAIN_SURFACES`, `WOW_WETNESS`, `WOW_WET_T`; `weather/wetness.rs`, `weather/shelter.rs`, `wet_hook.wgsl` | -| Foliage wind | one weather-fed gust/veer field drives grass and classified tree/bush foliage; grass also parts around the player and nearby units | Advanced Graphics → Foliage Wind, cvar `foliageWind` 0 Off / 1 Grass / 2 Grass + Trees (High = 2), capture override `WOW_FOLIAGE_WIND` (dev build + `WOW_CAPTURE` only); field and benders in `benilla-world/src/wind/`. The wave phase is the CPU-integrated travel (speed integrated over time, wrapped seamlessly), and the waves' spatial term uses the fixed profile heading; the veer turns only the bend. Tree exile copies sway through material marker bit 14 (`FOLIAGE_WIND_MARKER`). Capture offset `WOW_CAPTURE_WIND_T` (s). Known: shadow casters (world-shadow proxies, `torch_depth.wgsl`) do not apply the sway, so leaves sample a static shadow; a future world-camera DepthPrepass must call the same offsets | +| Foliage wind | one weather-fed gust/veer field drives grass and classified tree/bush foliage; grass also parts around the player and nearby units | Advanced Graphics → Foliage Wind, cvar `foliageWind` 0 Off / 1 Grass / 2 Grass + Trees (High = 2), plus the slider Foliage Wind Strength, cvar `foliageWindStrength` 0.25..3 (default 1, not on the preset ladder; a gain on the sway, not the parting; a storm adds up to +60 %). Tuning at 1 (fix-wind): grass tips about ±0.2-0.3 yd on a 2.5 s wave with 1.1 s cross flutter, gust bands 55 yd wide rolling downwind; tree crowns about ±0.3 yd (lever-arm weight grows with height to 1.3x at 8.5 yd) on a 4 s sway, 5.5 s side wave, 0.8 s leaf flutter; bent tips dip slightly. capture override `WOW_FOLIAGE_WIND` (dev build + `WOW_CAPTURE` only); field and benders in `benilla-world/src/wind/`. The wave phase is the CPU-integrated travel (speed integrated over time, wrapped seamlessly), and the waves' spatial term uses the fixed profile heading; the veer turns only the bend. Tree exile copies sway through material marker bit 14 (`FOLIAGE_WIND_MARKER`). Capture offset `WOW_CAPTURE_WIND_T` (s). Known: shadow casters (world-shadow proxies, `torch_depth.wgsl`) do not apply the sway, so leaves sample a static shadow; a future world-camera DepthPrepass must call the same offsets | | Ambient occlusion | soft contact shadows under props, in wall-floor corners and around feet; depth-only SSAO at half resolution (depth-reconstructed normals, 6/12 spiral taps, 4x4 depth-aware blur, bilateral upsample) multiplied into the opaque scene after the opaque pass and before the water copy, so water, particles and UI are untouched; fades out 45-90 yd (Low) / 60-120 yd (High), spares sky and bright pixels; on alpha-tested foliage (a dense window of two-sided depth spikes) the blur widens its depth tolerance and the AO drops 40 %, which removes the leaf grain while planes, silhouettes and contact creases keep full AO (`benilla-app/src/ssao.rs`, `benilla-app/src/shaders/ssao.wgsl`) | Advanced Graphics → Ambient Occlusion (Off/Low/High; default Off, High preset 2), cvar `ambientOcclusion`, env `WOW_AO=0\|1\|2`, `WOW_AO_DEBUG=1..5` diagnostic views (factor, protection, distance, raw occlusion, foliage mask), `WOW_AO_GAIN`/`_RADIUS`/`_BIAS`/`_STRENGTH` tuning | | Lamp fog | MONKEY (lampfog): the fog fullscreen pass selects the nearest resolved exterior and interior point-table sources and analytically integrates their softened inverse-square light over the camera-to-scene-depth ray. A near-field haze floor makes lamps visible inside the ordinary volumetric haze's 10 yd clear zone; it is zero by day, stronger in rain/fog, and deliberately unshadowed. The table colour already includes intensity, flame flicker and `fireLightGain`. Halo output uses destination headroom and stays below 1.0, so a later bloom pass cannot blow it out | Advanced Graphics → Lamp Fog, cvar `lampFog` 0 Off (default, unchanged) / 1 Low (16 lamps) / 2 High (32 lamps; Graphics preset High = 2), env `WOW_LAMPFOG=0\|1\|2`; `benilla-app/src/volumetric_fog.rs` | | City window daylight | MONKEY (fix-daylight/integration): a district WMO's oversized window batch (e.g. the Cathedral of Light's stained-glass walls) is split into window-sized daylight apertures, so the nave takes daylight pane by pane. Applies to WMOs loaded after a change | Advanced Graphics → City Window Daylight, cvar `daylightWindowSplit` 0/1 (default 1, the merged behaviour); `benilla-world/src/lighting/daylight.rs` (`set_window_split`), bridge `benilla-app/src/dynamic_interior.rs` | diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml index 74dc3699..dc95de76 100644 --- a/crates/benilla-app/assets/ui/OptionsFrame.xml +++ b/crates/benilla-app/assets/ui/OptionsFrame.xml @@ -543,6 +543,7 @@ foliageWind = "Foliage Wind", windGrass = "Grass", windTrees = "Grass + Trees", + foliageWindStrength = "Foliage Wind Strength", -- MONKEY (fix-wind) -- MONKEY (ao): localised contact-shadow row. ambientOcclusion = "Ambient Occlusion", -- MONKEY (lampfog) @@ -707,6 +708,11 @@ "Grass bends in two travelling wind waves and parts around nearby characters. " .. "Grass + Trees also gives classified tree and bush foliage a gentle sway.\n\n" .. "Off makes no vertex displacement and restores the unchanged image. Tree shadows stay static.", + -- MONKEY (fix-wind) + FOLIAGE_WIND_STRENGTH = + "How far grass and foliage swing in the wind. 100% is the standard sway; lower " + .. "is calmer, higher is a blustery day. Storms swing everything harder on top. " + .. "Grass still parts around characters the same way. Needs Foliage Wind on.", -- MONKEY (ao): what the contact shadows touch and what they spare. AMBIENT_OCCLUSION = "Soft contact shadows where surfaces meet: under props and furniture, in the corners " @@ -782,6 +788,7 @@ foliageWind = "Ветер листвы", windGrass = "Трава", windTrees = "Трава + деревья", + foliageWindStrength = "Сила ветра листвы", -- MONKEY (fix-wind) -- MONKEY (ao): Russian contact-shadow row. ambientOcclusion = "Затенение окружения", -- MONKEY (lampfog) @@ -956,6 +963,11 @@ "Трава гнётся двумя бегущими волнами ветра и расступается вокруг персонажей. " .. "Трава + деревья также добавляет мягкое качание листвы деревьев и кустов.\n\n" .. "Выключено не смещает вершины и восстанавливает исходный вид. Тени деревьев остаются неподвижными.", + -- MONKEY (fix-wind) + FOLIAGE_WIND_STRENGTH = + "Насколько сильно трава и листва качаются на ветру. 100% — обычное качание; меньше — " + .. "спокойнее, больше — ветреный день. В грозу всё качается ещё сильнее. " + .. "Трава по-прежнему расступается вокруг персонажей. Работает, если включён ветер листвы.", -- MONKEY (ao): Russian contact-shadow explanation. AMBIENT_OCCLUSION = "Мягкие контактные тени там, где поверхности соприкасаются: под предметами и мебелью, " @@ -1049,6 +1061,8 @@ "RowRainSurfaces", -- MONKEY (wind): appended for conflict-free lane merges. "RowFoliageWind", + -- MONKEY (fix-wind): the sway strength, right under its tier. + "RowFoliageWindStrength", -- MONKEY (ao): appended so the other rows keep their indices. "RowAmbientOcclusion", -- MONKEY (lampfog) @@ -1574,6 +1588,9 @@ -- MONKEY (daylight: terrain torch casters): the ground only casts for outdoor fires. OptionsRow_SetEnabled(getglobal(base .. "RowTorchTerrainShadows"), GetCVar("exteriorShadows") ~= "0") + -- MONKEY (fix-wind): the strength only matters while some foliage sways. + OptionsRow_SetEnabled(getglobal(base .. "RowFoliageWindStrength"), + GetCVar("foliageWind") ~= "0") return end if key == "Controls" then @@ -3260,12 +3277,25 @@ }) - - + + + + OptionsRow_OnLoad(self, "foliageWindStrength", + BENILLA_ADVGFX.foliageWindStrength, + "BENILLA_TOOLTIP_FOLIAGE_WIND_STRENGTH") + OptionsSliderRow_Setup(self, 0.25, 3, 0.25) + + + + + + + + OptionsRow_OnLoad(self, "ambientOcclusion", BENILLA_ADVGFX.ambientOcclusion, "BENILLA_TOOLTIP_AMBIENT_OCCLUSION") diff --git a/crates/benilla-app/src/cvars.rs b/crates/benilla-app/src/cvars.rs index 8e6286cb..ff90cf2e 100644 --- a/crates/benilla-app/src/cvars.rs +++ b/crates/benilla-app/src/cvars.rs @@ -964,6 +964,12 @@ pub(crate) const REGISTERED: &[Registered] = &[ "2", "benilla's own: foliage wind, 0 Off / 1 Grass / 2 Grass + trees", ), + // MONKEY (fix-wind): the sway strength slider under Foliage Wind; not on the preset ladder. + ours( + "foliageWindStrength", + "1", + "benilla's own: foliage wind strength, a gain on the sway, 0.25..3 (1 = the shipped tuning)", + ), // MONKEY (fog): the distance-fog model. 0 = the 1.12 linear fog (byte-identical), 1 = Modern // (exponential, sun/horizon colour, end-fog shift, fog end decoupled from farclip past 777). // Default 0; the Graphics preset's High sets 1. @@ -4420,6 +4426,7 @@ mod tests { app.init_resource::(); // MONKEY (wind) app.init_resource::(); + app.init_resource::(); // MONKEY (fix-wind) app.init_resource::(); // MONKEY (wet) app.add_observer(crate::monkey_gfx::on_cvar); }, diff --git a/crates/benilla-app/src/monkey_gfx.rs b/crates/benilla-app/src/monkey_gfx.rs index 8b40b233..261b48eb 100644 --- a/crates/benilla-app/src/monkey_gfx.rs +++ b/crates/benilla-app/src/monkey_gfx.rs @@ -6,12 +6,13 @@ //! |---|---|---|---|---| //! | `skyDither` | 0 Off / 1 On | 0 | 1 | `benilla_world::ffx_glow::SkyDither` | //! | `foliageWind` | 0 Off / 1 Grass / 2 Grass + trees | 2 | 2 | `benilla_world::wind::FoliageWind` | +//! | `foliageWindStrength` | 0.25..3 sway gain | 1 | not governed | `benilla_world::wind::FoliageWindStrength` | //! | `fogModel` | 0 Classic / 1 Modern | 0 | 1 | `benilla_world::lighting::FogModelSetting` | //! | `rainSurfaces` | 0 Off / 1 On | 1 | 1 | `benilla_world::weather::RainSurfaces` | use benilla_world::ffx_glow::SkyDither; use benilla_world::lighting::FogModelSetting; -use benilla_world::wind::FoliageWind; +use benilla_world::wind::{FoliageWind, FoliageWindStrength}; use bevy::prelude::*; use benilla_world::weather::RainSurfaces; // MONKEY (wet) @@ -20,6 +21,7 @@ pub(crate) fn on_cvar( ev: On, mut dither: ResMut, mut foliage_wind: ResMut, + mut wind_strength: ResMut, // MONKEY (fix-wind) // MONKEY (fog) mut fog_model: ResMut, mut rain: ResMut, // MONKEY (wet) @@ -41,6 +43,13 @@ pub(crate) fn on_cvar( foliage_wind.0 = want; } } + // MONKEY (fix-wind): the sway gain, live, clamped to the slider's 0.25..3. + "foliagewindstrength" => { + let want = FoliageWindStrength::from_cvar(ev.num()); + if *wind_strength != want { + *wind_strength = want; + } + } // MONKEY (fog): `WOW_FOGMODEL` still wins for the session (captures). "fogmodel" => fog_model.set_from_cvar(ev.num()), // MONKEY (wet): wet surfaces + rain ripples. @@ -74,6 +83,7 @@ impl Plugin for MonkeyGfxPlugin { .insert_resource(FoliageWind( foliage_wind_pin().unwrap_or(FoliageWind::REGISTERED), )) + .init_resource::() // MONKEY (fix-wind) .init_resource::() .init_resource::() // MONKEY (wet) .add_observer(on_cvar); diff --git a/crates/benilla-assets/src/shaders/wind_hook.wgsl b/crates/benilla-assets/src/shaders/wind_hook.wgsl index 28e60f68..0b61c428 100644 --- a/crates/benilla-assets/src/shaders/wind_hook.wgsl +++ b/crates/benilla-assets/src/shaders/wind_hook.wgsl @@ -21,6 +21,14 @@ fn wind_base_dir(frame: MonkeyFrame) -> vec2 { return vec2(cos(frame.wind_a.z), sin(frame.wind_a.z)); } +// MONKEY (fix-wind): gusts rolling across a field: a 55 yd band travelling downwind (68 cycles +// per wrap, about 8 yd/s at the base speed) that swells the sway to 1.2x and lulls it to 0.55x. +fn wind_gust_roll(frame: MonkeyFrame, xz: vec2) -> f32 { + let band = sin(dot(wind_base_dir(frame), xz) * (WIND_TAU / 55.0) + - wind_travel_phase(frame.wind_b.x, 68.0)); + return mix(0.55, 1.2, 0.5 + 0.5 * band); +} + fn wind_cross_dir(dir: vec2) -> vec2 { // WarcraftXL's secondary wave is 35 degrees off the primary. return vec2( @@ -51,19 +59,20 @@ fn grass_offset( let base_secondary = wind_cross_dir(base_primary); let wave_primary = WIND_TAU / 18.0; let wave_secondary = WIND_TAU / 6.5; - // WarcraftXL's k * 0.33 * speed (primary) and k * 0.33 * 1.37 * speed (secondary), as whole - // cycles per wrap: 75 and 285. + // MONKEY (fix-wind): visible tuning. At the 9 yd/s base speed the primary wave runs a 2.5 s + // period (182 cycles per wrap) and the cross flutter 1.1 s (410); both speed up with gusts. let phase_primary = wave_primary * dot(base_primary, world.xz) - - wind_travel_phase(frame.wind_b.x, 75.0) + tuft_phase; + - wind_travel_phase(frame.wind_b.x, 182.0) + tuft_phase; let phase_secondary = wave_secondary * dot(base_secondary, world.xz) - - wind_travel_phase(frame.wind_b.x, 285.0) + tuft_phase * 1.7; + - wind_travel_phase(frame.wind_b.x, 410.0) + tuft_phase * 1.7; let gust = 1.0 + 0.5 * (frame.wind_a.w - 0.5) * 2.0; let variance = mix(0.7, 1.3, fract(sin(tuft_phase * 12.9898) * 43758.5453)); let distance_fade = 1.0 / (1.0 + distance(camera, world) * 0.015); var xz = ( - dir_primary * 0.060 * (sin(phase_primary) + 0.35) - + dir_secondary * 0.020 * sin(phase_secondary) - ) * gust * variance * weight * distance_fade * frame.wind_b.y * frame.wind_b.z; + dir_primary * 0.18 * (sin(phase_primary) + 0.35) + + dir_secondary * 0.06 * sin(phase_secondary) + ) * gust * variance * wind_gust_roll(frame, world.xz) * weight * distance_fade + * frame.wind_b.y * frame.wind_b.z; let count = min(u32(frame.misc.x), 8u); for (var i = 0u; i < 8u; i += 1u) { @@ -77,7 +86,8 @@ fn grass_offset( xz += radial * 0.50 * edge * cone * weight * frame.wind_b.z; } } - return vec3(xz.x, 0.0, xz.y); + // MONKEY (fix-wind): a bent tip dips, so a blade reads as bending rather than stretching. + return vec3(xz.x, -min(0.6 * dot(xz, xz), 0.25), xz.y); } // MONKEY (wind): our tree/bush design (WarcraftXL has no tree wind). Only leaf-card batches call @@ -95,26 +105,29 @@ fn tree_offset( // A one-yard bush still needs a crown; tree leaf cards generally begin above this range and // therefore reach full weight, while the first 0.15 yd remains a planted base. let crown = smoothstep(0.15, 1.5, h); - let weight = crown * crown; + // MONKEY (fix-wind): a taller crown swings further (lever arm): 0.43x at 1.5 yd, 1x at 6 yd, + // capped at 1.3x from 8.5 yd, so a bush nods while a canopy sways. + let weight = crown * crown * clamp(0.25 + h * 0.12, 0.25, 1.3); let dir = normalize(frame.wind_a.xy + vec2(1.0e-6, 0.0)); let cross = vec2(-dir.y, dir.x); let base = wind_base_dir(frame); let base_cross = vec2(-base.y, base.x); let phase = fract(sin(dot(anchor.xz, vec2(0.173, 0.317))) * 43758.5453) * WIND_TAU; - // 0.08 rad/yd sway, 0.61x side wave and a 2.3 rad/s-at-base-speed flutter, as whole cycles - // per wrap of travel: 52, 32 and 167. + // MONKEY (fix-wind): visible tuning. At the 9 yd/s base speed: a 4 s crown sway (114 cycles + // per wrap), a 5.5 s side wave (82) and a 0.8 s leaf flutter (569). let slow = sin(dot(base, world.xz) * (WIND_TAU / 34.0) - - wind_travel_phase(frame.wind_b.x, 52.0) + phase); + - wind_travel_phase(frame.wind_b.x, 114.0) + phase); let side = sin(dot(base_cross, world.xz) * (WIND_TAU / 21.0) - - wind_travel_phase(frame.wind_b.x, 32.0) + phase * 1.7); - let flutter = sin(wind_travel_phase(frame.wind_b.x, 167.0) + - wind_travel_phase(frame.wind_b.x, 82.0) + phase * 1.7); + let flutter = sin(wind_travel_phase(frame.wind_b.x, 569.0) + dot(world.xz, vec2(1.7, 2.1)) + phase * 2.0); let gust = 0.75 + frame.wind_a.w * 0.5; let fade = 1.0 / (1.0 + distance(camera, world) * 0.006); let xz = ( - dir * (0.045 + slow * 0.055) - + cross * side * 0.025 - + dir * flutter * 0.012 - ) * weight * gust * fade * frame.wind_b.y * frame.wind_b.w; - return vec3(xz.x, 0.0, xz.y); + dir * (0.08 + slow * 0.20) + + cross * side * 0.08 + + dir * flutter * 0.03 + ) * weight * gust * mix(0.75, 1.05, (wind_gust_roll(frame, anchor.xz) - 0.55) / 0.65) + * fade * frame.wind_b.y * frame.wind_b.w; + return vec3(xz.x, -0.15 * dot(xz, xz), xz.y); } diff --git a/crates/benilla-world/src/wind/mod.rs b/crates/benilla-world/src/wind/mod.rs index 71fe4b45..1f88610c 100644 --- a/crates/benilla-world/src/wind/mod.rs +++ b/crates/benilla-world/src/wind/mod.rs @@ -91,6 +91,37 @@ impl FoliageWind { } } +/// MONKEY (fix-wind): the player's foliage sway strength (`foliageWindStrength`), a plain gain on +/// the sway amplitude: 0.25..3, 1 = the shipped tuning. Parting around units ignores it. +#[derive(Resource, Clone, Copy, Debug, PartialEq)] +pub struct FoliageWindStrength(pub f32); + +impl FoliageWindStrength { + pub const MIN: f32 = 0.25; + pub const MAX: f32 = 3.0; + /// The registered default. + pub const REGISTERED: f32 = 1.0; + + /// A CVar value into the slider's range (non-finite reads as the default). + pub fn from_cvar(v: f32) -> Self { + Self(if v.is_finite() { + v.clamp(Self::MIN, Self::MAX) + } else { + Self::REGISTERED + }) + } +} + +impl Default for FoliageWindStrength { + fn default() -> Self { + Self(Self::REGISTERED) + } +} + +/// MONKEY (fix-wind): a storm swings the foliage harder, up to +60 % at a full storm, on top of the +/// faster travel the storm's wind speed already gives. +pub const STORM_SWAY_GAIN: f32 = 0.6; + impl Default for FoliageWind { fn default() -> Self { // The resource exists before the CVar host. Its ordinary default therefore has to equal @@ -186,6 +217,7 @@ fn update_wind( time: Res