Files
benilla-octo/crates/benilla-app/src/dynamic_interior.rs
T
pkuzic f545081e9f MONKEY (planar water): planar replaces screen-space reflections - the Water Reflections option is gone
Owner: planar is cheap enough (~1 ms CPU, GPU about even) to replace SSR outright. High water
always reflects with the planar mirror; the waterReflections cvar, its preset rows, the options
row (EN/RU) and the debug knob are removed. The screen-space march stays only as the mirror's
fallback for water at another height than the mirror plane; WOW_WATER_REFLECT=0 remains a dev
A/B switch.
2026-10-09 19:06:59 +01:00

195 lines
9.4 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, EmissiveTier, 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>,
// MONKEY (post): bloom also arms the HDR source hooks through the shared light blob.
mut emissive: ResMut<EmissiveTier>,
// GFX (moonlight): the additive moon term's dial (Option: absent in headless tests).
moon_light: Option<ResMut<benilla_world::lighting::MoonLight>>,
) {
if let Some(mut moon_light) = moon_light {
if moon_light.0 != video.moon_light {
moon_light.0 = video.moon_light;
}
}
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 emissive.0 != video.bloom {
emissive.0 = video.bloom;
}
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;
}
// MONKEY (fix-daylight): `daylightWindowSplit` (an atomic store; seeds read it at load).
benilla_world::lighting::set_window_split(video.daylight_window_split);
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.
// MONKEY (daylight): `WOW_INTERIOR_DAYLIGHT=<0..1>` overrides it in a hermetic capture,
// which reads no config.toml (dev builds only; read once).
daylight: capture_daylight().unwrap_or(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;
}
}
/// MONKEY (daylight): the capture-only `interiorDaylight` override (see the bridge above).
fn capture_daylight() -> Option<f32> {
static V: std::sync::OnceLock<Option<f32>> = std::sync::OnceLock::new();
*V.get_or_init(|| {
// MONKEY (integration): the dev door is `run_mode` (decision 1179).
if !crate::run_mode::dev_affordances() || std::env::var_os("WOW_CAPTURE").is_none() {
return None;
}
std::env::var("WOW_INTERIOR_DAYLIGHT")
.ok()
.and_then(|v| v.parse::<f32>().ok())
.filter(|v| v.is_finite())
.map(|v| v.clamp(0.0, 1.0))
})
}
#[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>()
.init_resource::<EmissiveTier>()
.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());
}
}
}