Files
benilla-octo/crates/benilla-app/src/dynamic_interior.rs
T
pkuzicandClaude Opus 5.5 18914f02f6 MONKEY: enhanced water module (Water Quality: Classic/Enhanced/High) + lava glow
Water (Enhanced/High only; Classic stays byte-identical to upstream):
- scene depth resolved after the main opaque pass and bound to liquids
  (liquid/scene_depth.rs, water_depth.rs)
- procedural multi-band waves with analytic normals; long ocean swell moves
  vertices, mirrored on the CPU (liquid/waves.rs)
- separate ocean and inland profiles; depth-driven colour and soft edge
- Fresnel sky reflection, sun path, moon path, point-light glints
- beach surf driven by the shore wave phase: continuous front, dissolving
  lace, swash sheet; only where the authored sea bed is shallow, so sunken
  objects never foam; thin bed clutter (grass, reeds) ignored for contact foam
- swimmers: wake foam, treading rings, visual-only bob with the swell
- river flow effect tried and removed at the owner's request

Lava: magma surfaces emit warm point lights through the lighting module
(lighting/lava_light.rs, lavaLightGain).

Options: Water Quality + Lava Glow rows (EN, RU strings prepared).
Docs: WATER.md. Credits: design reference WarcraftXL wxl-experimental-water,
author iThorgrim (reuse permitted with attribution).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:15:57 +01:00

153 lines
7.8 KiB
Rust

//! MONKEY (dynamic interiors): the app-side half of the fixture-lit WMO interior lane — a
//! plug-and-play bridge from the `interiorLight` / `interiorAmbient` / `interiorFill` /
//! `interiorExposure` cvars ([`VideoConfig`]) to benilla-world's [`DynamicInteriors`] resource,
//! which `global_light` packs for `static_gx.wgsl`'s `interior_room_light` (WMO interior surfaces
//! AND interior props light from the room's live fixtures instead of the MOCV bake / the baked prop
//! probe). Independent of the shadow lanes: removing this plugin leaves the resource at its
//! shipped defaults; `interiorLight 0` restores the faithful baked interior path.
//!
//! The knobs are LIVE — `/script SetCVar("interiorExposure", 2)` in chat retunes the room without
//! a rebuild, which is how the defaults were found.
//!
//! MONKEY (fire GO lights): the same bridge also carries `fireLightGain` →
//! [`benilla_world::lighting::FireLightGain`]. Same shape, same guard, same liveness requirement —
//! and `fireLightGain 0` is the kill switch for the synthesised-fire lane the way `interiorLight 0`
//! is for this one.
//!
//! MONKEY (spellLightGain): and `spellLightGain` → [`benilla_world::lighting::SpellLightGain`],
//! the third of the same shape.
//!
//! MONKEY (moon shadows): and `moonShadowStrength` →
//! [`benilla_world::lighting::MoonShadowStrength`], the fourth. Same shape, same guard, same
//! liveness requirement — and `0` is that lane's faithful null the way `fireLightGain 0` is the
//! synthesised-fire lane's kill switch. It is what the Advanced Graphics page's Moon Shadows row
//! and the Off/Low presets reach.
//!
//! Water quality and lava glow use the same guarded bridge into their renderer resources.
use benilla_assets::WaterQuality;
use benilla_world::lighting::{DynamicInteriors, FireLightGain, LavaLightGain, MoonShadowStrength, SpellLightGain};
use bevy::prelude::*;
use crate::video::VideoConfig;
pub(crate) struct DynamicInteriorPlugin;
impl Plugin for DynamicInteriorPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, bridge);
}
}
/// Publish the cvars to benilla-world when they change. Guarded so the resource's change ticks
/// stay quiet on idle frames.
///
/// MONKEY (fire GO lights): `fireLightGain` rides along here rather than in a module of its own —
/// it is the same shape (a live video cvar → a benilla-world resource the light packer reads) and
/// the same one-line guard, and a second plugin to carry one `f32` is not worth its wiring.
fn bridge(
video: Res<VideoConfig>,
mut out: ResMut<DynamicInteriors>,
mut fire: ResMut<FireLightGain>,
// MONKEY (spellLightGain): the spell lane's own gain rides the same bridge as the fire gain,
// for the same reasons — same shape (a live video cvar → a benilla-world resource the packer
// reads), same one-line guard, and a third plugin to carry a second `f32` is not worth its
// wiring. Kept a SEPARATE resource rather than a field on `DynamicInteriors` because it is not
// an interior knob at all: a fireball down a street takes it exactly as one down a corridor
// does, and `FireLightGain` beside it already stands as the precedent for that.
mut spell: ResMut<SpellLightGain>,
// MONKEY (moon shadows): the night lane's shadow darkness, on the same bridge and for the same
// reasons as the two gains above it — one live `f32`, one benilla-world resource, one guard.
mut moon: ResMut<MoonShadowStrength>,
mut water: ResMut<WaterQuality>,
mut lava: ResMut<LavaLightGain>,
) {
if water.0 != video.water_quality {
water.0 = video.water_quality;
}
if lava.0 != video.lava_light_gain {
lava.0 = video.lava_light_gain;
}
if fire.0 != video.fire_light_gain {
fire.0 = video.fire_light_gain;
}
if spell.0 != video.spell_light_gain {
spell.0 = video.spell_light_gain;
}
if moon.0 != video.moon_shadow_strength {
moon.0 = video.moon_shadow_strength;
}
let want = DynamicInteriors {
enabled: video.interior_light,
ambient: video.interior_ambient,
fill: video.interior_fill,
exposure: video.interior_exposure,
// MONKEY (interior attenuation): the authored-window scale rides the same bridge — the
// light packer folds it into every interior entry's packed reach, so
// `SetCVar("interiorAttenScale", 2)` widens every pool on the very next frame.
atten_scale: video.interior_atten_scale,
// MONKEY (room gate): the packer turns this into a per-light claim head, so
// `SetCVar("interiorRoomGate", 0)` restores the ungated look on the very next frame.
room_gate: video.interior_room_gate,
debug: video.interior_debug,
// MONKEY (flame flicker): the wobble gain rides the same bridge — it is consumed entirely
// on the CPU in `build_light_data`, so `SetCVar("fireFlicker", 0)` freezes every flame on
// the very next frame with no respawn and no shader change.
flicker: video.fire_flicker,
// MONKEY (darkness gains): both dim dials ride this same bridge — they are consumed
// entirely on the CPU in `build_light_data` (folded into the packed rows / the packed
// colours), so `SetCVar("nightGain", 1)` restores the reference night on the very next
// frame with no respawn and no shader change. `night_gain` is an EXTERIOR knob living on an
// interior-named resource for the sake of one bridge rather than two; the packer reads it
// ungated by `enabled`.
night_gain: video.night_gain,
interior_gain: video.interior_gain,
// MONKEY (enclosed day floor): the daylight floor rides the same bridge, but is consumed in
// the SHADER (it rides the packed `wmo_fog_params.w` fraction) rather than folded on the
// CPU — the packer's only job is to put it in the lane.
daylight: video.interior_daylight,
// MONKEY (bake floor): the bake floor rides the same bridge. Half CPU, half shader: the
// packer folds `interiorGain` in and puts the product in the `sh_c16.w` fraction, and the
// two interior lanes read it from there — so `SetCVar("interiorBakeFloor", 0)` restores
// the pre-feature look on the very next frame with no respawn and no pipeline rebuild.
bake_floor: video.interior_bake_floor,
};
if *out != want {
*out = want;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn water_and_lava_bridge_publishes_changes_without_dirtying_idle_frames() {
let mut app = App::new();
app.init_resource::<VideoConfig>()
.init_resource::<DynamicInteriors>()
.init_resource::<FireLightGain>()
.init_resource::<SpellLightGain>()
.init_resource::<MoonShadowStrength>()
.init_resource::<WaterQuality>()
.init_resource::<LavaLightGain>()
.add_plugins(DynamicInteriorPlugin);
assert_eq!(app.world().resource::<WaterQuality>().0, VideoConfig::default().water_quality);
assert_eq!(app.world().resource::<LavaLightGain>().0, VideoConfig::default().lava_light_gain);
app.update();
for (water, lava) in [(0, 0.0), (2, 3.25), (1, 1.0)] {
{
let mut video = app.world_mut().resource_mut::<VideoConfig>();
video.water_quality = water;
video.lava_light_gain = lava;
}
app.world_mut().run_schedule(Update);
assert_eq!(app.world().resource::<WaterQuality>().0, water);
assert_eq!(app.world().resource::<LavaLightGain>().0, lava);
app.world_mut().clear_trackers();
app.world_mut().run_schedule(Update);
assert!(!app.world().resource_ref::<WaterQuality>().is_changed());
assert!(!app.world().resource_ref::<LavaLightGain>().is_changed());
}
}
}