Implement motion vectors and TAA for skinned meshes and meshes with morph targets. (#13572)

This is a revamped equivalent to #9902, though it shares none of the
code. It handles all special cases that I've tested correctly.

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.

## Changelog

### Fixed

* Motion vectors, and therefore TAA, are now supported for meshes with
skins and/or morph targets.
This commit is contained in:
Patrick Walton 2024-05-31 10:02:28 -07:00 committed by GitHub
parent 42d80375db
commit be053b1d7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 679 additions and 103 deletions

View file

@ -124,8 +124,6 @@ pub struct TemporalAntiAliasBundle {
///
/// [Currently](https://github.com/bevyengine/bevy/issues/8423) cannot be used with [`bevy_render::camera::OrthographicProjection`].
///
/// Currently does not support skinned meshes and morph targets.
/// There will probably be ghosting artifacts if used with them.
/// Does not work well with alpha-blended meshes as it requires depth writing to determine motion.
///
/// It is very important that correct motion vectors are written for everything on screen.

View file

@ -317,6 +317,14 @@ impl SpecializedRenderPipeline for DeferredLightingLayout {
shader_defs.push("SCREEN_SPACE_REFLECTIONS".into());
}
if key.contains(MeshPipelineKey::HAS_PREVIOUS_SKIN) {
shader_defs.push("HAS_PREVIOUS_SKIN".into());
}
if key.contains(MeshPipelineKey::HAS_PREVIOUS_MORPH) {
shader_defs.push("HAS_PREVIOUS_MORPH".into());
}
// Always true, since we're in the deferred lighting pipeline
shader_defs.push("DEFERRED_PREPASS".into());

View file

@ -701,6 +701,22 @@ pub fn queue_material_meshes<M: Material>(
mesh_key |= MeshPipelineKey::VISIBILITY_RANGE_DITHER;
}
if motion_vector_prepass {
// If the previous frame have skins or morph targets, note that.
if mesh_instance
.flags
.contains(RenderMeshInstanceFlags::HAS_PREVIOUS_SKIN)
{
mesh_key |= MeshPipelineKey::HAS_PREVIOUS_SKIN;
}
if mesh_instance
.flags
.contains(RenderMeshInstanceFlags::HAS_PREVIOUS_MORPH)
{
mesh_key |= MeshPipelineKey::HAS_PREVIOUS_MORPH;
}
}
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&material_pipeline,

View file

@ -443,6 +443,14 @@ where
shader_defs.push("MOTION_VECTOR_PREPASS".into());
}
if key.mesh_key.contains(MeshPipelineKey::HAS_PREVIOUS_SKIN) {
shader_defs.push("HAS_PREVIOUS_SKIN".into());
}
if key.mesh_key.contains(MeshPipelineKey::HAS_PREVIOUS_MORPH) {
shader_defs.push("HAS_PREVIOUS_MORPH".into());
}
if key.mesh_key.intersects(
MeshPipelineKey::NORMAL_PREPASS
| MeshPipelineKey::MOTION_VECTOR_PREPASS
@ -853,6 +861,22 @@ pub fn queue_prepass_material_meshes<M: Material>(
mesh_key |= MeshPipelineKey::LIGHTMAPPED;
}
// If the previous frame has skins or morph targets, note that.
if motion_vector_prepass.is_some() {
if mesh_instance
.flags
.contains(RenderMeshInstanceFlags::HAS_PREVIOUS_SKIN)
{
mesh_key |= MeshPipelineKey::HAS_PREVIOUS_SKIN;
}
if mesh_instance
.flags
.contains(RenderMeshInstanceFlags::HAS_PREVIOUS_MORPH)
{
mesh_key |= MeshPipelineKey::HAS_PREVIOUS_MORPH;
}
}
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&prepass_pipeline,

View file

@ -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 among other purposes. 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 HAS_PREVIOUS_MORPH
let prev_vertex = morph_prev_vertex(vertex_no_morph);
#else // HAS_PREVIOUS_MORPH
let prev_vertex = vertex_no_morph;
#endif // HAS_PREVIOUS_MORPH
#else // MORPH_TARGETS
let prev_vertex = vertex_no_morph;
#endif // MORPH_TARGETS
// Take skinning into account.
#ifdef SKINNED
#ifdef HAS_PREVIOUS_SKIN
let prev_model = skinning::skin_prev_model(
prev_vertex.joint_indices,
prev_vertex.joint_weights,
);
#else // HAS_PREVIOUS_SKIN
let prev_model = mesh_functions::get_previous_model_matrix(prev_vertex.instance_index);
#endif // HAS_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<f32>(vertex.position, 1.0)
prev_model,
vec4<f32>(prev_vertex.position, 1.0)
);
#endif // MOTION_VECTOR_PREPASS

View file

@ -4,6 +4,7 @@ use bevy_asset::{load_internal_asset, AssetId};
use bevy_core_pipeline::{
core_3d::{AlphaMask3d, Opaque3d, Transmissive3d, Transparent3d, CORE_3D_DEPTH_FORMAT},
deferred::{AlphaMask3dDeferred, Opaque3dDeferred},
prepass::MotionVectorPrepass,
};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::entity::EntityHashMap;
@ -46,7 +47,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 +55,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 +139,9 @@ impl Plugin for MeshRenderPlugin {
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<MeshBindGroups>()
.init_resource::<SkinUniform>()
.init_resource::<SkinUniforms>()
.init_resource::<SkinIndices>()
.init_resource::<MorphUniform>()
.init_resource::<MorphUniforms>()
.init_resource::<MorphIndices>()
.init_resource::<MeshCullingDataBuffer>()
.add_systems(
@ -157,6 +156,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),
@ -410,14 +410,21 @@ bitflags::bitflags! {
const SHADOW_CASTER = 1 << 0;
/// The mesh can participate in automatic batching.
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 transform last frame and so is eligible for motion
/// vector computation.
const HAS_PREVIOUS_TRANSFORM = 1 << 2;
/// The mesh had a skin last frame and so that skin should be taken into
/// account for motion vector computation.
const HAS_PREVIOUS_SKIN = 1 << 3;
/// The mesh had morph targets last frame and so they should be taken
/// into account for motion vector computation.
const HAS_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 +438,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.
@ -524,7 +531,7 @@ impl RenderMeshInstanceShared {
!no_automatic_batching,
);
mesh_instance_flags.set(
RenderMeshInstanceFlags::HAVE_PREVIOUS_TRANSFORM,
RenderMeshInstanceFlags::HAS_PREVIOUS_TRANSFORM,
previous_transform.is_some(),
);
@ -599,6 +606,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 +634,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 +657,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 {
@ -948,7 +984,7 @@ pub fn extract_meshes_for_gpu_building(
let previous_input_index = if shared
.flags
.contains(RenderMeshInstanceFlags::HAVE_PREVIOUS_TRANSFORM)
.contains(RenderMeshInstanceFlags::HAS_PREVIOUS_TRANSFORM)
{
render_mesh_instances
.get(&entity)
@ -977,6 +1013,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<RenderMeshInstances>,
skin_indices: Res<SkinIndices>,
morph_indices: Res<MorphIndices>,
) {
for &entity in skin_indices.prev.keys() {
render_mesh_instances
.insert_mesh_instance_flags(entity, RenderMeshInstanceFlags::HAS_PREVIOUS_SKIN);
}
for &entity in morph_indices.prev.keys() {
render_mesh_instances
.insert_mesh_instance_flags(entity, RenderMeshInstanceFlags::HAS_PREVIOUS_MORPH);
}
}
/// Creates the [`RenderMeshInstanceGpu`]s and [`MeshInputUniform`]s when GPU
/// mesh uniforms are built.
fn collect_meshes_for_gpu_building(
@ -1343,7 +1408,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 HAS_PREVIOUS_SKIN = 1 << 17;
const HAS_PREVIOUS_MORPH = 1 << 18;
const LAST_FLAG = Self::HAS_PREVIOUS_MORPH.bits();
// Bitfields
const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS;
@ -1484,22 +1551,41 @@ pub fn setup_morph_and_skinning_defs(
};
let is_morphed = key.intersects(MeshPipelineKey::MORPH_TARGETS);
let is_lightmapped = key.intersects(MeshPipelineKey::LIGHTMAPPED);
match (is_skinned(layout), is_morphed, is_lightmapped) {
(true, false, _) => {
let motion_vector_prepass = key.intersects(MeshPipelineKey::MOTION_VECTOR_PREPASS);
match (
is_skinned(layout),
is_morphed,
is_lightmapped,
motion_vector_prepass,
) {
(true, false, _, true) => {
add_skin_data();
mesh_layouts.skinned_motion.clone()
}
(true, false, _, false) => {
add_skin_data();
mesh_layouts.skinned.clone()
}
(true, true, _) => {
(true, true, _, true) => {
add_skin_data();
shader_defs.push("MORPH_TARGETS".into());
mesh_layouts.morphed_skinned_motion.clone()
}
(true, true, _, false) => {
add_skin_data();
shader_defs.push("MORPH_TARGETS".into());
mesh_layouts.morphed_skinned.clone()
}
(false, true, _) => {
(false, true, _, true) => {
shader_defs.push("MORPH_TARGETS".into());
mesh_layouts.morphed_motion.clone()
}
(false, true, _, false) => {
shader_defs.push("MORPH_TARGETS".into());
mesh_layouts.morphed.clone()
}
(false, false, true) => mesh_layouts.lightmapped.clone(),
(false, false, false) => mesh_layouts.model_only.clone(),
(false, false, true, _) => mesh_layouts.lightmapped.clone(),
(false, false, false, _) => mesh_layouts.model_only.clone(),
}
}
@ -1645,6 +1731,14 @@ impl SpecializedMeshPipeline for MeshPipeline {
shader_defs.push("MOTION_VECTOR_PREPASS".into());
}
if key.contains(MeshPipelineKey::HAS_PREVIOUS_SKIN) {
shader_defs.push("HAS_PREVIOUS_SKIN".into());
}
if key.contains(MeshPipelineKey::HAS_PREVIOUS_MORPH) {
shader_defs.push("HAS_PREVIOUS_MORPH".into());
}
if key.contains(MeshPipelineKey::DEFERRED_PREPASS) {
shader_defs.push("DEFERRED_PREPASS".into());
}
@ -1832,10 +1926,16 @@ impl SpecializedMeshPipeline for MeshPipeline {
#[derive(Resource, Default)]
pub struct MeshBindGroups {
model_only: Option<BindGroup>,
skinned: Option<BindGroup>,
morph_targets: HashMap<AssetId<Mesh>, BindGroup>,
skinned: Option<MeshBindGroupPair>,
morph_targets: HashMap<AssetId<Mesh>, MeshBindGroupPair>,
lightmaps: HashMap<AssetId<Image>, BindGroup>,
}
pub struct MeshBindGroupPair {
motion_vectors: BindGroup,
no_motion_vectors: BindGroup,
}
impl MeshBindGroups {
pub fn reset(&mut self) {
self.model_only = None;
@ -1851,16 +1951,33 @@ impl MeshBindGroups {
lightmap: Option<AssetId<Image>>,
is_skinned: bool,
morph: bool,
motion_vectors: bool,
) -> Option<&BindGroup> {
match (is_skinned, morph, lightmap) {
(_, true, _) => self.morph_targets.get(&asset_id),
(true, false, _) => self.skinned.as_ref(),
(_, true, _) => self
.morph_targets
.get(&asset_id)
.map(|bind_group_pair| bind_group_pair.get(motion_vectors)),
(true, false, _) => self
.skinned
.as_ref()
.map(|bind_group_pair| bind_group_pair.get(motion_vectors)),
(false, false, Some(lightmap)) => self.lightmaps.get(&lightmap),
(false, false, None) => self.model_only.as_ref(),
}
}
}
impl MeshBindGroupPair {
fn get(&self, motion_vectors: bool) -> &BindGroup {
if motion_vectors {
&self.motion_vectors
} else {
&self.no_motion_vectors
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn prepare_mesh_bind_group(
meshes: Res<RenderAssets<GpuMesh>>,
@ -1874,11 +1991,12 @@ pub fn prepare_mesh_bind_group(
gpu_batched_instance_buffers: Option<
Res<gpu_preprocessing::BatchedInstanceBuffers<MeshUniform, MeshInputUniform>>,
>,
skins_uniform: Res<SkinUniform>,
weights_uniform: Res<MorphUniform>,
skins_uniform: Res<SkinUniforms>,
weights_uniform: Res<MorphUniforms>,
render_lightmaps: Res<RenderLightmaps>,
) {
groups.reset();
let layouts = &mesh_pipeline.mesh_layouts;
let model = if let Some(cpu_batched_instance_buffer) = cpu_batched_instance_buffer {
@ -1896,20 +2014,63 @@ 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(MeshBindGroupPair {
motion_vectors: layouts.skinned_motion(&render_device, &model, skin, prev_skin),
no_motion_vectors: layouts.skinned(&render_device, &model, 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)
} else {
layouts.morphed(&render_device, &model, weights, targets)
let bind_group_pair = match skin.filter(|_| is_skinned(&gpu_mesh.layout)) {
Some(skin) => {
let prev_skin = skins_uniform.prev_buffer.buffer().unwrap_or(skin);
MeshBindGroupPair {
motion_vectors: layouts.morphed_skinned_motion(
&render_device,
&model,
skin,
weights,
targets,
prev_skin,
prev_weights,
),
no_motion_vectors: layouts.morphed_skinned(
&render_device,
&model,
skin,
weights,
targets,
),
}
}
None => MeshBindGroupPair {
motion_vectors: layouts.morphed_motion(
&render_device,
&model,
weights,
targets,
prev_weights,
),
no_motion_vectors: layouts.morphed(
&render_device,
&model,
weights,
targets,
),
},
};
groups.morph_targets.insert(id, group);
groups.morph_targets.insert(id, bind_group_pair);
}
}
}
@ -1973,13 +2134,13 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshBindGroup<I> {
SRes<MorphIndices>,
SRes<RenderLightmaps>,
);
type ViewQuery = ();
type ViewQuery = Has<MotionVectorPrepass>;
type ItemQuery = ();
#[inline]
fn render<'w>(
item: &P,
_view: (),
has_motion_vector_prepass: bool,
_item_query: Option<()>,
(bind_groups, mesh_instances, skin_indices, morph_indices, lightmaps): SystemParamItem<
'w,
@ -1998,19 +2159,26 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshBindGroup<I> {
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
.get(entity)
.map(|render_lightmap| render_lightmap.image);
let Some(bind_group) = bind_groups.get(mesh_asset_id, lightmap, is_skinned, is_morphed)
else {
let Some(bind_group) = bind_groups.get(
mesh_asset_id,
lightmap,
is_skinned,
is_morphed,
has_motion_vector_prepass,
) else {
error!(
"The MeshBindGroups resource wasn't set in the render phase. \
It should be set by the prepare_mesh_bind_group system.\n\
@ -2025,14 +2193,40 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshBindGroup<I> {
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 motion vectors if needed.
if has_motion_vector_prepass {
// 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

View file

@ -106,16 +106,29 @@ pub struct MeshLayouts {
/// Also includes the uniform for skinning
pub skinned: BindGroupLayout,
/// Like [`MeshLayouts::skinned`], but includes slots for the previous
/// frame's joint matrices, so that we can compute motion vectors.
pub skinned_motion: BindGroupLayout,
/// Also includes the uniform and [`MorphAttributes`] for morph targets.
///
/// [`MorphAttributes`]: bevy_render::mesh::morph::MorphAttributes
pub morphed: BindGroupLayout,
/// Like [`MeshLayouts::morphed`], but includes a slot for the previous
/// frame's morph weights, so that we can compute motion vectors.
pub morphed_motion: BindGroupLayout,
/// Also includes both uniforms for skinning and morph targets, also the
/// morph target [`MorphAttributes`] binding.
///
/// [`MorphAttributes`]: bevy_render::mesh::morph::MorphAttributes
pub morphed_skinned: BindGroupLayout,
/// Like [`MeshLayouts::morphed_skinned`], but includes slots for the
/// previous frame's joint matrices and morph weights, so that we can
/// compute motion vectors.
pub morphed_skinned_motion: BindGroupLayout,
}
impl MeshLayouts {
@ -127,8 +140,11 @@ impl MeshLayouts {
model_only: Self::model_only_layout(render_device),
lightmapped: Self::lightmapped_layout(render_device),
skinned: Self::skinned_layout(render_device),
skinned_motion: Self::skinned_motion_layout(render_device),
morphed: Self::morphed_layout(render_device),
morphed_motion: Self::morphed_motion_layout(render_device),
morphed_skinned: Self::morphed_skinned_layout(render_device),
morphed_skinned_motion: Self::morphed_skinned_motion_layout(render_device),
}
}
@ -143,6 +159,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 +168,32 @@ impl MeshLayouts {
ShaderStages::VERTEX,
(
(0, layout_entry::model(render_device)),
// The current frame's joint matrix buffer.
(1, layout_entry::skinning()),
),
),
)
}
/// Creates the layout for skinned meshes with the infrastructure to compute
/// motion vectors.
fn skinned_motion_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(
"skinned_motion_mesh_layout",
&BindGroupLayoutEntries::with_indices(
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 +201,35 @@ 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()),
),
),
)
}
/// Creates the layout for meshes with morph targets and the infrastructure
/// to compute motion vectors.
fn morphed_motion_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(
"morphed_mesh_layout",
&BindGroupLayoutEntries::with_indices(
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 +237,43 @@ 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()),
),
),
)
}
/// Creates the bind group layout for meshes with both skins and morph
/// targets, in addition to the infrastructure to compute motion vectors.
fn morphed_skinned_motion_layout(render_device: &RenderDevice) -> BindGroupLayout {
render_device.create_bind_group_layout(
"morphed_skinned_motion_mesh_layout",
&BindGroupLayoutEntries::with_indices(
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 +297,7 @@ impl MeshLayouts {
&[entry::model(0, model.clone())],
)
}
pub fn lightmapped(
&self,
render_device: &RenderDevice,
@ -221,23 +314,55 @@ impl MeshLayouts {
],
)
}
/// Creates the bind group for skinned meshes with no morph targets.
pub fn skinned(
&self,
render_device: &RenderDevice,
model: &BindingResource,
skin: &Buffer,
current_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),
],
)
}
/// Creates the bind group for skinned meshes with no morph targets, with
/// the infrastructure to compute motion vectors.
///
/// `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_motion(
&self,
render_device: &RenderDevice,
model: &BindingResource,
current_skin: &Buffer,
prev_skin: &Buffer,
) -> BindGroup {
render_device.create_bind_group(
"skinned_motion_mesh_bind_group",
&self.skinned_motion,
&[
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.
pub fn morphed(
&self,
render_device: &RenderDevice,
model: &BindingResource,
weights: &Buffer,
current_weights: &Buffer,
targets: &TextureView,
) -> BindGroup {
render_device.create_bind_group(
@ -245,17 +370,47 @@ impl MeshLayouts {
&self.morphed,
&[
entry::model(0, model.clone()),
entry::weights(2, weights),
entry::weights(2, current_weights),
entry::targets(3, targets),
],
)
}
/// Creates the bind group for meshes with no skins but morph targets, in
/// addition to the infrastructure to compute motion vectors.
///
/// `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_motion(
&self,
render_device: &RenderDevice,
model: &BindingResource,
current_weights: &Buffer,
targets: &TextureView,
prev_weights: &Buffer,
) -> BindGroup {
render_device.create_bind_group(
"morphed_motion_mesh_bind_group",
&self.morphed_motion,
&[
entry::model(0, model.clone()),
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.
#[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,
) -> BindGroup {
render_device.create_bind_group(
@ -263,10 +418,42 @@ impl MeshLayouts {
&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),
],
)
}
/// Creates the bind group for meshes with skins and morph targets, in
/// addition to the infrastructure to compute motion vectors.
///
/// See the documentation for [`MeshLayouts::skinned_motion`] and
/// [`MeshLayouts::morphed_motion`] 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_motion(
&self,
render_device: &RenderDevice,
model: &BindingResource,
current_skin: &Buffer,
current_weights: &Buffer,
targets: &TextureView,
prev_skin: &Buffer,
prev_weights: &Buffer,
) -> BindGroup {
render_device.create_bind_group(
"morphed_skinned_motion_mesh_bind_group",
&self.morphed_skinned_motion,
&[
entry::model(0, model.clone()),
entry::skinning(1, current_skin),
entry::weights(2, current_weights),
entry::targets(3, targets),
entry::skinning(6, prev_skin),
entry::weights(7, prev_weights),
],
)
}
}

View file

@ -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};

View file

@ -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<MorphIndex>);
/// 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<MorphIndex>,
#[derive(Resource)]
pub struct MorphUniform {
pub buffer: RawBufferVec<f32>,
/// Maps each entity with a morphed mesh to the appropriate offset within
/// [`MorphUniforms::prev_buffer`].
pub prev: EntityHashMap<MorphIndex>,
}
impl Default for MorphUniform {
/// 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 MorphUniforms {
/// The morph weights for the current frame.
pub current_buffer: RawBufferVec<f32>,
/// The morph weights for the previous frame.
pub prev_buffer: RawBufferVec<f32>,
}
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<RenderDevice>,
render_queue: Res<RenderQueue>,
mut uniform: ResMut<MorphUniform>,
mut uniform: ResMut<MorphUniforms>,
) {
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<T: NoUninit + Default>(buffer: &mut RawBufferVec<T>) {
// 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<MorphIndices>,
mut uniform: ResMut<MorphUniform>,
morph_indices: ResMut<MorphIndices>,
uniform: ResMut<MorphUniforms>,
query: Extract<Query<(Entity, &ViewVisibility, &MeshMorphWeights)>>,
) {
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::<f32>(&mut uniform.buffer);
uniform.current_buffer.extend(legal_weights);
add_to_alignment::<f32>(&mut uniform.current_buffer);
let index = (start * mem::size_of::<f32>()) as u32;
morph_indices.insert(entity, MorphIndex { index });
morph_indices.current.insert(entity, MorphIndex { index });
}
}

View file

@ -6,6 +6,7 @@
@group(1) @binding(2) var<uniform> morph_weights: MorphWeights;
@group(1) @binding(3) var morph_targets: texture_3d<f32>;
@group(1) @binding(7) var<uniform> 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

View file

@ -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<SkinIndex>);
/// 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<SkinIndex>,
// Notes on implementation: see comment on top of the `extract_skins` system.
#[derive(Resource)]
pub struct SkinUniform {
pub buffer: RawBufferVec<Mat4>,
/// Maps each skinned mesh to the applicable offset within
/// [`SkinUniforms::prev_buffer`].
pub prev: EntityHashMap<SkinIndex>,
}
impl Default for SkinUniform {
/// 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 SkinUniforms {
/// Stores all the joint matrices for skinned meshes in the current frame.
pub current_buffer: RawBufferVec<Mat4>,
/// Stores all the joint matrices for skinned meshes in the previous frame.
pub prev_buffer: RawBufferVec<Mat4>,
}
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<RenderDevice>,
render_queue: Res<RenderQueue>,
mut uniform: ResMut<SkinUniform>,
mut uniform: ResMut<SkinUniforms>,
) {
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<SkinIndices>,
mut uniform: ResMut<SkinUniform>,
skin_indices: ResMut<SkinIndices>,
uniform: ResMut<SkinUniforms>,
query: Extract<Query<(Entity, &ViewVisibility, &SkinnedMesh)>>,
inverse_bindposes: Extract<Res<Assets<SkinnedMeshInverseBindposes>>>,
joints: Extract<Query<&GlobalTransform>>,
) {
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);
}
}

View file

@ -6,6 +6,14 @@
@group(1) @binding(1) var<uniform> 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<uniform> prev_joint_matrices: SkinnedMesh;
fn skin_model(
indexes: vec4<u32>,
weights: vec4<f32>,
@ -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<u32>,
weights: vec4<f32>,
) -> mat4x4<f32> {
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<f32>) -> mat3x3<f32> {
let x = cross(in[1], in[2]);
let y = cross(in[2], in[0]);