From e254e6dce8a5f26a57aca864c58e1bfa167404d5 Mon Sep 17 00:00:00 2001 From: pkuzic Date: Mon, 7 Sep 2026 20:12:47 +0100 Subject: [PATCH] MONKEY: dynamic WMO interiors + cube-map torch shadows (surfaces + entities) Interior rooms light per fragment from their authored fixtures instead of the MOCV bake (direct + fill + ambient, soft rolloff; live cvars interiorLight/interiorAmbient/interiorFill/interiorExposure/interiorShadows/ interiorDebug), TRANS/EXT batches blend to the day/night reference at the portal seam, and fixtures admit camera-independently (90 yd). Torch shadows are a from-scratch depth-map renderer (Bevy point shadows are dead under ClusterConfig::None): a shared Depth32Float 512^2 x 24-layer Image + depth-only pipeline + ViewNode rendering the static caster and a per-frame entity caster into 6 cube faces for the nearest <=4 fixtures (reverse-Z, GreaterEqual compare, 4-tap PCF, far fade). WMO surfaces receive through static_gx group 3; entities receive on the wow_model path through WowModelExt bindings 91/92/93 (depth array, comparison sampler, 1616-byte torch table) with a 0.15 yd normal offset against CPU-vs-GPU skinning self-shadow. The entity lane is gated on is_interior OR the new MATTE_INDOOR tag bit (exterior payload bit 14, written by the classifier's Matte law), so a unit whose verdict flickers Bake<->Matte at one spot keeps its room lighting. Co-Authored-By: Claude Fable 5.1 --- crates/benilla-app/src/cvars.rs | 51 +- crates/benilla-app/src/dynamic_interior.rs | 37 ++ crates/benilla-app/src/entities/wmo_props.rs | 6 +- crates/benilla-app/src/lib.rs | 5 + crates/benilla-app/src/pipe_warm/menagerie.rs | 11 +- crates/benilla-app/src/pipe_warm/mod.rs | 6 + crates/benilla-app/src/torch_shadow.rs | 309 +++++++--- crates/benilla-app/src/video.rs | 21 + crates/benilla-assets/src/materials.rs | 37 ++ .../src/shaders/shadow_hook.wgsl | 110 +++- .../benilla-assets/src/shaders/wow_model.wgsl | 151 +++++ crates/benilla-assets/src/world_assets/mod.rs | 15 +- crates/benilla-world/src/assets/mod.rs | 22 +- crates/benilla-world/src/interior.rs | 6 +- .../src/lighting/global_light.rs | 95 ++- crates/benilla-world/src/lighting/mod.rs | 3 +- crates/benilla-world/src/mesh_tag.rs | 37 +- crates/benilla-world/src/model_render.rs | 10 +- .../benilla-world/src/model_render/batch.rs | 53 +- crates/benilla-world/src/particles/model.rs | 8 + crates/benilla-world/src/shaders.rs | 7 +- .../benilla-world/src/shaders/static_gx.wgsl | 206 ++++++- .../src/shaders/torch_depth.wgsl | 16 + crates/benilla-world/src/static_gx/mod.rs | 9 + crates/benilla-world/src/static_gx/render.rs | 88 +++ .../src/static_gx/torch_depth.rs | 569 ++++++++++++++++++ .../src/terrain_stream/spawn/assemble.rs | 6 +- .../src/terrain_stream/spawn/mod.rs | 13 +- 28 files changed, 1774 insertions(+), 133 deletions(-) create mode 100644 crates/benilla-app/src/dynamic_interior.rs create mode 100644 crates/benilla-world/src/shaders/torch_depth.wgsl create mode 100644 crates/benilla-world/src/static_gx/torch_depth.rs diff --git a/crates/benilla-app/src/cvars.rs b/crates/benilla-app/src/cvars.rs index 076154a8..bf3d08e1 100644 --- a/crates/benilla-app/src/cvars.rs +++ b/crates/benilla-app/src/cvars.rs @@ -600,6 +600,41 @@ pub(crate) const REGISTERED: &[Registered] = &[ "1900: benilla's own realtime-shadow render-distance slider; the reference bakes MCSH and \ has no cascade to size", ), + // MONKEY (dynamic interiors): WMO interiors + their props light from the room's LIVE fixtures + // instead of the MOCV bake / the baked prop probe (`static_gx.wgsl` `interior_room_light`; + // bridged by `dynamic_interior`). The three numeric knobs are live-tunable from chat — + // `/script SetCVar("interiorExposure", 2)` — which is how their defaults were found. + ours( + "interiorLight", + "1", + "benilla's own: fixture-lit WMO interiors (0 = the reference's baked interior path)", + ), + ours( + "interiorAmbient", + "0.15", + "benilla's own: interior base ambient, 0..1", + ), + ours( + "interiorFill", + "0.12", + "benilla's own: interior per-fixture bounce gain, 0..2", + ), + ours( + "interiorExposure", + "2.5", + "benilla's own: interior light-budget multiplier before the soft rolloff, 0.25..8", + ), + ours( + "interiorShadows", + "1", + "benilla's own: interior fixtures cast real shadows (Stage B, the nearest few); needs \ + interiorLight", + ), + ours( + "interiorDebug", + "0", + "benilla's own: interior diagnostic overlay — 1 classification, 2 shadow, 3 caster count", + ), // **Display mode** (decisions 1627, 1650) — 1.12's own `gxWindow`, worn since 1650 as modern // Classic's two-entry *Display Mode* dropdown rather than 1.12's *Windowed Mode* checkbox: the // two states 1627 settled on ARE that client's two (its own `Graphics.lua` builds the list from @@ -1085,6 +1120,14 @@ fn apply_to_knobs(name: &str, value: &str, knobs: &mut Knobs) -> bool { knobs.video.shadow_distance = v.clamp(*SHADOW_DISTANCE_RANGE.start(), *SHADOW_DISTANCE_RANGE.end()); } + // MONKEY (dynamic interiors): the interior lane's on/off + knobs, clamped at the edge like + // every other numeric row. `dynamic_interior::bridge` publishes them to benilla-world. + "interiorlight" => knobs.video.interior_light = v != 0.0, + "interiorambient" => knobs.video.interior_ambient = v.clamp(0.0, 1.0), + "interiorfill" => knobs.video.interior_fill = v.clamp(0.0, 2.0), + "interiorexposure" => knobs.video.interior_exposure = v.clamp(0.25, 8.0), + "interiorshadows" => knobs.video.interior_shadows = v != 0.0, + "interiordebug" => knobs.video.interior_debug = (v.max(0.0) as u32).min(3), // Display mode (1627) — a flag like every other checkbox here, and the reference's own // polarity: `1` is WINDOWED (the row is "Windowed Mode"). `video::apply_window_mode` // watches the value and pushes it to the window; nothing else reads it. @@ -1387,7 +1430,7 @@ fn sync_cvars( .collect(), ); let flag = |b: bool| if b { "1" } else { "0" }.to_string(); - let session: [(&str, String); 47] = [ + let session: [(&str, String); 53] = [ ("MasterVolume", sound.master.to_string()), ("SoundVolume", sound.sfx.to_string()), ("MusicVolume", sound.music.to_string()), @@ -1436,6 +1479,12 @@ fn sync_cvars( ("worldShadows", flag(video.world_shadows)), ("characterShadows", flag(video.character_shadows)), ("shadowDistance", video.shadow_distance.to_string()), + ("interiorLight", flag(video.interior_light)), + ("interiorAmbient", video.interior_ambient.to_string()), + ("interiorFill", video.interior_fill.to_string()), + ("interiorExposure", video.interior_exposure.to_string()), + ("interiorShadows", flag(video.interior_shadows)), + ("interiorDebug", video.interior_debug.to_string()), // The reference's polarity: the CVar is `gxWindow`, so `1` is the WINDOWED state. ( "gxWindow", diff --git a/crates/benilla-app/src/dynamic_interior.rs b/crates/benilla-app/src/dynamic_interior.rs new file mode 100644 index 00000000..8d25440e --- /dev/null +++ b/crates/benilla-app/src/dynamic_interior.rs @@ -0,0 +1,37 @@ +//! 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. +use benilla_world::lighting::DynamicInteriors; +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. +fn bridge(video: Res, mut out: ResMut) { + let want = DynamicInteriors { + enabled: video.interior_light, + ambient: video.interior_ambient, + fill: video.interior_fill, + exposure: video.interior_exposure, + debug: video.interior_debug, + }; + if *out != want { + *out = want; + } +} diff --git a/crates/benilla-app/src/entities/wmo_props.rs b/crates/benilla-app/src/entities/wmo_props.rs index ea4e298d..e9e437d5 100644 --- a/crates/benilla-app/src/entities/wmo_props.rs +++ b/crates/benilla-app/src/entities/wmo_props.rs @@ -193,10 +193,11 @@ pub(super) fn spawn_wmo_gameobject_props( time: Res