From 83d376c1c19c32e99b8dd01ad53847d216115bd9 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Wed, 29 May 2024 20:26:31 -0700 Subject: [PATCH] Implement motion vectors and TAA for skinned meshes and meshes with morph targets. This is a revamped version of #9902. Instead of adding more bind group layouts as that patch did, which created a combinatorial explosion of layouts, this patch unconditionally adds `prev_joint_matrices` and `prev_morph_weights` bindings to the shader bind groups. These add no significant overhead if unused because we simply bind dummy buffers, and the driver strips them out if unused. We already do this extensively with the various `StandardMaterial` bindings as well as the mesh view bindings, so this approach isn't anything new. The overall technique consists of double-buffering the joint matrix and morph weights buffers, as most of the previous attempts to solve this problem did. The process is generally straightforward. Note that, to avoid regressing the ability of mesh extraction, skin extraction, and morph target extraction to run in parallel, I had to add a new system to rendering, `set_mesh_motion_vector_flags`. The comment there explains the details; it generally runs very quickly. I've tested this with modified versions of the `animated_fox`, `morph_targets`, and `many_foxes` examples that add TAA, and the patch works. To avoid bloating those examples, I didn't add switches for TAA to them. Addresses points (1) and (2) of #8423. --- crates/bevy_pbr/src/deferred/mod.rs | 8 + crates/bevy_pbr/src/material.rs | 14 ++ crates/bevy_pbr/src/prepass/mod.rs | 22 +++ crates/bevy_pbr/src/prepass/prepass.wgsl | 59 +++++++- crates/bevy_pbr/src/render/mesh.rs | 158 +++++++++++++++++--- crates/bevy_pbr/src/render/mesh_bindings.rs | 69 ++++++++- crates/bevy_pbr/src/render/mod.rs | 2 +- crates/bevy_pbr/src/render/morph.rs | 74 ++++++--- crates/bevy_pbr/src/render/morph.wgsl | 5 + crates/bevy_pbr/src/render/skin.rs | 81 +++++++--- crates/bevy_pbr/src/render/skinning.wgsl | 22 +++ 11 files changed, 436 insertions(+), 78 deletions(-) diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index fbb6ab548fe59..c04e2203ad7d4 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -317,6 +317,14 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { shader_defs.push("SCREEN_SPACE_REFLECTIONS".into()); } + if key.contains(MeshPipelineKey::HAVE_PREVIOUS_SKIN) { + shader_defs.push("HAVE_PREVIOUS_SKIN".into()); + } + + if key.contains(MeshPipelineKey::HAVE_PREVIOUS_MORPH) { + shader_defs.push("HAVE_PREVIOUS_MORPH".into()); + } + // Always true, since we're in the deferred lighting pipeline shader_defs.push("DEFERRED_PREPASS".into()); diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index adf08f3068064..1d2c91abac06c 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -701,6 +701,20 @@ pub fn queue_material_meshes( mesh_key |= MeshPipelineKey::VISIBILITY_RANGE_DITHER; } + // If the previous frame have skins or morph targets, note that. + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAVE_PREVIOUS_SKIN) + { + mesh_key |= MeshPipelineKey::HAVE_PREVIOUS_SKIN; + } + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAVE_PREVIOUS_MORPH) + { + mesh_key |= MeshPipelineKey::HAVE_PREVIOUS_MORPH; + } + let pipeline_id = pipelines.specialize( &pipeline_cache, &material_pipeline, diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 1a9695cc6a5bb..2e431c2b9796d 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -443,6 +443,14 @@ where shader_defs.push("MOTION_VECTOR_PREPASS".into()); } + if key.mesh_key.contains(MeshPipelineKey::HAVE_PREVIOUS_SKIN) { + shader_defs.push("HAVE_PREVIOUS_SKIN".into()); + } + + if key.mesh_key.contains(MeshPipelineKey::HAVE_PREVIOUS_MORPH) { + shader_defs.push("HAVE_PREVIOUS_MORPH".into()); + } + if key.mesh_key.intersects( MeshPipelineKey::NORMAL_PREPASS | MeshPipelineKey::MOTION_VECTOR_PREPASS @@ -853,6 +861,20 @@ pub fn queue_prepass_material_meshes( mesh_key |= MeshPipelineKey::LIGHTMAPPED; } + // If the previous frame have skins or morph targets, note that. + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAVE_PREVIOUS_SKIN) + { + mesh_key |= MeshPipelineKey::HAVE_PREVIOUS_SKIN; + } + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAVE_PREVIOUS_MORPH) + { + mesh_key |= MeshPipelineKey::HAVE_PREVIOUS_MORPH; + } + let pipeline_id = pipelines.specialize( &pipeline_cache, &prepass_pipeline, diff --git a/crates/bevy_pbr/src/prepass/prepass.wgsl b/crates/bevy_pbr/src/prepass/prepass.wgsl index 0113618d3b13c..35f901f1a155a 100644 --- a/crates/bevy_pbr/src/prepass/prepass.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass.wgsl @@ -31,7 +31,26 @@ fn morph_vertex(vertex_in: Vertex) -> Vertex { } return vertex; } -#endif + +// Returns the morphed position of the given vertex from the previous frame. +// +// This function is used for motion vector calculation, and, as such, it doesn't +// bother morphing the normals and tangents. +fn morph_prev_vertex(vertex_in: Vertex) -> Vertex { + var vertex = vertex_in; + let weight_count = morph::layer_count(); + for (var i: u32 = 0u; i < weight_count; i ++) { + let weight = morph::prev_weight_at(i); + if weight == 0.0 { + continue; + } + vertex.position += weight * morph::morph(vertex.index, morph::position_offset, i); + // Don't bother morphing normals and tangents; we don't need them for + // motion vector calculation. + } + return vertex; +} +#endif // MORPH_TARGETS @vertex fn vertex(vertex_no_morph: Vertex) -> VertexOutput { @@ -93,12 +112,42 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput { out.color = vertex.color; #endif + // Compute the motion vector, for TAA. For this we need to know where the + // vertex was last frame. #ifdef MOTION_VECTOR_PREPASS - // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. - // See https://github.com/gfx-rs/naga/issues/2416 + + // Take morph targets into account. +#ifdef MORPH_TARGETS + +#ifdef HAVE_PREVIOUS_MORPH + let prev_vertex = morph_prev_vertex(vertex_no_morph); +#else // HAVE_PREVIOUS_MORPH + let prev_vertex = vertex_no_morph; +#endif // HAVE_PREVIOUS_MORPH + +#else // MORPH_TARGETS + let prev_vertex = vertex_no_morph; +#endif // MORPH_TARGETS + + // Take skinning into account. +#ifdef SKINNED + +#ifdef HAVE_PREVIOUS_SKIN + let prev_model = skinning::skin_prev_model( + prev_vertex.joint_indices, + prev_vertex.joint_weights, + ); +#else // HAVE_PREVIOUS_SKIN + let prev_model = mesh_functions::get_previous_model_matrix(prev_vertex.instance_index); +#endif // HAVE_PREVIOUS_SKIN + +#else // SKINNED + let prev_model = mesh_functions::get_previous_model_matrix(prev_vertex.instance_index); +#endif // SKINNED + out.previous_world_position = mesh_functions::mesh_position_local_to_world( - mesh_functions::get_previous_model_matrix(vertex_no_morph.instance_index), - vec4(vertex.position, 1.0) + prev_model, + vec4(prev_vertex.position, 1.0) ); #endif // MOTION_VECTOR_PREPASS diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 646a72f2ee721..8819ca0503e85 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -46,7 +46,7 @@ use static_assertions::const_assert_eq; use crate::render::{ morph::{ - extract_morphs, no_automatic_morph_batching, prepare_morphs, MorphIndices, MorphUniform, + extract_morphs, no_automatic_morph_batching, prepare_morphs, MorphIndices, MorphUniforms, }, skin::no_automatic_skin_batching, }; @@ -54,8 +54,6 @@ use crate::*; use self::irradiance_volume::IRRADIANCE_VOLUMES_ARE_USABLE; -use super::skin::SkinIndices; - /// Provides support for rendering 3D meshes. #[derive(Default)] pub struct MeshRenderPlugin { @@ -140,9 +138,9 @@ impl Plugin for MeshRenderPlugin { if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::() - .init_resource::() + .init_resource::() .init_resource::() - .init_resource::() + .init_resource::() .init_resource::() .init_resource::() .add_systems( @@ -157,6 +155,7 @@ impl Plugin for MeshRenderPlugin { .add_systems( Render, ( + set_mesh_motion_vector_flags.before(RenderSet::Queue), prepare_skins.in_set(RenderSet::PrepareResources), prepare_morphs.in_set(RenderSet::PrepareResources), prepare_mesh_bind_group.in_set(RenderSet::PrepareBindGroups), @@ -412,12 +411,18 @@ bitflags::bitflags! { const AUTOMATIC_BATCHING = 1 << 1; /// The mesh had a transform last frame and so is eligible for TAA. const HAVE_PREVIOUS_TRANSFORM = 1 << 2; + /// The mesh had a skin last frame and so that skin should be taken into + /// account for TAA. + const HAVE_PREVIOUS_SKIN = 1 << 3; + /// The mesh had morph targets last frame and so they should be taken + /// into account for TAA. + const HAVE_PREVIOUS_MORPH = 1 << 4; } } /// CPU data that the render world keeps for each entity, when *not* using GPU /// mesh uniform building. -#[derive(Deref)] +#[derive(Deref, DerefMut)] pub struct RenderMeshInstanceCpu { /// Data shared between both the CPU mesh uniform building and the GPU mesh /// uniform building paths. @@ -431,7 +436,7 @@ pub struct RenderMeshInstanceCpu { /// CPU data that the render world needs to keep for each entity that contains a /// mesh when using GPU mesh uniform building. -#[derive(Deref)] +#[derive(Deref, DerefMut)] pub struct RenderMeshInstanceGpu { /// Data shared between both the CPU mesh uniform building and the GPU mesh /// uniform building paths. @@ -599,6 +604,19 @@ impl RenderMeshInstances { } } } + + /// Inserts the given flags into the CPU or GPU render mesh instance data + /// for the given mesh as appropriate. + fn insert_mesh_instance_flags(&mut self, entity: Entity, flags: RenderMeshInstanceFlags) { + match *self { + RenderMeshInstances::CpuBuilding(ref mut instances) => { + instances.insert_mesh_instance_flags(entity, flags); + } + RenderMeshInstances::GpuBuilding(ref mut instances) => { + instances.insert_mesh_instance_flags(entity, flags); + } + } + } } impl RenderMeshInstancesCpu { @@ -614,6 +632,14 @@ impl RenderMeshInstancesCpu { translation: render_mesh_instance.transforms.transform.translation, }) } + + /// Inserts the given flags into the render mesh instance data for the given + /// mesh. + fn insert_mesh_instance_flags(&mut self, entity: Entity, flags: RenderMeshInstanceFlags) { + if let Some(instance) = self.get_mut(&entity) { + instance.flags.insert(flags); + } + } } impl RenderMeshInstancesGpu { @@ -629,6 +655,14 @@ impl RenderMeshInstancesGpu { translation: render_mesh_instance.translation, }) } + + /// Inserts the given flags into the render mesh instance data for the given + /// mesh. + fn insert_mesh_instance_flags(&mut self, entity: Entity, flags: RenderMeshInstanceFlags) { + if let Some(instance) = self.get_mut(&entity) { + instance.flags.insert(flags); + } + } } impl RenderMeshInstanceGpuQueue { @@ -977,6 +1011,35 @@ pub fn extract_meshes_for_gpu_building( ); } +/// A system that sets the [`RenderMeshInstanceFlags`] for each mesh based on +/// whether the previous frame had skins and/or morph targets. +/// +/// Ordinarily, [`RenderMeshInstanceFlags`] are set during the extraction phase. +/// However, we can't do that for the flags related to skins and morph targets +/// because the previous frame's skin and morph targets are the responsibility +/// of [`extract_skins`] and [`extract_morphs`] respectively. We want to run +/// those systems in parallel with mesh extraction for performance, so we need +/// to defer setting of these mesh instance flags to after extraction, which +/// this system does. An alternative to having skin- and morph-target-related +/// data in [`RenderMeshInstanceFlags`] would be to have +/// [`crate::material::queue_material_meshes`] check the skin and morph target +/// tables for each mesh, but that would be too slow in the hot mesh queuing +/// loop. +fn set_mesh_motion_vector_flags( + mut render_mesh_instances: ResMut, + skin_indices: Res, + morph_indices: Res, +) { + for &entity in skin_indices.prev.keys() { + render_mesh_instances + .insert_mesh_instance_flags(entity, RenderMeshInstanceFlags::HAVE_PREVIOUS_SKIN); + } + for &entity in morph_indices.prev.keys() { + render_mesh_instances + .insert_mesh_instance_flags(entity, RenderMeshInstanceFlags::HAVE_PREVIOUS_MORPH); + } +} + /// Creates the [`RenderMeshInstanceGpu`]s and [`MeshInputUniform`]s when GPU /// mesh uniforms are built. fn collect_meshes_for_gpu_building( @@ -1343,7 +1406,9 @@ bitflags::bitflags! { const IRRADIANCE_VOLUME = 1 << 14; const VISIBILITY_RANGE_DITHER = 1 << 15; const SCREEN_SPACE_REFLECTIONS = 1 << 16; - const LAST_FLAG = Self::SCREEN_SPACE_REFLECTIONS.bits(); + const HAVE_PREVIOUS_SKIN = 1 << 17; + const HAVE_PREVIOUS_MORPH = 1 << 18; + const LAST_FLAG = Self::HAVE_PREVIOUS_MORPH.bits(); // Bitfields const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; @@ -1645,6 +1710,14 @@ impl SpecializedMeshPipeline for MeshPipeline { shader_defs.push("MOTION_VECTOR_PREPASS".into()); } + if key.contains(MeshPipelineKey::HAVE_PREVIOUS_SKIN) { + shader_defs.push("HAVE_PREVIOUS_SKIN".into()); + } + + if key.contains(MeshPipelineKey::HAVE_PREVIOUS_MORPH) { + shader_defs.push("HAVE_PREVIOUS_MORPH".into()); + } + if key.contains(MeshPipelineKey::DEFERRED_PREPASS) { shader_defs.push("DEFERRED_PREPASS".into()); } @@ -1874,8 +1947,8 @@ pub fn prepare_mesh_bind_group( gpu_batched_instance_buffers: Option< Res>, >, - skins_uniform: Res, - weights_uniform: Res, + skins_uniform: Res, + weights_uniform: Res, render_lightmaps: Res, ) { groups.reset(); @@ -1896,18 +1969,34 @@ pub fn prepare_mesh_bind_group( groups.model_only = Some(layouts.model_only(&render_device, &model)); - let skin = skins_uniform.buffer.buffer(); + // Create the skinned mesh bind group with the current and previous buffers + // (the latter being for motion vector computation). If there's no previous + // buffer, just use the current one as the shader will ignore it. + let skin = skins_uniform.current_buffer.buffer(); if let Some(skin) = skin { - groups.skinned = Some(layouts.skinned(&render_device, &model, skin)); + let prev_skin = skins_uniform.prev_buffer.buffer().unwrap_or(skin); + groups.skinned = Some(layouts.skinned(&render_device, &model, skin, prev_skin)); } - if let Some(weights) = weights_uniform.buffer.buffer() { + // Create the morphed bind groups just like we did for the skinned bind + // group. + if let Some(weights) = weights_uniform.current_buffer.buffer() { + let prev_weights = weights_uniform.prev_buffer.buffer().unwrap_or(weights); for (id, gpu_mesh) in meshes.iter() { if let Some(targets) = gpu_mesh.morph_targets.as_ref() { let group = if let Some(skin) = skin.filter(|_| is_skinned(&gpu_mesh.layout)) { - layouts.morphed_skinned(&render_device, &model, skin, weights, targets) + let prev_skin = skins_uniform.prev_buffer.buffer().unwrap_or(skin); + layouts.morphed_skinned( + &render_device, + &model, + skin, + weights, + targets, + prev_skin, + prev_weights, + ) } else { - layouts.morphed(&render_device, &model, weights, targets) + layouts.morphed(&render_device, &model, weights, targets, prev_weights) }; groups.morph_targets.insert(id, group); } @@ -1998,11 +2087,13 @@ impl RenderCommand

for SetMeshBindGroup { let Some(mesh_asset_id) = mesh_instances.mesh_asset_id(*entity) else { return RenderCommandResult::Success; }; - let skin_index = skin_indices.get(entity); - let morph_index = morph_indices.get(entity); + let current_skin_index = skin_indices.current.get(entity); + let prev_skin_index = skin_indices.prev.get(entity); + let current_morph_index = morph_indices.current.get(entity); + let prev_morph_index = morph_indices.prev.get(entity); - let is_skinned = skin_index.is_some(); - let is_morphed = morph_index.is_some(); + let is_skinned = current_skin_index.is_some(); + let is_morphed = current_morph_index.is_some(); let lightmap = lightmaps .render_lightmaps @@ -2025,14 +2116,35 @@ impl RenderCommand

for SetMeshBindGroup { dynamic_offsets[offset_count] = dynamic_offset.get(); offset_count += 1; } - if let Some(skin_index) = skin_index { - dynamic_offsets[offset_count] = skin_index.index; + if let Some(current_skin_index) = current_skin_index { + dynamic_offsets[offset_count] = current_skin_index.index; offset_count += 1; } - if let Some(morph_index) = morph_index { - dynamic_offsets[offset_count] = morph_index.index; + if let Some(current_morph_index) = current_morph_index { + dynamic_offsets[offset_count] = current_morph_index.index; offset_count += 1; } + + // Attach the previous skin index for motion vector computation. If + // there isn't one, just use zero as the shader will ignore it. + if current_skin_index.is_some() { + match prev_skin_index { + Some(prev_skin_index) => dynamic_offsets[offset_count] = prev_skin_index.index, + None => dynamic_offsets[offset_count] = 0, + } + offset_count += 1; + } + + // Attach the previous morph index for motion vector computation. If + // there isn't one, just use zero as the shader will ignore it. + if current_morph_index.is_some() { + match prev_morph_index { + Some(prev_morph_index) => dynamic_offsets[offset_count] = prev_morph_index.index, + None => dynamic_offsets[offset_count] = 0, + } + offset_count += 1; + } + pass.set_bind_group(I, bind_group, &dynamic_offsets[0..offset_count]); RenderCommandResult::Success diff --git a/crates/bevy_pbr/src/render/mesh_bindings.rs b/crates/bevy_pbr/src/render/mesh_bindings.rs index 60beb9911530a..3cdae40ef939b 100644 --- a/crates/bevy_pbr/src/render/mesh_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_bindings.rs @@ -143,6 +143,8 @@ impl MeshLayouts { ), ) } + + /// Creates the layout for skinned meshes. fn skinned_layout(render_device: &RenderDevice) -> BindGroupLayout { render_device.create_bind_group_layout( "skinned_mesh_layout", @@ -150,11 +152,16 @@ impl MeshLayouts { ShaderStages::VERTEX, ( (0, layout_entry::model(render_device)), + // The current frame's joint matrix buffer. (1, layout_entry::skinning()), + // The previous frame's joint matrix buffer. + (6, layout_entry::skinning()), ), ), ) } + + /// Creates the layout for meshes with morph targets. fn morphed_layout(render_device: &RenderDevice) -> BindGroupLayout { render_device.create_bind_group_layout( "morphed_mesh_layout", @@ -162,12 +169,18 @@ impl MeshLayouts { ShaderStages::VERTEX, ( (0, layout_entry::model(render_device)), + // The current frame's morph weight buffer. (2, layout_entry::weights()), (3, layout_entry::targets()), + // The previous frame's morph weight buffer. + (7, layout_entry::weights()), ), ), ) } + + /// Creates the bind group layout for meshes with both skins and morph + /// targets. fn morphed_skinned_layout(render_device: &RenderDevice) -> BindGroupLayout { render_device.create_bind_group_layout( "morphed_skinned_mesh_layout", @@ -175,13 +188,20 @@ impl MeshLayouts { ShaderStages::VERTEX, ( (0, layout_entry::model(render_device)), + // The current frame's joint matrix buffer. (1, layout_entry::skinning()), + // The current frame's morph weight buffer. (2, layout_entry::weights()), (3, layout_entry::targets()), + // The previous frame's joint matrix buffer. + (6, layout_entry::skinning()), + // The previous frame's morph weight buffer. + (7, layout_entry::weights()), ), ), ) } + fn lightmapped_layout(render_device: &RenderDevice) -> BindGroupLayout { render_device.create_bind_group_layout( "lightmapped_mesh_layout", @@ -205,6 +225,7 @@ impl MeshLayouts { &[entry::model(0, model.clone())], ) } + pub fn lightmapped( &self, render_device: &RenderDevice, @@ -221,51 +242,83 @@ impl MeshLayouts { ], ) } + + /// Creates the bind group for skinned meshes with no morph targets. + /// + /// `current_skin` is the buffer of joint matrices for this frame; + /// `prev_skin` is the buffer for the previous frame. The latter is used for + /// motion vector computation. If there is no such applicable buffer, + /// `current_skin` and `prev_skin` will reference the same buffer. pub fn skinned( &self, render_device: &RenderDevice, model: &BindingResource, - skin: &Buffer, + current_skin: &Buffer, + prev_skin: &Buffer, ) -> BindGroup { render_device.create_bind_group( "skinned_mesh_bind_group", &self.skinned, - &[entry::model(0, model.clone()), entry::skinning(1, skin)], + &[ + entry::model(0, model.clone()), + entry::skinning(1, current_skin), + entry::skinning(6, prev_skin), + ], ) } + + /// Creates the bind group for meshes with no skins but morph targets. + /// + /// `current_weights` is the buffer of morph weights for this frame; + /// `prev_weights` is the buffer for the previous frame. The latter is used + /// for motion vector computation. If there is no such applicable buffer, + /// `current_weights` and `prev_weights` will reference the same buffer. pub fn morphed( &self, render_device: &RenderDevice, model: &BindingResource, - weights: &Buffer, + current_weights: &Buffer, targets: &TextureView, + prev_weights: &Buffer, ) -> BindGroup { render_device.create_bind_group( "morphed_mesh_bind_group", &self.morphed, &[ entry::model(0, model.clone()), - entry::weights(2, weights), + entry::weights(2, current_weights), entry::targets(3, targets), + entry::weights(7, prev_weights), ], ) } + + /// Creates the bind group for meshes with skins and morph targets. + /// + /// See the documentation for [`skinned`] and [`morphed`] above for more + /// information about the `current_skin`, `prev_skin`, `current_weights`, + /// and `prev_weights` buffers. + #[allow(clippy::too_many_arguments)] pub fn morphed_skinned( &self, render_device: &RenderDevice, model: &BindingResource, - skin: &Buffer, - weights: &Buffer, + current_skin: &Buffer, + current_weights: &Buffer, targets: &TextureView, + prev_skin: &Buffer, + prev_weights: &Buffer, ) -> BindGroup { render_device.create_bind_group( "morphed_skinned_mesh_bind_group", &self.morphed_skinned, &[ entry::model(0, model.clone()), - entry::skinning(1, skin), - entry::weights(2, weights), + entry::skinning(1, current_skin), + entry::weights(2, current_weights), entry::targets(3, targets), + entry::skinning(6, prev_skin), + entry::weights(7, prev_weights), ], ) } diff --git a/crates/bevy_pbr/src/render/mod.rs b/crates/bevy_pbr/src/render/mod.rs index 53bc9bcde14b4..2a69e28bf3a44 100644 --- a/crates/bevy_pbr/src/render/mod.rs +++ b/crates/bevy_pbr/src/render/mod.rs @@ -13,4 +13,4 @@ pub use light::*; pub use mesh::*; pub use mesh_bindings::MeshLayouts; pub use mesh_view_bindings::*; -pub use skin::{extract_skins, prepare_skins, SkinIndex, SkinUniform, MAX_JOINTS}; +pub use skin::{extract_skins, prepare_skins, SkinIndices, SkinUniforms, MAX_JOINTS}; diff --git a/crates/bevy_pbr/src/render/morph.rs b/crates/bevy_pbr/src/render/morph.rs index 692af99621966..1aa82ec80e9f6 100644 --- a/crates/bevy_pbr/src/render/morph.rs +++ b/crates/bevy_pbr/src/render/morph.rs @@ -1,6 +1,5 @@ use std::{iter, mem}; -use bevy_derive::{Deref, DerefMut}; use bevy_ecs::entity::EntityHashMap; use bevy_ecs::prelude::*; use bevy_render::{ @@ -18,18 +17,41 @@ pub struct MorphIndex { pub(super) index: u32, } -#[derive(Default, Resource, Deref, DerefMut)] -pub struct MorphIndices(EntityHashMap); +/// Maps each mesh affected by morph targets to the applicable offset within the +/// [`MorphUniforms`] buffer. +/// +/// We store both the current frame's mapping and the previous frame's mapping +/// for the purposes of motion vector calculation. +#[derive(Default, Resource)] +pub struct MorphIndices { + /// Maps each entity with a morphed mesh to the appropriate offset within + /// [`MorphUniforms::current_buffer`]. + pub current: EntityHashMap, + /// Maps each entity with a morphed mesh to the appropriate offset within + /// [`MorphUniforms::prev_buffer`]. + pub prev: EntityHashMap, +} + +/// The GPU buffers containing morph weights for all meshes with morph targets. +/// +/// This is double-buffered: we store the weights of the previous frame in +/// addition to those of the current frame. This is for motion vector +/// calculation. Every frame, we swap buffers and reuse the morph target weight +/// buffer from two frames ago for the current frame. #[derive(Resource)] -pub struct MorphUniform { - pub buffer: RawBufferVec, +pub struct MorphUniforms { + /// The morph weights for the current frame. + pub current_buffer: RawBufferVec, + /// The morph weights for the previous frame. + pub prev_buffer: RawBufferVec, } -impl Default for MorphUniform { +impl Default for MorphUniforms { fn default() -> Self { Self { - buffer: RawBufferVec::new(BufferUsages::UNIFORM), + current_buffer: RawBufferVec::new(BufferUsages::UNIFORM), + prev_buffer: RawBufferVec::new(BufferUsages::UNIFORM), } } } @@ -37,14 +59,19 @@ impl Default for MorphUniform { pub fn prepare_morphs( render_device: Res, render_queue: Res, - mut uniform: ResMut, + mut uniform: ResMut, ) { - if uniform.buffer.is_empty() { + if uniform.current_buffer.is_empty() { return; } - let len = uniform.buffer.len(); - uniform.buffer.reserve(len, &render_device); - uniform.buffer.write_buffer(&render_device, &render_queue); + let len = uniform.current_buffer.len(); + uniform.current_buffer.reserve(len, &render_device); + uniform + .current_buffer + .write_buffer(&render_device, &render_queue); + + // We don't need to write `uniform.prev_buffer` because we already wrote it + // last frame, and the data should still be on the GPU. } const fn can_align(step: usize, target: usize) -> bool { @@ -80,25 +107,32 @@ fn add_to_alignment(buffer: &mut RawBufferVec) { // Notes on implementation: see comment on top of the extract_skins system in skin module. // This works similarly, but for `f32` instead of `Mat4` pub fn extract_morphs( - mut morph_indices: ResMut, - mut uniform: ResMut, + morph_indices: ResMut, + uniform: ResMut, query: Extract>, ) { - morph_indices.clear(); - uniform.buffer.clear(); + // Borrow check workaround. + let (morph_indices, uniform) = (morph_indices.into_inner(), uniform.into_inner()); + + // Swap buffers. We need to keep the previous frame's buffer around for the + // purposes of motion vector computation. + mem::swap(&mut morph_indices.current, &mut morph_indices.prev); + mem::swap(&mut uniform.current_buffer, &mut uniform.prev_buffer); + morph_indices.current.clear(); + uniform.current_buffer.clear(); for (entity, view_visibility, morph_weights) in &query { if !view_visibility.get() { continue; } - let start = uniform.buffer.len(); + let start = uniform.current_buffer.len(); let weights = morph_weights.weights(); let legal_weights = weights.iter().take(MAX_MORPH_WEIGHTS).copied(); - uniform.buffer.extend(legal_weights); - add_to_alignment::(&mut uniform.buffer); + uniform.current_buffer.extend(legal_weights); + add_to_alignment::(&mut uniform.current_buffer); let index = (start * mem::size_of::()) as u32; - morph_indices.insert(entity, MorphIndex { index }); + morph_indices.current.insert(entity, MorphIndex { index }); } } diff --git a/crates/bevy_pbr/src/render/morph.wgsl b/crates/bevy_pbr/src/render/morph.wgsl index bd6eb1d041832..939b714c777ed 100644 --- a/crates/bevy_pbr/src/render/morph.wgsl +++ b/crates/bevy_pbr/src/render/morph.wgsl @@ -6,6 +6,7 @@ @group(1) @binding(2) var morph_weights: MorphWeights; @group(1) @binding(3) var morph_targets: texture_3d; +@group(1) @binding(7) var prev_morph_weights: MorphWeights; // NOTE: Those are the "hardcoded" values found in `MorphAttributes` struct // in crates/bevy_render/src/mesh/morph/visitors.rs @@ -29,6 +30,10 @@ fn weight_at(weight_index: u32) -> f32 { let i = weight_index; return morph_weights.weights[i / 4u][i % 4u]; } +fn prev_weight_at(weight_index: u32) -> f32 { + let i = weight_index; + return prev_morph_weights.weights[i / 4u][i % 4u]; +} fn morph_pixel(vertex: u32, component: u32, weight: u32) -> f32 { let coord = component_texture_coord(vertex, component); // Due to https://gpuweb.github.io/gpuweb/wgsl/#texel-formats diff --git a/crates/bevy_pbr/src/render/skin.rs b/crates/bevy_pbr/src/render/skin.rs index 5d3e2ba2c9369..0f7cc7a9f3cb4 100644 --- a/crates/bevy_pbr/src/render/skin.rs +++ b/crates/bevy_pbr/src/render/skin.rs @@ -1,5 +1,6 @@ +use std::mem; + use bevy_asset::Assets; -use bevy_derive::{Deref, DerefMut}; use bevy_ecs::entity::EntityHashMap; use bevy_ecs::prelude::*; use bevy_math::Mat4; @@ -30,19 +31,44 @@ impl SkinIndex { } } -#[derive(Default, Resource, Deref, DerefMut)] -pub struct SkinIndices(EntityHashMap); +/// Maps each skinned mesh to the applicable offset within the [`SkinUniforms`] +/// buffer. +/// +/// We store both the current frame's joint matrices and the previous frame's +/// joint matrices for the purposes of motion vector calculation. +#[derive(Default, Resource)] +pub struct SkinIndices { + /// Maps each skinned mesh to the applicable offset within + /// [`SkinUniforms::current_buffer`]. + pub current: EntityHashMap, + + /// Maps each skinned mesh to the applicable offset within + /// [`SkinUniforms::prev_buffer`]. + pub prev: EntityHashMap, +} -// Notes on implementation: see comment on top of the `extract_skins` system. +/// The GPU buffers containing joint matrices for all skinned meshes. +/// +/// This is double-buffered: we store the joint matrices of each mesh for the +/// previous frame in addition to those of each mesh for the current frame. This +/// is for motion vector calculation. Every frame, we swap buffers and overwrite +/// the joint matrix buffer from two frames ago with the data for the current +/// frame. +/// +/// Notes on implementation: see comment on top of the `extract_skins` system. #[derive(Resource)] -pub struct SkinUniform { - pub buffer: RawBufferVec, +pub struct SkinUniforms { + /// Stores all the joint matrices for skinned meshes in the current frame. + pub current_buffer: RawBufferVec, + /// Stores all the joint matrices for skinned meshes in the previous frame. + pub prev_buffer: RawBufferVec, } -impl Default for SkinUniform { +impl Default for SkinUniforms { fn default() -> Self { Self { - buffer: RawBufferVec::new(BufferUsages::UNIFORM), + current_buffer: RawBufferVec::new(BufferUsages::UNIFORM), + prev_buffer: RawBufferVec::new(BufferUsages::UNIFORM), } } } @@ -50,15 +76,20 @@ impl Default for SkinUniform { pub fn prepare_skins( render_device: Res, render_queue: Res, - mut uniform: ResMut, + mut uniform: ResMut, ) { - if uniform.buffer.is_empty() { + if uniform.current_buffer.is_empty() { return; } - let len = uniform.buffer.len(); - uniform.buffer.reserve(len, &render_device); - uniform.buffer.write_buffer(&render_device, &render_queue); + let len = uniform.current_buffer.len(); + uniform.current_buffer.reserve(len, &render_device); + uniform + .current_buffer + .write_buffer(&render_device, &render_queue); + + // We don't need to write `uniform.prev_buffer` because we already wrote it + // last frame, and the data should still be on the GPU. } // Notes on implementation: @@ -88,14 +119,22 @@ pub fn prepare_skins( // which normally only support fixed size arrays. You just have to make sure // in the shader that you only read the values that are valid for that binding. pub fn extract_skins( - mut skin_indices: ResMut, - mut uniform: ResMut, + skin_indices: ResMut, + uniform: ResMut, query: Extract>, inverse_bindposes: Extract>>, joints: Extract>, ) { - uniform.buffer.clear(); - skin_indices.clear(); + // Borrow check workaround. + let (skin_indices, uniform) = (skin_indices.into_inner(), uniform.into_inner()); + + // Swap buffers. We need to keep the previous frame's buffer around for the + // purposes of motion vector computation. + mem::swap(&mut skin_indices.current, &mut skin_indices.prev); + mem::swap(&mut uniform.current_buffer, &mut uniform.prev_buffer); + skin_indices.current.clear(); + uniform.current_buffer.clear(); + let mut last_start = 0; // PERF: This can be expensive, can we move this to prepare? @@ -103,7 +142,7 @@ pub fn extract_skins( if !view_visibility.get() { continue; } - let buffer = &mut uniform.buffer; + let buffer = &mut uniform.current_buffer; let Some(inverse_bindposes) = inverse_bindposes.get(&skin.inverse_bindposes) else { continue; }; @@ -130,12 +169,12 @@ pub fn extract_skins( buffer.push(Mat4::ZERO); } - skin_indices.insert(entity, SkinIndex::new(start)); + skin_indices.current.insert(entity, SkinIndex::new(start)); } // Pad out the buffer to ensure that there's enough space for bindings - while uniform.buffer.len() - last_start < MAX_JOINTS { - uniform.buffer.push(Mat4::ZERO); + while uniform.current_buffer.len() - last_start < MAX_JOINTS { + uniform.current_buffer.push(Mat4::ZERO); } } diff --git a/crates/bevy_pbr/src/render/skinning.wgsl b/crates/bevy_pbr/src/render/skinning.wgsl index 4307a22ddffa0..1ec9ef15ebe03 100644 --- a/crates/bevy_pbr/src/render/skinning.wgsl +++ b/crates/bevy_pbr/src/render/skinning.wgsl @@ -6,6 +6,14 @@ @group(1) @binding(1) var joint_matrices: SkinnedMesh; +// An array of matrices specifying the joint positions from the previous frame. +// +// This is used for motion vector computation. +// +// If this is the first frame, or we're otherwise prevented from using data from +// the previous frame, this is simply the same as `joint_matrices` above. +@group(1) @binding(6) var prev_joint_matrices: SkinnedMesh; + fn skin_model( indexes: vec4, weights: vec4, @@ -16,6 +24,20 @@ fn skin_model( + weights.w * joint_matrices.data[indexes.w]; } +// Returns the skinned position of a vertex with the given weights from the +// previous frame. +// +// This is used for motion vector computation. +fn skin_prev_model( + indexes: vec4, + weights: vec4, +) -> mat4x4 { + return weights.x * prev_joint_matrices.data[indexes.x] + + weights.y * prev_joint_matrices.data[indexes.y] + + weights.z * prev_joint_matrices.data[indexes.z] + + weights.w * prev_joint_matrices.data[indexes.w]; +} + fn inverse_transpose_3x3m(in: mat3x3) -> mat3x3 { let x = cross(in[1], in[2]); let y = cross(in[2], in[0]);