shadow: skin the character caster on the GPU, so a crowd costs no CPU skin
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.
This commit is contained in:
@@ -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<Entity>,
|
||||
mesh: Option<Handle<Mesh>>,
|
||||
/// [`shadow_trace`]'s change detector: (tris, admitted, rejected).
|
||||
casters: HashMap<Entity, Entity>,
|
||||
material: Option<Handle<SkinnedShadowCasterMaterial>>,
|
||||
/// [`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<VideoConfig>,
|
||||
time: Res<Time>,
|
||||
mut demand: ResMut<ShadowDemand>,
|
||||
frame: Res<ShadowFrame>,
|
||||
mut lane: ResMut<CharacterLane>,
|
||||
mut ready: ResMut<CharacterShadowReady>,
|
||||
parents: Query<&ChildOf>,
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
light: Option<Res<SharedLightBuffer>>,
|
||||
mut materials: ResMut<Assets<SkinnedShadowCasterMaterial>>,
|
||||
parts: Query<
|
||||
(
|
||||
Entity,
|
||||
&PickMesh,
|
||||
&Mesh3d,
|
||||
&MeshTag,
|
||||
&ModelPart,
|
||||
&ShadowOccluder,
|
||||
Option<&GlobalTransform>,
|
||||
Option<&RigPart>,
|
||||
&ShadowOccluder,
|
||||
Option<&MeshMaterial3d<WowModelMaterial>>,
|
||||
),
|
||||
Without<BillboardCard>,
|
||||
>,
|
||||
rigs: Query<&RigSkin>,
|
||||
palettes: Res<RigPalettes>,
|
||||
) {
|
||||
let on = video.character_shadows;
|
||||
// Declare demand so the shared rig stays up while this lane is on (the rig reads it next frame).
|
||||
demand.0 = demand.0 || on;
|
||||
|
||||
// Rig down or lane off → drop our caster so it stops casting.
|
||||
if !(frame.active && on) {
|
||||
// Rig down, lane off, or an off night → drop every caster so none keeps casting.
|
||||
if !(frame.active && on) || frame.suspended {
|
||||
ready.0.clear();
|
||||
if let Some(entity) = lane.caster.take() {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
if let Some(handle) = lane.mesh.take() {
|
||||
meshes.remove(handle.id());
|
||||
}
|
||||
// The mesh this gate was pacing is gone — re-arm so a re-enable builds on its first frame.
|
||||
lane.rate.reset();
|
||||
teardown(&mut commands, &mut lane);
|
||||
return;
|
||||
}
|
||||
// MONKEY (moon shadows): keep the cache but suspend CPU skin/re-upload on feature-off nights.
|
||||
if frame.suspended {
|
||||
ready.0.clear();
|
||||
lane.rate.reset();
|
||||
return;
|
||||
}
|
||||
let Some(material) = frame.material.clone() else {
|
||||
return;
|
||||
let Some(light) = light else {
|
||||
return; // the shared buffer is not up yet; the rig will still be live next frame
|
||||
};
|
||||
if lane.material.is_none() {
|
||||
lane.material = Some(materials.add(SkinnedShadowCasterMaterial {
|
||||
light_buf: light.0.clone(),
|
||||
}));
|
||||
}
|
||||
let material = lane
|
||||
.material
|
||||
.clone()
|
||||
.expect("the material was inserted just above");
|
||||
|
||||
// Animated rigs re-skin every frame and creatures spawn/despawn under lifecycles this lane
|
||||
// can't cheaply observe — so the caster is collected fresh, at a CAPPED cadence.
|
||||
if lane.caster.is_none() {
|
||||
let mesh = meshes.add(empty_shadow_mesh());
|
||||
let entity = spawn_solid_caster(&mut commands, mesh.clone(), material);
|
||||
lane.caster = Some(entity);
|
||||
lane.mesh = Some(mesh);
|
||||
// A brand-new caster is an EMPTY mesh; make sure this frame fills it.
|
||||
lane.rate.reset();
|
||||
ready.0.clear();
|
||||
let mut wanted = EntityHashSet::default();
|
||||
let (reach, light_pos) = (frame.entity_reach, frame.light_position);
|
||||
let (mut admitted, mut rejected) = (0u32, 0u32);
|
||||
for (part, mesh, tag, model_part, occluder, global, rig_part) in &parts {
|
||||
if !casts_realtime_shadow(model_part.kind, model_part.blend, true, false) {
|
||||
continue;
|
||||
}
|
||||
// The camera-independent content gate (toggles, far clip, distance fade, material alpha).
|
||||
if !occluder.0 {
|
||||
rejected += 1;
|
||||
continue;
|
||||
}
|
||||
// GPU skinning needs the part's rig; a part with no palette would render in bind pose.
|
||||
if rig_part.is_none() {
|
||||
rejected += 1;
|
||||
continue;
|
||||
}
|
||||
let anchor = global.map(GlobalTransform::translation).unwrap_or(Vec3::ZERO);
|
||||
if anchor.distance_squared(light_pos) > reach * reach {
|
||||
continue;
|
||||
}
|
||||
admitted += 1;
|
||||
wanted.insert(part);
|
||||
ready.0.insert(part);
|
||||
// The unit root owns its blob; mounts and equipment can be nested below it.
|
||||
ready.0.extend(parents.iter_ancestors(part));
|
||||
if !lane.casters.contains_key(&part) {
|
||||
let caster =
|
||||
spawn_skinned_caster(&mut commands, mesh.0.clone(), material.clone(), tag.0);
|
||||
lane.casters.insert(part, caster);
|
||||
}
|
||||
}
|
||||
// MONKEY (sun shadow perf): the `characterShadowRate` gate. This rebuild is the expensive half
|
||||
// of the ~3 ms the character lane costs — it CPU-skins every admitted unit and then mutates the
|
||||
// `Mesh` asset, and a mutated mesh is re-extracted and re-uploaded (vertices AND indices) to the
|
||||
// GPU. Skipping it simply leaves the previous proxy in place: the shadow MAP is still rendered
|
||||
// from that proxy every frame, so nothing flickers or disappears — a moving unit's silhouette
|
||||
// merely lags by up to 1/rate second.
|
||||
//
|
||||
// Why a rate cap and not change detection: the obvious `Changed<GlobalTransform>` gate does not
|
||||
// work here. A unit's shadow geometry comes from the RIG PALETTE (`append_skinned` reads
|
||||
// `RigPalettes`, the per-frame skinning pose), not from the part's transform — so a standing NPC
|
||||
// whose idle animation is breathing has an unchanged `GlobalTransform` and changed VERTICES.
|
||||
// Gating on the transform would freeze exactly the poses this lane exists to draw. The rate cap
|
||||
// is the honest saving, and it is bounded work rather than a guess about content.
|
||||
if !lane
|
||||
.rate
|
||||
.due(time.elapsed_secs(), video.character_shadow_rate)
|
||||
{
|
||||
return;
|
||||
// A part that left the admit set (destroyed, out of range, occluder off) loses its caster.
|
||||
let stale: Vec<Entity> = lane
|
||||
.casters
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|part| !wanted.contains(part))
|
||||
.collect();
|
||||
for part in stale {
|
||||
if let Some(caster) = lane.casters.remove(&part) {
|
||||
commands.entity(caster).despawn();
|
||||
}
|
||||
}
|
||||
if let Some(handle) = lane.mesh.clone() {
|
||||
if let Some(mesh) = meshes.get_mut(&handle) {
|
||||
let (mut positions, mut indices) = take_mesh_buffers(mesh);
|
||||
let mut built_parts = EntityHashSet::default();
|
||||
let (admitted, rejected) = collect_entity_geometry(
|
||||
&parts,
|
||||
&rigs,
|
||||
&palettes,
|
||||
true, // want_creatures
|
||||
false, // want_environment (the world lane's job)
|
||||
frame.light_position,
|
||||
frame.entity_reach,
|
||||
frame.tall_reach,
|
||||
&mut positions,
|
||||
&mut indices,
|
||||
Some(&mut built_parts),
|
||||
if shadow_trace() {
|
||||
let now = (admitted, rejected, lane.casters.len() as u32);
|
||||
if lane.traced != now {
|
||||
info!(
|
||||
"shadow-trace: character gpu {} casters | creatures admitted {} / rejected {} (was {:?})",
|
||||
lane.casters.len(),
|
||||
admitted,
|
||||
rejected,
|
||||
lane.traced
|
||||
);
|
||||
ready.0.clear();
|
||||
for part in built_parts {
|
||||
ready.0.insert(part);
|
||||
// The unit root owns its blob; mounts and equipment can be nested below it.
|
||||
ready.0.extend(parents.iter_ancestors(part));
|
||||
}
|
||||
let tris = (indices.len() / 3) as u32;
|
||||
restore_mesh_buffers(mesh, positions, indices);
|
||||
if shadow_trace() {
|
||||
let now = (tris, admitted, rejected);
|
||||
if lane.traced != now {
|
||||
info!(
|
||||
"shadow-trace: character {} tris | creatures admitted {} / rejected {} (was {:?})",
|
||||
tris, admitted, rejected, lane.traced
|
||||
);
|
||||
lane.traced = now;
|
||||
}
|
||||
}
|
||||
lane.traced = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ pub(crate) fn plugin(app: &mut App) {
|
||||
bevy::asset::embedded_asset!(app, "shaders/ui_slice_gamma.wgsl");
|
||||
bevy::asset::embedded_asset!(app, "shaders/shadow_caster.wgsl");
|
||||
bevy::asset::embedded_asset!(app, "shaders/shadow_caster_cutout_prepass.wgsl");
|
||||
bevy::asset::embedded_asset!(app, "shaders/shadow_caster_skinned.wgsl");
|
||||
// MONKEY (ao): the contact-shadow pass.
|
||||
bevy::asset::embedded_asset!(app, "shaders/ssao.wgsl");
|
||||
}
|
||||
@@ -50,6 +51,6 @@ mod tests {
|
||||
`shaders::plugin`, or was registered from a file that is not directly under src/"
|
||||
);
|
||||
}
|
||||
assert_eq!(found, 8, "the game's shader set changed size");
|
||||
assert_eq!(found, 9, "the game's shader set changed size");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// GPU-skinned shadow caster: one per animated unit part. It carries the part's own skinned mesh and
|
||||
// MeshTag and skins in the VERTEX stage from the shared light buffer's rig palette — the same rows
|
||||
// `wow_model.wgsl` reads — so the character shadow lane spends no per-frame CPU skinning and uploads
|
||||
// no merged mesh. The forward pass discards it (invisible to the camera); the prepass, which is the
|
||||
// directional-light shadow pass, writes depth from the skinned position.
|
||||
//
|
||||
// The palette region offsets are pushed as shader defs by
|
||||
// `shadow_core::SkinnedShadowCasterMaterial::specialize` from `benilla_world::rig_palette`, so this
|
||||
// file needs no copy of the huge `WowLight` layout: it reads the tail as a flat `array<f32>`.
|
||||
#import bevy_pbr::{
|
||||
mesh_functions,
|
||||
mesh_view_bindings::view,
|
||||
prepass_io::{VertexOutput},
|
||||
view_transformations::position_world_to_clip,
|
||||
}
|
||||
|
||||
// Bevy's standard vertex locations (position @0) plus the owned-palette joints at 10/11, exactly
|
||||
// the layout `SkinnedShadowCasterMaterial::specialize` binds.
|
||||
struct CasterVertex {
|
||||
@builtin(instance_index) instance_index: u32,
|
||||
@location(0) position: vec3<f32>,
|
||||
@location(10) joint_indices: vec4<u32>,
|
||||
@location(11) joint_weights: vec4<f32>,
|
||||
}
|
||||
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(90) var<storage, read> rig_buf: array<f32>;
|
||||
|
||||
// Byte offsets of the slot-keyed regions, divided by 4 to index the flat f32 array.
|
||||
const RIG_TABLE_F: u32 = #{WOW_RIG_TABLE_OFF} / 4u;
|
||||
const RIG_ORIGIN_F: u32 = #{WOW_RIG_ORIGIN_OFF} / 4u;
|
||||
const PALETTE_F: u32 = #{WOW_PALETTE_OFF} / 4u;
|
||||
|
||||
fn rig_slot(instance_index: u32) -> u32 {
|
||||
return (mesh_functions::get_tag(instance_index) >> 19u) & 0x7ffu;
|
||||
}
|
||||
|
||||
// Palette row `row` (0..2) of bone `bone`, as the model shader's `palettes[3*bone + row]`.
|
||||
fn palette_row(bone: u32, row: u32) -> vec4<f32> {
|
||||
let o = PALETTE_F + 4u * (3u * bone + row);
|
||||
return vec4<f32>(rig_buf[o], rig_buf[o + 1u], rig_buf[o + 2u], rig_buf[o + 3u]);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vertex(v: CasterVertex) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
let slot = rig_slot(v.instance_index);
|
||||
let base = u32(rig_buf[RIG_TABLE_F + slot]);
|
||||
let w = v.joint_weights;
|
||||
let b0 = base + v.joint_indices.x;
|
||||
let b1 = base + v.joint_indices.y;
|
||||
let b2 = base + v.joint_indices.z;
|
||||
let b3 = base + v.joint_indices.w;
|
||||
let r0 = w.x * palette_row(b0, 0u) + w.y * palette_row(b1, 0u)
|
||||
+ w.z * palette_row(b2, 0u) + w.w * palette_row(b3, 0u);
|
||||
let r1 = w.x * palette_row(b0, 1u) + w.y * palette_row(b1, 1u)
|
||||
+ w.z * palette_row(b2, 1u) + w.w * palette_row(b3, 1u);
|
||||
let r2 = w.x * palette_row(b0, 2u) + w.y * palette_row(b1, 2u)
|
||||
+ w.z * palette_row(b2, 2u) + w.w * palette_row(b3, 2u);
|
||||
// r0/r1/r2 are the affine's rows; a WGSL matrix is column-major (as in `wow_skin_model`).
|
||||
let frame = mat4x4<f32>(
|
||||
vec4<f32>(r0.x, r1.x, r2.x, 0.0),
|
||||
vec4<f32>(r0.y, r1.y, r2.y, 0.0),
|
||||
vec4<f32>(r0.z, r1.z, r2.z, 0.0),
|
||||
vec4<f32>(r0.w, r1.w, r2.w, 1.0),
|
||||
);
|
||||
let origin = vec3<f32>(
|
||||
rig_buf[RIG_ORIGIN_F + 4u * slot],
|
||||
rig_buf[RIG_ORIGIN_F + 4u * slot + 1u],
|
||||
rig_buf[RIG_ORIGIN_F + 4u * slot + 2u],
|
||||
);
|
||||
// Camera-relative like `wow_model.wgsl`, so no f32 product mixes the ~9 k yd origin with a
|
||||
// local position.
|
||||
let p_cam = (frame * vec4<f32>(v.position, 1.0)).xyz + (origin - view.world_position);
|
||||
out.world_position = vec4<f32>(p_cam + view.world_position, 1.0);
|
||||
out.position = position_world_to_clip(out.world_position.xyz);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Invisible to the camera's forward pass; the shadow map renders through the prepass instead.
|
||||
@fragment
|
||||
fn fragment() -> @location(0) vec4<f32> {
|
||||
discard;
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
@@ -32,17 +32,18 @@ use bevy::light::{
|
||||
CascadeShadowConfigBuilder, DirectionalLight, DirectionalLightShadowMap, NotShadowReceiver,
|
||||
ShadowFilteringMethod,
|
||||
};
|
||||
use bevy::mesh::{Indices, PrimitiveTopology};
|
||||
use bevy::mesh::{Indices, Mesh, PrimitiveTopology};
|
||||
use bevy::pbr::{Material, MaterialPipeline, MaterialPlugin, MeshMaterial3d};
|
||||
use bevy::prelude::*;
|
||||
use bevy::reflect::TypePath;
|
||||
use bevy::render::render_resource::{
|
||||
AsBindGroup, RenderPipelineDescriptor, SpecializedMeshPipelineError,
|
||||
AsBindGroup, Buffer, RenderPipelineDescriptor, SpecializedMeshPipelineError,
|
||||
};
|
||||
use bevy::shader::ShaderRef;
|
||||
use bevy::shader::{ShaderDefVal, ShaderRef};
|
||||
|
||||
use benilla_assets::coords::wow_to_bevy;
|
||||
use benilla_assets::materials::WowModelMaterial;
|
||||
use benilla_assets::{ATTRIBUTE_WOW_JOINT_INDEX, ATTRIBUTE_WOW_JOINT_WEIGHT};
|
||||
use benilla_formats::ModelBlend;
|
||||
use benilla_world::billboard::BillboardCard;
|
||||
use benilla_world::interact::PickMesh;
|
||||
@@ -354,6 +355,72 @@ impl Material for ShadowCasterMaterial {
|
||||
}
|
||||
}
|
||||
|
||||
/// The per-unit GPU-skinned shadow caster: the character lane's replacement for the CPU-skinned
|
||||
/// merged proxy. It carries a part's own skinned `Mesh3d` and `MeshTag` and skins in the vertex
|
||||
/// stage from the shared light buffer's rig palette (the same rows `wow_model.wgsl` reads), so a
|
||||
/// crowd costs no per-frame CPU skin and no full-mesh re-upload. The forward pass discards it; the
|
||||
/// prepass (the directional shadow pass) writes depth from the skinned position.
|
||||
#[derive(Asset, AsBindGroup, TypePath, Debug, Clone)]
|
||||
pub(crate) struct SkinnedShadowCasterMaterial {
|
||||
/// The shared light buffer (`lighting::SharedLightBuffer`), whose slot-keyed tail holds
|
||||
/// `rig_table`, `rig_origin` and `palettes`. Bound at the same binding the model material uses.
|
||||
#[storage(90, read_only, buffer, visibility(vertex))]
|
||||
pub light_buf: Buffer,
|
||||
}
|
||||
|
||||
impl Material for SkinnedShadowCasterMaterial {
|
||||
fn vertex_shader() -> ShaderRef {
|
||||
"embedded://benilla_app/shaders/shadow_caster_skinned.wgsl".into()
|
||||
}
|
||||
|
||||
fn fragment_shader() -> ShaderRef {
|
||||
"embedded://benilla_app/shaders/shadow_caster_skinned.wgsl".into()
|
||||
}
|
||||
|
||||
fn prepass_vertex_shader() -> ShaderRef {
|
||||
"embedded://benilla_app/shaders/shadow_caster_skinned.wgsl".into()
|
||||
}
|
||||
|
||||
fn specialize(
|
||||
_pipeline: &MaterialPipeline,
|
||||
descriptor: &mut RenderPipelineDescriptor,
|
||||
layout: &bevy::mesh::MeshVertexBufferLayoutRef,
|
||||
_key: bevy::pbr::MaterialPipelineKey<Self>,
|
||||
) -> Result<(), SpecializedMeshPipelineError> {
|
||||
// Two-sided, like the solid proxy: a mixed-winding batch's silhouette must have no holes.
|
||||
descriptor.primitive.cull_mode = None;
|
||||
// The palette region byte offsets, substituted into the shader's flat `array<f32>` reads.
|
||||
// (`benilla_world::rig_palette` owns them; the buffer's prefix is irrelevant to the tail.)
|
||||
for (name, offset) in [
|
||||
(
|
||||
"WOW_RIG_TABLE_OFF",
|
||||
benilla_world::rig_palette::rig_table_region_offset(),
|
||||
),
|
||||
(
|
||||
"WOW_RIG_ORIGIN_OFF",
|
||||
benilla_world::rig_palette::rig_origin_region_offset(),
|
||||
),
|
||||
(
|
||||
"WOW_PALETTE_OFF",
|
||||
benilla_world::rig_palette::palette_region_offset(),
|
||||
),
|
||||
] {
|
||||
descriptor
|
||||
.vertex
|
||||
.shader_defs
|
||||
.push(ShaderDefVal::UInt(name.into(), offset as u32));
|
||||
}
|
||||
// The owned-palette joint attributes at 10/11, matching `WowModelExt::specialize`.
|
||||
let attrs = [
|
||||
Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
|
||||
ATTRIBUTE_WOW_JOINT_INDEX.at_shader_location(10),
|
||||
ATTRIBUTE_WOW_JOINT_WEIGHT.at_shader_location(11),
|
||||
];
|
||||
descriptor.vertex.buffers = vec![layout.0.get_layout(&attrs)?];
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for ShadowCorePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
// MONKEY (sun shadow perf): the shipped edge is now 2048, and it is a LIVE cvar
|
||||
@@ -364,6 +431,7 @@ impl Plugin for ShadowCorePlugin {
|
||||
size: DEFAULT_SHADOW_MAP_SIZE as usize,
|
||||
})
|
||||
.add_plugins(MaterialPlugin::<ShadowCasterMaterial>::default())
|
||||
.add_plugins(MaterialPlugin::<SkinnedShadowCasterMaterial>::default())
|
||||
.init_resource::<ShadowRigState>()
|
||||
.init_resource::<ShadowDemand>()
|
||||
.init_resource::<ShadowFrame>()
|
||||
@@ -687,6 +755,30 @@ pub(crate) fn spawn_solid_caster(
|
||||
.id()
|
||||
}
|
||||
|
||||
/// Spawn a GPU-skinned caster: a part's own skinned mesh and `MeshTag`, drawn through
|
||||
/// [`SkinnedShadowCasterMaterial`] on the private shadow layer. Its `Transform` is unused (the
|
||||
/// palette carries the world pose), so it is the identity.
|
||||
pub(crate) fn spawn_skinned_caster(
|
||||
commands: &mut Commands,
|
||||
mesh: Handle<Mesh>,
|
||||
material: Handle<SkinnedShadowCasterMaterial>,
|
||||
tag: u32,
|
||||
) -> Entity {
|
||||
commands
|
||||
.spawn((
|
||||
Mesh3d(mesh),
|
||||
MeshMaterial3d(material),
|
||||
bevy::mesh::MeshTag(tag),
|
||||
Transform::IDENTITY,
|
||||
Visibility::Visible,
|
||||
RenderLayers::layer(PLAYER_SHADOW_LAYER),
|
||||
NoFrustumCulling,
|
||||
NotShadowReceiver,
|
||||
ShadowCaster,
|
||||
))
|
||||
.id()
|
||||
}
|
||||
|
||||
/// Append a triangle list, dropping WHOLE triangles whose indices exceed the vertex range (filtering
|
||||
/// single indices out of a `TriangleList` rewires the rest of the submesh into garbage).
|
||||
pub(crate) fn append_triangles(
|
||||
@@ -835,7 +927,7 @@ pub(crate) fn collect_entity_geometry(
|
||||
/// ENVIRONMENT (gameobjects, faded doodads, WMO props) is OPAQUE only — a foliage leaf-card cast
|
||||
/// solid becomes a box; retained-world cutout is cast leaf-shaped by the world lane's alpha-tested
|
||||
/// material instead. Additive/transparent geometry never casts a solid shadow.
|
||||
fn casts_realtime_shadow(
|
||||
pub(crate) fn casts_realtime_shadow(
|
||||
kind: ModelKind,
|
||||
blend: ModelBlend,
|
||||
want_creatures: bool,
|
||||
|
||||
@@ -27,12 +27,12 @@ pub(crate) const MAX_PALETTE_BONES: usize = 131_072;
|
||||
const BONE_BYTES: u64 = 48;
|
||||
|
||||
/// Byte offset of the rig slot table (a base bone index a slot), after the prop-probe region.
|
||||
pub(crate) fn rig_table_region_offset() -> u64 {
|
||||
pub fn rig_table_region_offset() -> u64 {
|
||||
crate::lighting::prop_probe_region_offset() + (7 * crate::lighting::MAX_PROP_PROBES * 16) as u64
|
||||
}
|
||||
|
||||
/// Byte offset of the rig origin table (a `vec4` a slot), after the tint table, as in the shader.
|
||||
pub(crate) fn rig_origin_region_offset() -> u64 {
|
||||
pub fn rig_origin_region_offset() -> u64 {
|
||||
crate::instance_tint::region_offset() + crate::instance_tint::region_bytes()
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ pub(crate) fn rig_origin_region_bytes() -> u64 {
|
||||
|
||||
/// Byte offset of the palette rows, after the slot, tint, origin, mat-anim and straddle-clip
|
||||
/// tables: last, because `wow_model.wgsl` declares them as the struct's one runtime-sized array.
|
||||
pub(crate) fn palette_region_offset() -> u64 {
|
||||
pub fn palette_region_offset() -> u64 {
|
||||
crate::straddle::region_offset() + crate::straddle::region_bytes()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user