diff --git a/LIGHTING.md b/LIGHTING.md new file mode 100644 index 00000000..0a7ed62b --- /dev/null +++ b/LIGHTING.md @@ -0,0 +1,69 @@ +# benilla-lighting + +A dynamic light and shadow system for [benilla](https://github.com/samwhosung/benilla), the Rust + +Bevy reimplementation of the 1.12.1 client. This branch is upstream benilla (merged at +`66388278`) plus the lighting work, and nothing else. It needs no data changes: everything is +derived from what a 1.12 install already carries (WMO `MOLT` lights, `MOCV` vertex colour, portals, +M2 particle emitters, `Light.dbc`). + +Everything here is a departure from the reference client's look, so every lane has a live cvar and +most can be switched off. Code is tagged `MONKEY ()` with the rationale and the measurements +behind each constant. + +## What it does + +| Lane | What you see | Main switch | +|---|---|---| +| Sun shadows, characters | real silhouettes for units instead of the oval blob | `characterShadows` | +| Sun shadows, world | trees, buildings and alpha-tested foliage cast; baked terrain shadows step aside | `worldShadows` | +| Dynamic interiors | WMO rooms are lit per fragment by their own fixtures with a soft falloff, gated per room so light does not leak through walls | `interiorLight`, `interiorRoomGate` | +| Synthesised lights | torches, braziers, lanterns, campfires and lampposts emit a light in the colour of their flame, with flicker | `fireLightGain`, `fireFlicker` | +| Torch shadows | cube-map shadows from point lights onto buildings, models and terrain, with a contact-hardening penumbra and a shadow floor | `interiorShadows`, `exteriorShadows`, `torchShadowStrength`, `interiorShadowSoft` | +| Daylight and doorways | calibrated fixtures at doors, windows and open boundaries carry daylight into a room, and doorways between rooms bleed light | `interiorDaylight`, env `WOW_DAYLIGHT`, `WOW_BLEED` | +| Spell and firework lights | fire, holy and fel effects light their surroundings for their lifetime; frost, nature, arcane and shadow do not | `spellLightGain`, env `WOW_SPELL_LIGHT=0` | +| Night and interior level | global dimming of the night sky term and of interior ambient | `nightGain`, `interiorGain`, `interiorBakeFloor` | + +The dev build has a panel for all of it: **Ctrl+Shift+D → Lighting & shadows**, with Dim / Default / +Bright presets. The Video options page gains Real Character Shadows, World Shadows and Shadow +Distance. + +## Where the code lives + +- **Shaders**: `crates/benilla-assets/src/shaders/shadow_hook.wgsl` (shared shadow sampling), + `crates/benilla-world/src/shaders/torch_depth.wgsl`, and the lighting lanes inside + `static_gx.wgsl`, `wow_model.wgsl` and `terrain.wgsl`. The three receivers mirror each other; the + comments say where. +- **World** (`crates/benilla-world/src`): `lighting/global_light.rs` (light table packer, lanes, + room claims, gains), `lighting/daylight.rs`, `lighting/flicker.rs`, `static_gx/torch_depth.rs` + (cube-map cache), `static_gx/shadow.rs`, and the light spawning in `terrain_stream/spawn/`. +- **Data rules** (`crates/benilla-formats/src`): `fire_light.rs` (which models emit light, their + colour and reach, the spell school rules) and `room_claim.rs` (which rooms a light may light). +- **App** (`crates/benilla-app/src`): `torch_shadow.rs` (caster selection), `shadow_core.rs`, + `character_shadow.rs`, `world_shadow.rs`, `blob_shadow.rs`, `entities/carried_light.rs`, + `entities/spell_fx/lifecycle.rs`, `dynamic_interior.rs`, `debug_panel/lighting_controls.rs`, and + the cvars in `cvars.rs` / `video.rs`. +- **Tools**: `benilla-extract wmolights [--verts ]`, `wmolamps`, `m2firescan` + print the inputs the system works from (groups, batch classes, portals, claims, flame emitters). + +## Invariants worth knowing before editing + +- The shared light buffer stays 8528 bytes and the torch table 6416 bytes; both are mirrored in + three shaders and pinned by tests. +- World lights use upstream's `WorldPointLight`, never Bevy's `PointLight`. +- WGSL only fails at pipeline creation, so a shader edit is verified by running a world scene, not + by `cargo check`. +- Exterior point lights are selected per draw unit (12 slots, ranked against the chunk's box); the + cube-map occlusion is evaluated for the nearest three. + +## Build + +```bash +cargo build --release -p benilla +``` + +Debug tracing: `WOW_TORCH_TRACE=1`, `WOW_POINTS_DUMP=1`, `WOW_SHADOW_TRACE=1`, and the cvar +`interiorDebug` 1..4. + +## Licence + +Same as upstream: MIT OR Apache-2.0. diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml index 9619b109..d9d432eb 100644 --- a/crates/benilla-app/assets/ui/OptionsFrame.xml +++ b/crates/benilla-app/assets/ui/OptionsFrame.xml @@ -804,6 +804,20 @@ .. "which it could only do in exclusive fullscreen. benilla applies it to the " .. "finished image instead, so it works in both display modes." + BENILLA_TOOLTIP_CHARACTER_SHADOWS = + "Renders real dynamic silhouettes for players, NPCs, creatures, and mounts, replacing " + .. "the legacy oval blob. Matches Wrath's character-only shadow tier. This uses " + .. "additional GPU time and memory." + + BENILLA_TOOLTIP_WORLD_SHADOWS = + "Casts real dynamic shadows from the static world (trees, buildings, and foliage) " + .. "following the sun, and switches off the baked terrain shadows they replace. " + .. "Independent of Real Character Shadows. This uses additional GPU time and memory." + + BENILLA_TOOLTIP_SHADOW_DISTANCE = + "How far realtime shadows are drawn, in yards. Higher covers more ground but spreads " + .. "the shadow map thinner (softer edges) and costs more. Applies on Okay/Apply." + OPTIONS_PAGE_ROWS = { -- Seven rows became sixteen at 2180, and the nine that arrived were not new -- settings: every one names a CVar this client already registered AND read, with no @@ -836,7 +850,9 @@ Graphics = { "RowUiScale", "RowFarclip", "RowWorldDetail", "RowBrightness", "RowWeatherDensity", - "RowDisplayMode", "RowVerticalSync", "RowRenderScale", + "RowDisplayMode", "RowVerticalSync", + "RowCharacterShadows", "RowWorldShadows", "RowShadowDistance", + "RowRenderScale", }, -- Player Guild Names (2180) rides as a CHILD of Player Names, which is the -- reference's own relationship twice over: its CheckButton22 hangs off 21 with a +26 @@ -3083,11 +3099,48 @@ "OPTION_TOOLTIP_VERTICAL_SYNC") - + + + OptionsRow_OnLoad(self, "characterShadows", "Real Character Shadows", + "BENILLA_TOOLTIP_CHARACTER_SHADOWS") + + + + + + + + + OptionsRow_OnLoad(self, "worldShadows", "World Shadows", + "BENILLA_TOOLTIP_WORLD_SHADOWS") + + + + + + + + + -- Realtime-shadow render distance (yd): the cascade + -- range + caster reach. 40-200, matching the CVar's + -- own clamp (SHADOW_DISTANCE_RANGE). DEFERRED to + -- Apply so a drag doesn't re-fit the cascade + recollect + -- casters on every tick (one rebuild on commit instead). + OptionsRow_OnLoad(self, "shadowDistance", "Shadow Distance", + "BENILLA_TOOLTIP_SHADOW_DISTANCE") + OptionsSliderRow_Setup(self, 40, 200, 10, "yards") + self.deferred = 1 + + + + + + + -- Render Scale (decision 1639) -- benilla's own -- row, no 1.12 counterpart: the reference renders diff --git a/crates/benilla-app/src/blob_shadow.rs b/crates/benilla-app/src/blob_shadow.rs index d9b01c8a..cacc5293 100644 --- a/crates/benilla-app/src/blob_shadow.rs +++ b/crates/benilla-app/src/blob_shadow.rs @@ -99,9 +99,11 @@ use bevy::prelude::*; use crate::creature_anim::AnimData; use crate::net::{Embodied, NetEntity}; use benilla_world::decal::{DecalFrame, WorldDecal}; +use benilla_world::lighting::WowLighting; use benilla_world::particles::buffer::{begin_effect_frame, EffectVertex}; use benilla_world::schedule::WorldStage; use benilla_world::view::WorldCamera; +use benilla_world::wmo_portal::UnitWmoRoom; /// The reference's shadow disc (`Textures\ShadowBlob.blp`, wow-re unit-blob-shadow RE): grayscale /// radial blob (gray-160 core → white rim) under a binary alpha disc, multiplied onto the ground. @@ -112,6 +114,10 @@ const BOX_CLAMP: f32 = 5.0; /// Degenerate-box epsilon (the reference's `[0x8029d4]` = 2.384e-7): a zero horizontal extent is /// the no-op exit — no shadow. const DEGENERATE_EPS: f32 = 2.384e-7; +/// MONKEY (night blob): the realtime lane's edge band — mirrors `SHADOW_EDGE_BAND` in +/// `shadow_hook.wgsl`, where the realtime shadow lightens over the last yards of the cascade. +/// The oval fades back IN across the same band, so the handover is a crossfade at both ends. +const SHADOW_EDGE_BAND: f32 = 14.0; /// One unit's shadow record (a top-level entity — no render components; the cached projection /// rides the effect stream). Despawned when the owner goes. @@ -229,11 +235,18 @@ fn sync_shadows( /// reaches zero, or no receiving surface is in the box (the reference's no-ground gate). #[allow(clippy::type_complexity)] fn update_shadows( + video: Res, time: Res