From 4c15ab4bddcc2650ffb34be40f3fb8190e9572dc Mon Sep 17 00:00:00 2001 From: chairmanpow Date: Sat, 3 Oct 2026 16:37:38 +0100 Subject: [PATCH] shadow: skin the character caster on the GPU, so a crowd costs no CPU skin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The character shadow lane CPU-skinned every admitted unit into one merged Mesh asset and re-uploaded it each rebuild — the documented expensive half of the lane's ~3 ms, and the part that scales with players. Replace it with one GPU-skinned caster per admitted creature part: the part's own skinned mesh and MeshTag, drawn through a new SkinnedShadowCasterMaterial whose prepass vertex skins from the shared rig palette (the same rows wow_model reads). Pose changes are free; only membership is reconciled per frame. Palette region offsets are pushed as shader defs, so the caster shader reads the shared light buffer's tail as a flat array and needs no copy of the WowLight layout. --- crates/benilla-app/src/character_shadow.rs | 208 +++++++++--------- crates/benilla-app/src/shaders.rs | 3 +- .../src/shaders/shadow_caster_skinned.wgsl | 84 +++++++ crates/benilla-app/src/shadow_core.rs | 100 ++++++++- crates/benilla-world/src/rig_palette.rs | 6 +- 5 files changed, 286 insertions(+), 115 deletions(-) create mode 100644 crates/benilla-app/src/shaders/shadow_caster_skinned.wgsl diff --git a/crates/benilla-app/src/character_shadow.rs b/crates/benilla-app/src/character_shadow.rs index cdb5452f..b0477447 100644 --- a/crates/benilla-app/src/character_shadow.rs +++ b/crates/benilla-app/src/character_shadow.rs @@ -1,24 +1,27 @@ //! The CHARACTER shadow lane — a thin plug-in on top of [`super::shadow_core`]. //! -//! It owns nothing but its own per-frame caster (players, NPCs, creatures, mounts). When -//! `characterShadows` is on it declares demand to the shared rig ([`ShadowDemand`]); while the rig -//! is live it collects the CREATURE entity parts into its caster using the rig's shared material and -//! the per-frame facts the rig publishes ([`ShadowFrame`]). It does not know [`super::world_shadow`] -//! exists — remove either lane and the other is untouched. +//! It owns the per-unit caster for players, NPCs, creatures and mounts. When `characterShadows` is +//! on it declares demand to the shared rig ([`ShadowDemand`]); while the rig is live it keeps one +//! GPU-skinned caster per admitted creature part, drawn through [`SkinnedShadowCasterMaterial`]. +//! +//! The caster carries the part's own skinned mesh and `MeshTag`, so it skins in the shadow pass's +//! VERTEX stage from the shared rig palette the forward model already uploads — no per-frame CPU +//! skinning, no merged mesh, no re-upload. Pose changes are free: only membership (spawn/despawn, +//! range, occluder) is reconciled here. It does not know [`super::world_shadow`] exists. use bevy::ecs::entity::EntityHashSet; -use bevy::pbr::MeshMaterial3d; +use bevy::mesh::MeshTag; +use bevy::platform::collections::HashMap; use bevy::prelude::*; -use benilla_assets::materials::WowModelMaterial; use benilla_world::billboard::BillboardCard; -use benilla_world::interact::PickMesh; +use benilla_world::lighting::SharedLightBuffer; use benilla_world::model_render::{ModelPart, ShadowOccluder}; -use benilla_world::rig_palette::{RigPalettes, RigPart, RigSkin}; +use benilla_world::rig_palette::RigPart; use crate::shadow_core::{ - collect_entity_geometry, empty_shadow_mesh, restore_mesh_buffers, shadow_trace, - spawn_solid_caster, take_mesh_buffers, RebuildRate, ShadowDemand, ShadowFrame, ShadowSet, + casts_realtime_shadow, shadow_trace, spawn_skinned_caster, ShadowDemand, ShadowFrame, + ShadowSet, SkinnedShadowCasterMaterial, }; use crate::video::VideoConfig; @@ -32,141 +35,132 @@ impl Plugin for CharacterShadowPlugin { } } -/// MONKEY (moon shadows): roots/ancestors with triangles in the LAST completed caster build. -/// Blobs consume this in PostUpdate, before this frame's Last rebuild: a newly uploaded proxy -/// gets a render frame before its oval yields. Cadence skips retain the same geometry/verdict. +/// MONKEY (moon shadows): roots/ancestors with a live caster in the LAST completed reconciliation. +/// Blobs consume this in PostUpdate, before this frame's Last pass: a newly spawned caster gets a +/// render frame before its oval yields. #[derive(Resource, Default)] pub(crate) struct CharacterShadowReady(pub EntityHashSet); -/// The character lane's retained per-frame caster. +/// The character lane's retained casters: part entity → its shadow-caster entity. #[derive(Resource, Default)] struct CharacterLane { - caster: Option, - mesh: Option>, - /// [`shadow_trace`]'s change detector: (tris, admitted, rejected). + casters: HashMap, + material: Option>, + /// [`shadow_trace`]'s change detector: (admitted, rejected, casters). traced: (u32, u32, u32), - /// MONKEY (sun shadow perf): the `characterShadowRate` cadence gate — see the rebuild comment - /// in [`collect_character_shadows`]. - rate: RebuildRate, +} + +/// Drop every caster and the shared material handle. +fn teardown(commands: &mut Commands, lane: &mut CharacterLane) { + for (_, caster) in lane.casters.drain() { + commands.entity(caster).despawn(); + } + lane.material = None; } #[allow(clippy::too_many_arguments)] fn collect_character_shadows( video: Res, - time: Res