From a3badd0fbaf971c118f9fbfe352293e742c4f811 Mon Sep 17 00:00:00 2001 From: pkuzic Date: Fri, 9 Oct 2026 16:53:12 +0100 Subject: [PATCH] MONKEY (mcsh model shade): models drop the MCSH 0.5 sun dim while the world shadow lane replaces the bake A doodad standing on its own baked terrain shadow (Lakeshire gryphon roosts, most forest trees) was dimmed twice by day: by the MCSH 0.5 sun band AND by the realtime shadow map, on ground that no longer shows the bake. wow_model.wgsl and static_gx.wgsl now use terrain.wgsl's own gate (sh_c16.w, weighted by the sun lane so the bake returns at dusk). Lane off = reference bits. Capture scenarios lakeshire-statue*/lakeshire-gryphons*, WOW_CAPTURE_EYE/LOOK/MINUTE re-aim overrides. --- LIGHTING.md | 2 +- crates/benilla-app/src/capture/mod.rs | 18 ++++++++++++-- crates/benilla-app/src/capture/scenarios.rs | 23 ++++++++++++++++++ .../benilla-assets/src/shaders/wow_model.wgsl | 12 +++++++++- .../src/lighting/global_light.rs | 24 +++++++++++++++++++ .../benilla-world/src/shaders/static_gx.wgsl | 8 ++++++- 6 files changed, 82 insertions(+), 5 deletions(-) diff --git a/LIGHTING.md b/LIGHTING.md index 53e4504e..be9300cb 100644 --- a/LIGHTING.md +++ b/LIGHTING.md @@ -15,7 +15,7 @@ behind each constant. | 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; the static caster cache also refreshes when streamed geometry arrives, so a standing camera picks up late trees (`world_shadow.rs::refresh_streamed_shadows`, independent of volumetric fog) | `worldShadows` | +| Sun shadows, world | trees, buildings and alpha-tested foliage cast; baked terrain shadows step aside, and so does the MCSH 0.5 sun dim on models standing on them (`wow_model.wgsl` / `static_gx.wgsl`, terrain's gate and dusk weight); the static caster cache also refreshes when streamed geometry arrives, so a standing camera picks up late trees (`world_shadow.rs::refresh_streamed_shadows`, independent of volumetric fog) | `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` | diff --git a/crates/benilla-app/src/capture/mod.rs b/crates/benilla-app/src/capture/mod.rs index e6130035..1bead6e1 100644 --- a/crates/benilla-app/src/capture/mod.rs +++ b/crates/benilla-app/src/capture/mod.rs @@ -714,7 +714,21 @@ fn pin_scene( return; }; debug.lighting.follow_server_time = false; - debug.lighting.manual_minute = scenario.minute; + // MONKEY (capture framing): `WOW_CAPTURE_MINUTE=<0..1440>`, `WOW_CAPTURE_EYE=x,y,z` and + // `WOW_CAPTURE_LOOK=x,y,z` (raw WoW coords) re-aim a scenario without a rebuild, for framing a + // reported spot; a scenario's baseline never sets them. + let triple = |var: &str| { + std::env::var(var).ok().and_then(|s| { + let v: Vec = s.split(',').filter_map(|x| x.trim().parse().ok()).collect(); + (v.len() == 3).then(|| [v[0], v[1], v[2]]) + }) + }; + debug.lighting.manual_minute = std::env::var("WOW_CAPTURE_MINUTE") + .ok() + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(scenario.minute); + let scene_eye = triple("WOW_CAPTURE_EYE").unwrap_or(scenario.eye); + let scene_look = triple("WOW_CAPTURE_LOOK").unwrap_or(scenario.look); // `WOW_MM_PROBE=x,y,z` (raw WoW coords) places an active player there, so the interior // minimap renders server-less, with the camera above so the WMO streams in around it. @@ -748,7 +762,7 @@ fn pin_scene( * Quat::from_rotation_x(-req.el_deg.to_radians()); (look + orbit * (Vec3::Z * req.dist), look) } - _ => (wow_to_bevy(scenario.eye), wow_to_bevy(scenario.look)), + _ => (wow_to_bevy(scene_eye), wow_to_bevy(scene_look)), }; for mut t in &mut cam { *t = Transform::from_translation(eye).looking_at(look, Vec3::Y); diff --git a/crates/benilla-app/src/capture/scenarios.rs b/crates/benilla-app/src/capture/scenarios.rs index 9d254c9c..429f9af0 100644 --- a/crates/benilla-app/src/capture/scenarios.rs +++ b/crates/benilla-app/src/capture/scenarios.rs @@ -1198,6 +1198,29 @@ pub(super) const ON_DEMAND: &[Scenario] = &[ name: "redridge-lake", map: Some(MAP_AZEROTH), eye: [-9350.0, -2340.0, 82.0], look: [-9500.0, -2650.0, 50.0], minute: 720, ui: None, }, + // MONKEY (gfx-1009 unlit): Lakeshire's report sites. The sunken `lionstatue.mdx` (MDDF uid + // 83242, (-9355.5, -2246.1, 47.9), ~8 yd under the 56.16 surface) seen from the water surface + // and from under it, and the gryphon master's two `gryphonroost01.mdx` (uids 325784/325785). + Scenario { + name: "lakeshire-statue", map: Some(MAP_AZEROTH), + eye: [-9318.0, -2238.0, 59.0], look: [-9356.0, -2247.0, 49.0], minute: 720, ui: None, + }, + Scenario { + name: "lakeshire-statue-dusk", map: Some(MAP_AZEROTH), + eye: [-9318.0, -2238.0, 59.0], look: [-9356.0, -2247.0, 49.0], minute: 1170, ui: None, + }, + Scenario { + name: "lakeshire-statue-under", map: Some(MAP_AZEROTH), + eye: [-9343.0, -2254.0, 52.0], look: [-9356.0, -2246.0, 48.5], minute: 720, ui: None, + }, + Scenario { + name: "lakeshire-gryphons", map: Some(MAP_AZEROTH), + eye: [-9407.0, -2250.0, 72.5], look: [-9422.5, -2237.0, 69.5], minute: 720, ui: None, + }, + Scenario { + name: "lakeshire-gryphons-dusk", map: Some(MAP_AZEROTH), + eye: [-9407.0, -2250.0, 72.5], look: [-9422.5, -2237.0, 69.5], minute: 1170, ui: None, + }, // Duskwood: the road into Darkshire, the zone's short dark fog. Scenario { name: "duskwood-road", map: Some(MAP_AZEROTH), diff --git a/crates/benilla-assets/src/shaders/wow_model.wgsl b/crates/benilla-assets/src/shaders/wow_model.wgsl index 815fa8bf..6c80c27a 100644 --- a/crates/benilla-assets/src/shaders/wow_model.wgsl +++ b/crates/benilla-assets/src/shaders/wow_model.wgsl @@ -1424,7 +1424,17 @@ fn fragment(in: WowVsOut, @builtin(front_facing) is_front: bool) -> WowFragOut { let mat_shade = select(0.0, 1.0, m.sun_scale.x < 0.5); let shade_t = max(mat_shade, inst_shade); let mid_band = m.sun_scale.x >= 0.5 && m.sun_scale.x < 0.85; - let intensity = min(select(mix(2.5, 0.5, shade_t), 1.0, mid_band), 1.0); + let intensity_ref = min(select(mix(2.5, 0.5, shade_t), 1.0, mid_band), 1.0); + // MONKEY (mcsh model shade): the 0.5 band is the MCSH bit under the model, the same baked + // shadow `terrain.wgsl` drops while the realtime world lane is on (`sh_c16.w > 0.5`, weighted + // by the sun lane so the bake returns at night). Keeping it here left a doodad standing on its + // own baked shadow (Lakeshire's gryphon roosts) dimmed twice, by the bake AND the realtime map, + // on ground that no longer shows the bake. Only that band is < 1, so lit, matte, interior and + // rig lanes are untouched; with the lane off this is the reference value bit for bit. + let mcsh_suppress = select(0.0, shadow_hook::sun_shadow_w(wow_light.fog_params.z), + wow_light.sh_c16.w > 0.5); + let intensity = select(intensity_ref, mix(intensity_ref, 1.0, mcsh_suppress), + mcsh_suppress > 0.0); // One `intensity` multiply covers every sun band (never I²); the c10 `.w` ambient does not // scale. `pack_model_core_rows` packs the same closed form. let sun_dc = wow_light.grade.yzw * intensity; diff --git a/crates/benilla-world/src/lighting/global_light.rs b/crates/benilla-world/src/lighting/global_light.rs index f5ee39d6..90974303 100644 --- a/crates/benilla-world/src/lighting/global_light.rs +++ b/crates/benilla-world/src/lighting/global_light.rs @@ -2836,6 +2836,30 @@ mod tests { } } + /// MONKEY (mcsh model shade): the model receivers drop the MCSH 0.5 sun band on exactly the + /// gate terrain drops its MCSH on (`sh_c16.w > 0.5`, weighted by the sun lane), so a doodad on + /// its own baked shadow is not dimmed twice once the realtime world lane has replaced the bake. + #[test] + fn model_receivers_drop_the_mcsh_band_on_terrains_gate() { + const TERRAIN: &str = include_str!("../../../benilla-assets/src/shaders/terrain.wgsl"); + const MODEL: &str = include_str!("../../../benilla-assets/src/shaders/wow_model.wgsl"); + const STATIC_GX: &str = include_str!("../shaders/static_gx.wgsl"); + let squash = |s: &str| s.split_whitespace().collect::(); + assert!(squash(TERRAIN).contains("letworld_shadow_lane=wow_light.sh_c16.w>0.5;")); + let gate = "letmcsh_suppress=select(0.0,shadow_hook::sun_shadow_w(wow_light.fog_params.z),\ + wow_light.sh_c16.w>0.5);"; + let apply = "letintensity=select(intensity_ref,mix(intensity_ref,1.0,mcsh_suppress),\ + mcsh_suppress>0.0);"; + for (name, shader) in [("wow_model.wgsl", MODEL), ("static_gx.wgsl", STATIC_GX)] { + let s = squash(shader); + assert!( + s.contains(gate), + "{name} must gate the MCSH band like terrain" + ); + assert!(s.contains(apply), "{name} must lift only through the gate"); + } + } + /// MONKEY (enclosed day floor): `interiorDaylight` rides the FRACTION of the interior lane's /// on/off word, and every existing decode of that word must be blind to it. /// diff --git a/crates/benilla-world/src/shaders/static_gx.wgsl b/crates/benilla-world/src/shaders/static_gx.wgsl index e1c2c332..e029fd5a 100644 --- a/crates/benilla-world/src/shaders/static_gx.wgsl +++ b/crates/benilla-world/src/shaders/static_gx.wgsl @@ -2034,10 +2034,16 @@ fn fragment(in: GxVsOut) -> @location(0) vec4 { // Deviation: the `min(I, 1)` cap, as in wow_model.wgsl, since lifting it takes sun-facing // surfaces past 1.0; Matte keeps its own bit so it stays 1.0 if the cap goes. let shade_t = select(1.0, 0.0, (in.word & WORD_SHADE_LIT) != 0u); - let intensity = min( + let intensity_ref = min( select(mix(2.5, 0.5, shade_t), 1.0, (in.word & WORD_MATTE) != 0u), 1.0, ); + // MONKEY (mcsh model shade): the MCSH 0.5 steps aside with terrain's MCSH while the world + // shadow lane is on; see `wow_model.wgsl` (same expression, same gate). + let mcsh_suppress = select(0.0, shadow_hook::sun_shadow_w(wow_light.fog_params.z), + wow_light.sh_c16.w > 0.5); + let intensity = select(intensity_ref, mix(intensity_ref, 1.0, mcsh_suppress), + mcsh_suppress > 0.0); let sun_dc = wow_light.grade.yzw * intensity; let sun_lobe = vec3( wow_light.sh_c10_r.w + sun_dc.x