Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add motion vectors support for animated meshes #9902

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions crates/bevy_core_pipeline/src/taa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,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.
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_pbr/src/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ type DrawMaterial<M> = (
SetItemPipeline,
SetMeshViewBindGroup<0>,
SetMaterialBindGroup<M, 1>,
SetMeshBindGroup<2>,
SetMeshBindGroup<2, false>,
DrawMesh,
);

Expand Down
21 changes: 17 additions & 4 deletions crates/bevy_pbr/src/prepass/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use bevy_render::{
use bevy_transform::prelude::GlobalTransform;
use bevy_utils::tracing::error;

use crate::render::{MorphIndices, SkinIndices};
use crate::*;

use std::{hash::Hash, marker::PhantomData};
Expand Down Expand Up @@ -344,7 +345,7 @@ where
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
let mut bind_group_layouts = vec![if key
.mesh_key
.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS)
.contains(MeshPipelineKey::VIEW_MOTION_VECTOR_PREPASS)
{
self.view_layout_motion_vectors.clone()
} else {
Expand Down Expand Up @@ -453,7 +454,7 @@ where
shader_defs.push("PREPASS_FRAGMENT".into());
}

let bind_group = setup_morph_and_skinning_defs(
let bind_group = setup_morph_and_skinning_defs::<true>(
&self.mesh_layouts,
layout,
4,
Expand All @@ -465,6 +466,8 @@ where

let vertex_buffer_layout = layout.get_layout(&vertex_attributes)?;

// Note that if you are updating this code, you probably need to update the equivalent
// code that depends on color target states in crates/bevy_core_pipeline/src/prepass/node.rs
// Setup prepass fragment targets - normals in slot 0 (or None if not needed), motion vectors in slot 1
let mut targets = vec![
key.mesh_key
Expand All @@ -476,7 +479,7 @@ where
write_mask: ColorWrites::ALL,
}),
key.mesh_key
.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS)
.contains(MeshPipelineKey::VIEW_MOTION_VECTOR_PREPASS)
.then_some(ColorTargetState {
format: MOTION_VECTOR_PREPASS_FORMAT,
// BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases.
Expand Down Expand Up @@ -721,6 +724,8 @@ pub fn queue_prepass_material_meshes<M: Material>(
render_mesh_instances: Res<RenderMeshInstances>,
render_materials: Res<RenderMaterials<M>>,
render_material_instances: Res<RenderMaterialInstances<M>>,
morph_indices: Res<MorphIndices>,
skin_indices: Res<SkinIndices>,
mut views: Query<
(
&ExtractedView,
Expand Down Expand Up @@ -781,6 +786,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
view_key |= MeshPipelineKey::NORMAL_PREPASS;
}
if motion_vector_prepass.is_some() {
view_key |= MeshPipelineKey::VIEW_MOTION_VECTOR_PREPASS;
view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS;
}

Expand All @@ -802,6 +808,13 @@ pub fn queue_prepass_material_meshes<M: Material>(

let mut mesh_key =
MeshPipelineKey::from_primitive_topology(mesh.primitive_topology) | view_key;

let missing_motion_vector = morph_indices.missing_previous(visible_entity)
|| skin_indices.missing_previous(visible_entity);

if missing_motion_vector {
mesh_key &= !MeshPipelineKey::MOTION_VECTOR_PREPASS;
}
if mesh.morph_targets.is_some() {
mesh_key |= MeshPipelineKey::MORPH_TARGETS;
}
Expand Down Expand Up @@ -960,7 +973,7 @@ pub type DrawPrepass<M> = (
SetItemPipeline,
SetPrepassViewBindGroup<0>,
SetMaterialBindGroup<M, 1>,
SetMeshBindGroup<2>,
SetMeshBindGroup<2, true>,
DrawMesh,
);

Expand Down
31 changes: 28 additions & 3 deletions crates/bevy_pbr/src/prepass/prepass.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,29 @@ fn morph_vertex(vertex_in: Vertex) -> Vertex {
}
return vertex;
}


#ifdef MOTION_VECTOR_PREPASS
fn morph_previous_position(vertex: Vertex) -> vec3<f32> {
var vertex_position = vertex.position;
let weight_count = bevy_pbr::morph::layer_count();
for (var i: u32 = 0u; i < weight_count; i++) {
let weight = bevy_pbr::morph::previous_weight_at(i);
if weight != 0.0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't new, so not blocking, but I wonder if this branch is helpful or harmful. It probably has to run both branches regardless I would think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a really good question. My GPU-fu is not strong enough to be able to answer it. I don't remember the motivation behind it.

vertex_position += weight * bevy_pbr::morph::morph(vertex.index, bevy_pbr::morph::position_offset, i);
}
}
return vertex_position;
}
#endif // MOTION_VECTOR_PREPASS
#endif

@vertex
fn vertex(vertex_no_morph: Vertex) -> VertexOutput {
var out: VertexOutput;

#ifdef MORPH_TARGETS
var vertex = morph::morph_vertex(vertex_no_morph);
var vertex = morph_vertex(vertex_no_morph);
#else
var vertex = vertex_no_morph;
#endif
Expand Down Expand Up @@ -94,11 +109,21 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput {
#endif // MOTION_VECTOR_PREPASS_OR_DEFERRED_PREPASS

#ifdef MOTION_VECTOR_PREPASS
#ifdef MORPH_TARGETS
var previous_position = morph_previous_position(vertex_no_morph);
#else
var previous_position = vertex.position;
#endif // MORPH_TARGETS
#ifdef SKINNED
var previous_model = skinning::previous_skin_model(vertex.joint_indices, vertex.joint_weights);
#else
// 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
var previous_model = mesh_functions::get_previous_model_matrix(vertex_no_morph.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)
previous_model,
vec4<f32>(previous_position, 1.0)
);
#endif // MOTION_VECTOR_PREPASS

Expand Down
62 changes: 62 additions & 0 deletions crates/bevy_pbr/src/render/double_buffer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use std::mem;

use bevy_ecs::prelude::Entity;
use bevy_render::render_resource::{BufferUsages, BufferVec};
use bevy_utils::EntityHashMap;
use bytemuck::Pod;

/// A double buffer of `T`.
///
/// Use [`DoubleBuffer::swap`] to swap buffer,
/// access the current buffer with [`DoubleBuffer::current`],
/// and the previous one with [`DoubleBuffer::previous`].
#[derive(Default)]
pub struct DoubleBuffer<T> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big deal for now, but I'd like to have some MultiBuffer<const N: usize, T> where N is the number of buffers that get rotated through. It is apparently common to use 3-4 buffers of dynamic per-frame data like mesh uniforms. This is because the GPU can have 2-3 frames in flight, meaning the rendering depends on the contents of those buffers, and if we want to update the contents, wgpu would have to wait until the target buffer is no longer in use by the GPU to be able to transfer the data to it. We can take that in a separate PR though, and just make DoubleBuffer<T> into MultiBuffer<2, T>.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like a fun thing to implement. But I think it should be part of a different PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be in bevy_render.

pub previous: T,
pub current: T,
}

impl<T> DoubleBuffer<T> {
nicopap marked this conversation as resolved.
Show resolved Hide resolved
pub fn swap(&mut self, swap_buffer: bool) {
if swap_buffer {
mem::swap(&mut self.current, &mut self.previous);
}
}
}

pub type DoubleBufferVec<T> = DoubleBuffer<BufferVec<T>>;

impl<T: Pod> DoubleBufferVec<T> {
pub const fn new(buffer_usage: BufferUsages) -> Self {
DoubleBufferVec {
previous: BufferVec::new(buffer_usage),
current: BufferVec::new(buffer_usage),
}
}

pub fn clear(&mut self, swap_buffer: bool) {
nicopap marked this conversation as resolved.
Show resolved Hide resolved
self.swap(swap_buffer);
self.current.clear();
}
}

pub type DoubleEntityMap<T> = DoubleBuffer<EntityHashMap<Entity, T>>;

impl<T> DoubleEntityMap<T> {
pub fn clear(&mut self, swap_buffer: bool) {
self.swap(swap_buffer);
self.current.clear();
}

pub fn insert(&mut self, entity: Entity, value: T) {
nicopap marked this conversation as resolved.
Show resolved Hide resolved
self.current.insert(entity, value);
}

pub fn missing_previous(&self, entity: &Entity) -> bool {
nicopap marked this conversation as resolved.
Show resolved Hide resolved
let current = self.current.contains_key(entity);
let previous = self.previous.contains_key(entity);
// Either it's already missing (therefore there is no "previous" to miss)
// or it's not missing and there is no "previous", so we miss previous.
current && !previous
}
}
Loading