From b2353797d7857fdef53550685116a8ef44901650 Mon Sep 17 00:00:00 2001 From: pkuzic Date: Sat, 5 Sep 2026 17:48:06 +0100 Subject: [PATCH] MONKEY: keep the blob shadow for indoor units under Real Character Shadows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An indoor unit was left with NO ground shadow when characterShadows was on: the realtime sun shadow is gated out of WMO interiors (correct — no sun reaches a sealed room), and the oval blob was hidden for every unit. So indoor characters floated. Now the blob is hidden only for OUTDOOR units (where the realtime cast owns them); INDOOR units (UnitWmoRoom::room() = Some) keep the soft oval, so they stay grounded. Folded the old blanket early-return into a per-unit gate in the projection loop. Confirmed in-game. Default path (both cvars off) unchanged. Co-Authored-By: Claude Opus 4.8 --- crates/benilla-app/src/blob_shadow.rs | 37 +++++++++++++++------------ 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/crates/benilla-app/src/blob_shadow.rs b/crates/benilla-app/src/blob_shadow.rs index c511e27f..5acf0792 100644 --- a/crates/benilla-app/src/blob_shadow.rs +++ b/crates/benilla-app/src/blob_shadow.rs @@ -70,6 +70,7 @@ use benilla_world::model_fade::{fade_alpha, RenderFade}; 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. @@ -204,6 +205,10 @@ fn update_shadows( // pass 2 is not in the reference's scene, so it casts nothing (decision 1277). The // election writes the ROOT's `Visibility`; this is that verdict after propagation. Option<&InheritedVisibility>, + // …and the unit's WMO room. `Some(room)` = indoors — the realtime SUN shadow is gated + // out of interiors, so an INDOOR unit keeps its oval even when `characterShadows` is on + // (otherwise it would have no ground shadow at all). Absent / no-room = outdoors. + Option<&UnitWmoRoom>, ), Without, >, @@ -220,21 +225,12 @@ fn update_shadows( // report, answerable from a log instead of a debugger. mut census_at: Local, ) { - // `characterShadows 1`: the realtime shadow-map path owns UNIT shadows — every record hides so a - // unit never wears the oval underneath its cast silhouette. (This keys on the CHARACTER lane, not - // the world lane: the oval is a character's shadow, so `worldShadows` alone must leave it be.) - // The records themselves stay (spawned by `sync_shadows` as usual) and `hide` drops each cache - // key, so flipping the cvar back rebuilds every projection on the next frame. With the cvar off — - // the shipped default — this gate is a single false branch and the lane below is the untouched - // reference path. - if video.character_shadows { - for (_, mut key, mut verts) in &mut shadows { - if key.shown || !verts.0.is_empty() { - hide(&mut key, &mut verts); - } - } - return; - } + // `characterShadows 1`: the realtime shadow-map path owns OUTDOOR unit shadows — the per-unit + // gate in the loop below hides the oval for outdoor units so they don't wear both, while INDOOR + // units keep it (the realtime sun shadow is gated out of interiors, so an indoor unit would + // otherwise have no ground shadow at all). (This keys on the CHARACTER lane, not the world lane: + // the oval is a character's shadow, so `worldShadows` alone must leave it be.) With the cvar off + // — the shipped default — that gate is a single false branch and this is the reference path. let now = time.elapsed_secs(); let census = now >= *census_at; if census { @@ -259,12 +255,21 @@ fn update_shadows( let surface_count = decals.receiver_count(); for (shadow, mut key, mut verts) in &mut shadows { n_total += 1; - let Ok((unit, anims, is_self, mount_child, drawn)) = owners.get(shadow.owner) else { + let Ok((unit, anims, is_self, mount_child, drawn, room)) = owners.get(shadow.owner) else { // sync_shadows despawns next frame; keep it cleared meanwhile. hide(&mut key, &mut verts); n_no_owner += 1; continue; }; + // `characterShadows`: the realtime cast owns OUTDOOR units, so hide the oval there (no + // double shadow). INDOOR units (`UnitWmoRoom::room()` = Some) keep it — the realtime sun + // shadow is gated out of interiors, so without this an indoor unit has no ground shadow at + // all. Absent room component / no-room claim = outdoors. + let indoors = room.is_some_and(|r| r.room().is_some()); + if video.character_shadows && !indoors { + hide(&mut key, &mut verts); + continue; + } // An owner that is not drawn casts nothing. The director's report from inside Caverns of // Time (decision 1277): the exterior election had correctly stopped drawing the Tanaris // mobs overhead, and their shadows carried on being projected onto the cavern floor,