MONKEY: keep the blob shadow for indoor units under Real Character Shadows

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 <noreply@anthropic.com>
This commit is contained in:
pkuzic
2026-09-05 17:48:06 +01:00
co-authored by Claude Opus 4.8
parent b9c84be4f4
commit 44e09effba
+21 -16
View File
@@ -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<BlobShadow>,
>,
@@ -220,21 +225,12 @@ fn update_shadows(
// report, answerable from a log instead of a debugger.
mut census_at: Local<f32>,
) {
// `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,