Revert "shadow: skin the character caster on the GPU, so a crowd costs no CPU skin"
The GPU-skinned character caster made realtime shadows render wrong, and the Stormwind lag it was meant to fix does not come from the shadow lanes anyway (character shadows are off there, so the rig is torn down). Restore the CPU-skinned merged proxy exactly as it was.
This commit is contained in:
@@ -1,27 +1,24 @@
|
||||
//! The CHARACTER shadow lane — a thin plug-in on top of [`super::shadow_core`].
|
||||
//!
|
||||
//! 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.
|
||||
//! 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.
|
||||
|
||||
use bevy::ecs::entity::EntityHashSet;
|
||||
use bevy::mesh::MeshTag;
|
||||
use bevy::platform::collections::HashMap;
|
||||
use bevy::pbr::MeshMaterial3d;
|
||||
use bevy::prelude::*;
|
||||
|
||||
use benilla_assets::materials::WowModelMaterial;
|
||||
use benilla_world::billboard::BillboardCard;
|
||||
use benilla_world::lighting::SharedLightBuffer;
|
||||
use benilla_world::interact::PickMesh;
|
||||
use benilla_world::model_render::{ModelPart, ShadowOccluder};
|
||||
use benilla_world::rig_palette::RigPart;
|
||||
use benilla_world::rig_palette::{RigPalettes, RigPart, RigSkin};
|
||||
|
||||
use crate::shadow_core::{
|
||||
casts_realtime_shadow, shadow_trace, spawn_skinned_caster, ShadowDemand, ShadowFrame,
|
||||
ShadowSet, SkinnedShadowCasterMaterial,
|
||||
collect_entity_geometry, empty_shadow_mesh, restore_mesh_buffers, shadow_trace,
|
||||
spawn_solid_caster, take_mesh_buffers, RebuildRate, ShadowDemand, ShadowFrame, ShadowSet,
|
||||
};
|
||||
use crate::video::VideoConfig;
|
||||
|
||||
@@ -35,132 +32,141 @@ impl Plugin for CharacterShadowPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// 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.
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct CharacterShadowReady(pub EntityHashSet);
|
||||
|
||||
/// The character lane's retained casters: part entity → its shadow-caster entity.
|
||||
/// The character lane's retained per-frame caster.
|
||||
#[derive(Resource, Default)]
|
||||
struct CharacterLane {
|
||||
casters: HashMap<Entity, Entity>,
|
||||
material: Option<Handle<SkinnedShadowCasterMaterial>>,
|
||||
/// [`shadow_trace`]'s change detector: (admitted, rejected, casters).
|
||||
caster: Option<Entity>,
|
||||
mesh: Option<Handle<Mesh>>,
|
||||
/// [`shadow_trace`]'s change detector: (tris, admitted, rejected).
|
||||
traced: (u32, u32, u32),
|
||||
}
|
||||
|
||||
/// 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;
|
||||
/// MONKEY (sun shadow perf): the `characterShadowRate` cadence gate — see the rebuild comment
|
||||
/// in [`collect_character_shadows`].
|
||||
rate: RebuildRate,
|
||||
}
|
||||
|
||||
#[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,
|
||||
light: Option<Res<SharedLightBuffer>>,
|
||||
mut materials: ResMut<Assets<SkinnedShadowCasterMaterial>>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
parts: Query<
|
||||
(
|
||||
Entity,
|
||||
&Mesh3d,
|
||||
&MeshTag,
|
||||
&PickMesh,
|
||||
&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, lane off, or an off night → drop every caster so none keeps casting.
|
||||
if !(frame.active && on) || frame.suspended {
|
||||
// Rig down or lane off → drop our caster so it stops casting.
|
||||
if !(frame.active && on) {
|
||||
ready.0.clear();
|
||||
teardown(&mut commands, &mut lane);
|
||||
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();
|
||||
return;
|
||||
}
|
||||
let Some(light) = light else {
|
||||
return; // the shared buffer is not up yet; the rig will still be live next frame
|
||||
// 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;
|
||||
};
|
||||
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");
|
||||
|
||||
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);
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
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
|
||||
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),
|
||||
);
|
||||
lane.traced = now;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ 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");
|
||||
}
|
||||
@@ -51,6 +50,6 @@ mod tests {
|
||||
`shaders::plugin`, or was registered from a file that is not directly under src/"
|
||||
);
|
||||
}
|
||||
assert_eq!(found, 9, "the game's shader set changed size");
|
||||
assert_eq!(found, 8, "the game's shader set changed size");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
// 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,18 +32,17 @@ use bevy::light::{
|
||||
CascadeShadowConfigBuilder, DirectionalLight, DirectionalLightShadowMap, NotShadowReceiver,
|
||||
ShadowFilteringMethod,
|
||||
};
|
||||
use bevy::mesh::{Indices, Mesh, PrimitiveTopology};
|
||||
use bevy::mesh::{Indices, PrimitiveTopology};
|
||||
use bevy::pbr::{Material, MaterialPipeline, MaterialPlugin, MeshMaterial3d};
|
||||
use bevy::prelude::*;
|
||||
use bevy::reflect::TypePath;
|
||||
use bevy::render::render_resource::{
|
||||
AsBindGroup, Buffer, RenderPipelineDescriptor, SpecializedMeshPipelineError,
|
||||
AsBindGroup, RenderPipelineDescriptor, SpecializedMeshPipelineError,
|
||||
};
|
||||
use bevy::shader::{ShaderDefVal, ShaderRef};
|
||||
use bevy::shader::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;
|
||||
@@ -355,72 +354,6 @@ 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
|
||||
@@ -431,7 +364,6 @@ 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>()
|
||||
@@ -755,30 +687,6 @@ 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(
|
||||
@@ -927,7 +835,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.
|
||||
pub(crate) fn casts_realtime_shadow(
|
||||
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 fn rig_table_region_offset() -> u64 {
|
||||
pub(crate) 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 fn rig_origin_region_offset() -> u64 {
|
||||
pub(crate) 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 fn palette_region_offset() -> u64 {
|
||||
pub(crate) fn palette_region_offset() -> u64 {
|
||||
crate::straddle::region_offset() + crate::straddle::region_bytes()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user