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

Per-Object Motion Blur #9924

Merged
merged 46 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
f06f766
Motion blur implementation
aevyrie Sep 25, 2023
7a4a86a
Updated example readme
aevyrie Sep 25, 2023
94425b6
remove unused imports from shader
aevyrie Sep 25, 2023
d44d366
CI nit
aevyrie Sep 25, 2023
82aed62
fix for web and improve noise
aevyrie Sep 26, 2023
0b04699
improve example
aevyrie Sep 26, 2023
81d5e32
fix system ordering ambiguity
aevyrie Sep 26, 2023
6e42f15
More example improvements
aevyrie Sep 26, 2023
2a4f346
motion based sample filtering
aevyrie Sep 29, 2023
1efce9d
Improve naming
aevyrie Oct 2, 2023
f20bccf
Tweak example and defaults
aevyrie Oct 2, 2023
e318c86
fix offset error
aevyrie Oct 2, 2023
cd59ff5
Add more context for motion filtering
aevyrie Oct 2, 2023
7e484ce
add depth and vel checks
aevyrie Nov 2, 2023
c1de46b
Merge remote-tracking branch 'origin/main' into motion-blur
aevyrie Dec 9, 2023
fe2b514
Fix merge errors
aevyrie Dec 9, 2023
6b221b6
reduce artifacts
aevyrie Dec 9, 2023
6f342c0
allow single sample setting
aevyrie Dec 9, 2023
e8d79b7
shader docs
aevyrie Dec 9, 2023
5b6c845
simplify shader and fix artifacts from discontinuity
aevyrie Dec 10, 2023
e49f71d
Remove dependency on depth prepass
aevyrie Dec 10, 2023
7540805
Fix warning in release builds
aevyrie Dec 10, 2023
4268f88
Fix artifacts caused by missing samples
aevyrie Dec 10, 2023
f27d0be
improve docs and defaults
aevyrie Dec 10, 2023
f243e8b
Blur bg samples to fix under blur
aevyrie Dec 11, 2023
23e4f9e
Documentation and review feedback
aevyrie Dec 13, 2023
783f368
review feedback
aevyrie Dec 13, 2023
1e0fdc4
Remove use of bevy_internal
aevyrie Dec 13, 2023
975ca29
Improve demo appearance
aevyrie Dec 13, 2023
6c4ee36
Merge remote-tracking branch 'origin/main' into motion-blur
aevyrie Dec 15, 2023
d2cdb06
Fix for renaming ViewQuery to ViewData
aevyrie Dec 15, 2023
51fb2d3
Cleanup
aevyrie Feb 3, 2024
65586be
Review feedback
aevyrie Feb 3, 2024
ed2c1c7
Merge branch 'main' into motion-blur
aevyrie Feb 3, 2024
9d5aa8b
review feedback
aevyrie Feb 3, 2024
8be1d96
Avoid running nodes with no blur
aevyrie Feb 4, 2024
3067923
improve example
aevyrie Feb 4, 2024
79ec4c2
Merge branch 'main' into motion-blur
aevyrie Feb 4, 2024
5c117ed
Merge remote-tracking branch 'upstream/main' into motion-blur
aevyrie Feb 25, 2024
e917032
Add moving camera toggle to example
aevyrie Feb 25, 2024
cb7c5ca
Shorten example descriptions
aevyrie Feb 25, 2024
46a2b55
Merge branch 'main' into motion-blur
aevyrie Apr 12, 2024
3aa4799
update example
aevyrie Apr 12, 2024
8a642ad
Use generated example readme
aevyrie Apr 12, 2024
d335565
Review feedback
aevyrie Apr 19, 2024
673e90d
Merge branch 'main' into motion-blur
aevyrie Apr 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.toml
Expand Up @@ -620,6 +620,17 @@ description = "Loads and renders a glTF file as a scene"
category = "3D Rendering"
wasm = true

[[example]]
name = "motion_blur"
path = "examples/3d/motion_blur.rs"
doc-scrape-examples = true

[package.metadata.example.motion_blur]
name = "Motion Blur"
description = "Demonstrates per-pixel motion blur"
category = "3D Rendering"
wasm = false

[[example]]
name = "tonemapping"
path = "examples/3d/tonemapping.rs"
Expand Down
Binary file added assets/textures/checkered.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions crates/bevy_core_pipeline/src/core_3d/mod.rs
Expand Up @@ -14,6 +14,7 @@ pub mod graph {
pub const MAIN_OPAQUE_PASS: &str = "main_opaque_pass";
pub const MAIN_TRANSPARENT_PASS: &str = "main_transparent_pass";
pub const END_MAIN_PASS: &str = "end_main_pass";
pub const MOTION_BLUR: &str = "motion_blur";
pub const BLOOM: &str = "bloom";
pub const TONEMAPPING: &str = "tonemapping";
pub const FXAA: &str = "fxaa";
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_core_pipeline/src/lib.rs
Expand Up @@ -8,6 +8,7 @@ pub mod core_2d;
pub mod core_3d;
pub mod fullscreen_vertex_shader;
pub mod fxaa;
pub mod motion_blur;
pub mod msaa_writeback;
pub mod prepass;
mod skybox;
Expand Down Expand Up @@ -42,6 +43,7 @@ use crate::{
core_3d::Core3dPlugin,
fullscreen_vertex_shader::FULLSCREEN_SHADER_HANDLE,
fxaa::FxaaPlugin,
motion_blur::MotionBlurPlugin,
msaa_writeback::MsaaWritebackPlugin,
prepass::{DepthPrepass, NormalPrepass},
tonemapping::TonemappingPlugin,
Expand Down Expand Up @@ -79,6 +81,7 @@ impl Plugin for CorePipelinePlugin {
BloomPlugin,
FxaaPlugin,
CASPlugin,
MotionBlurPlugin,
));
}
}
129 changes: 129 additions & 0 deletions crates/bevy_core_pipeline/src/motion_blur/mod.rs
@@ -0,0 +1,129 @@
//! Per-object motion blur.
//!
//! Add the [`MotionBlurBundle`] to a camera to enable motion blur.

use crate::core_3d;
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, Handle};
use bevy_ecs::{bundle::Bundle, component::Component, schedule::IntoSystemConfigs};
use bevy_render::{
extract_component::{ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin},
render_graph::{RenderGraphApp, ViewNodeRunner},
render_resource::{Shader, ShaderType, SpecializedRenderPipelines},
renderer::RenderDevice,
Render, RenderApp, RenderSet,
};

pub mod node;
pub mod pipeline;

#[derive(Bundle, Default)]
pub struct MotionBlurBundle {
pub motion_blur: MotionBlur,
pub depth_prepass: crate::prepass::DepthPrepass,
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
pub motion_vector_prepass: crate::prepass::MotionVectorPrepass,
}

/// A component that enables and configures motion blur when added to a camera.
#[derive(Component, Clone, Copy, Debug, ExtractComponent, ShaderType)]
pub struct MotionBlur {
/// Camera shutter angle from 0 to 1 (0-100%), which determines the strength of the blur.
///
/// The shutter angle describes the fraction of a frame that a camera's shutter is open and
/// exposing the film/sensor. For 24fps cinematic film, a shutter angle of 0.5 (180 degrees) is
/// common. This means that the shutter was open for half of the frame, or 1/48th of a second.
/// The lower the shutter angle, the less exposure time and thus less blur. A value greater than
/// one is unrealistic and results in an object's blur stretching further than it traveled in
/// that frame.
pub shutter_angle: f32,
/// The upper limit for how many samples will be taken per-pixel. The number of samples taken
/// depends on the speed of an object.
pub max_samples: u32,
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
/// Motion blur will consider fragments closer to the camera than the fragment being blurred,
/// but only by this small amount.
///
/// Without considering depth, fast moving objects moving behind a stationary object will sample
/// this stationary object, and cause it to incorrectly smear into the moving object. To solve
/// this, per-object motion blur can filter out samples by depth, ignoring fragments that are
/// closer to the camera than the fragment being blurred.
///
/// However, it's desireable to add some tolerance to this comparison. Consider blurring the
/// edge of the sphere. When sampling fragments to create blur at the edge of the sphere,
/// sampling the center of the sphere will fail because the center of the sphere is closer to
/// the camera than the edge is! The `depth_bias` adds tolerance to allow for cases like this.
///
/// You can effectively disable this by setting the bias to -1.0. This will result in streaking,
/// which may be artistically desired.
pub depth_bias: f32,
#[cfg(all(feature = "webgl", target_arch = "wasm32"))]
// WebGL2 structs must be 16 byte aligned.
pub _webgl2_padding: bevy_math::Vec3,
}

impl Default for MotionBlur {
fn default() -> Self {
Self {
shutter_angle: 1.0,
max_samples: 8,
depth_bias: -0.001,
#[cfg(all(feature = "webgl", target_arch = "wasm32"))]
_webgl2_padding: bevy_math::Vec3::default(),
}
}
}

pub const MOTION_BLUR_SHADER_HANDLE: Handle<Shader> =
Handle::weak_from_u128(987457899187986082347921);

/// Adds support for per-object motion blur to the app.
pub struct MotionBlurPlugin;
impl Plugin for MotionBlurPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
MOTION_BLUR_SHADER_HANDLE,
"motion_blur.wgsl",
Shader::from_wgsl
);

app.add_plugins((
ExtractComponentPlugin::<MotionBlur>::default(),
UniformComponentPlugin::<MotionBlur>::default(),
));

let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
return;
};

render_app
.init_resource::<SpecializedRenderPipelines<pipeline::MotionBlurPipeline>>()
.add_systems(
Render,
(pipeline::prepare_motion_blur_pipelines.in_set(RenderSet::Prepare),),
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
);

render_app
.add_render_graph_node::<ViewNodeRunner<node::MotionBlurNode>>(
core_3d::graph::NAME,
core_3d::graph::node::MOTION_BLUR,
)
.add_render_graph_edges(
core_3d::graph::NAME,
&[
core_3d::graph::node::END_MAIN_PASS,
core_3d::graph::node::MOTION_BLUR,
core_3d::graph::node::BLOOM, // we want blurred areas to bloom and tonemap properly.
],
);
}

fn finish(&self, app: &mut App) {
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};

let render_device = render_app.world.resource::<RenderDevice>().clone();

render_app.insert_resource(pipeline::MotionBlurPipeline::new(&render_device));
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
}
}
83 changes: 83 additions & 0 deletions crates/bevy_core_pipeline/src/motion_blur/motion_blur.wgsl
@@ -0,0 +1,83 @@
#import bevy_pbr::prepass_utils
#import bevy_pbr::utils
#import bevy_core_pipeline::fullscreen_vertex_shader FullscreenVertexOutput

#ifdef MULTISAMPLED
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
@group(0) @binding(0) var screen_texture: texture_2d<f32>;
@group(0) @binding(1) var motion_vectors: texture_multisampled_2d<f32>;
@group(0) @binding(2) var depth: texture_depth_multisampled_2d;
#else
@group(0) @binding(0) var screen_texture: texture_2d<f32>;
@group(0) @binding(1) var motion_vectors: texture_2d<f32>;
@group(0) @binding(2) var depth: texture_depth_2d;
#endif
@group(0) @binding(3) var texture_sampler: sampler;
struct PostProcessSettings {
shutter_angle: f32,
max_samples: u32,
depth_bias: f32,
#ifdef SIXTEEN_BYTE_ALIGNMENT
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
// WebGL2 structs must be 16 byte aligned.
_webgl2_padding: vec3<f32>
#endif
}
@group(0) @binding(4) var<uniform> settings: PostProcessSettings;

@fragment
fn fragment(
#ifdef MULTISAMPLED
@builtin(sample_index) sample_index: u32,
#endif
in: FullscreenVertexOutput
) -> @location(0) vec4<f32> {
#ifndef MULTISAMPLED
let sample_index = 0u;
#endif
let shutter_angle = settings.shutter_angle;
let texture_size = vec2<f32>(textureDimensions(screen_texture));
let frag_coords = vec2<i32>(in.uv * texture_size);

let motion_vector = textureLoad(motion_vectors, frag_coords, i32(sample_index)).rg;
let exposure_vector = shutter_angle * motion_vector;
let speed = length(exposure_vector * texture_size);
let n_samples = i32(clamp(speed * 2.0, 1.0, f32(settings.max_samples)));

let this_depth = textureLoad(depth, frag_coords, i32(sample_index));
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
let base_color = textureSample(screen_texture, texture_sampler, in.uv);
var weight_total = 0.0;
var accumulator = vec4<f32>(0.0);

for (var i = 0; i < n_samples; i++) {
var offset = vec2<f32>(0.0);
if speed > 1.0 && n_samples > 1 {
offset = exposure_vector * ((f32(i) + noise(in.uv)) / f32(n_samples) - 0.5);
}
let sample_uv = in.uv + offset;
let sample_coords = vec2<i32>(sample_uv * texture_size);

// If depth is not considered during sampling, you can end up sampling objects in front of a
// fast moving object, which will cause the (possibly stationary) objects in front of that
// fast moving object to smear. To prevent this, we check the depth of the fragment we are
// sampling. If it is closer to the camera than this fragment (plus the user-defined bias),
// we discard it. If the bias is too small, fragments from the same object will be filtered
// out.
let sample_depth = textureLoad(depth, sample_coords, i32(sample_index));
let weight = step(settings.depth_bias, this_depth - sample_depth);

weight_total += weight;
accumulator += weight * textureSample(screen_texture, texture_sampler, sample_uv);
}

// Avoid black pixels by falling back to the unblurred fragment color
if weight_total == 0.0 {
accumulator = base_color;
weight_total = 1.0;
}

return accumulator / weight_total;
}

fn noise(frag_coord: vec2<f32>) -> f32 {
let k1 = vec2<f32>(23.14069263277926, 2.665144142690225);
return fract(cos(dot(frag_coord, k1)) * 12345.6789);
}
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
108 changes: 108 additions & 0 deletions crates/bevy_core_pipeline/src/motion_blur/node.rs
@@ -0,0 +1,108 @@
use bevy_ecs::{query::QueryItem, world::World};
use bevy_render::{
extract_component::ComponentUniforms,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_resource::{
BindGroupDescriptor, BindGroupEntry, BindingResource, Operations, PipelineCache,
RenderPassColorAttachment, RenderPassDescriptor,
},
renderer::RenderContext,
view::{Msaa, ViewTarget},
};

use crate::prepass::ViewPrepassTextures;

use super::{
pipeline::{MotionBlurPipeline, MotionBlurPipelineId},
MotionBlur,
};

#[derive(Default)]
pub struct MotionBlurNode;

impl ViewNode for MotionBlurNode {
type ViewQuery = (
&'static ViewTarget,
&'static MotionBlurPipelineId,
&'static ViewPrepassTextures,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(view_target, pipeline_id, prepass_textures): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
let motion_blur_pipeline = world.resource::<MotionBlurPipeline>();
let pipeline_cache = world.resource::<PipelineCache>();
let settings_uniforms = world.resource::<ComponentUniforms<MotionBlur>>();
let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline_id.0) else {
return Ok(());
};

let Some(settings_binding) = settings_uniforms.uniforms().binding() else {
return Ok(());
};
let (Some(prepass_motion_vectors_texture), Some(prepass_depth_texture)) =
(&prepass_textures.motion_vectors, &prepass_textures.depth)
else {
return Ok(());
};

let post_process = view_target.post_process_write();

let msaa = world.resource::<Msaa>();
let layout = if msaa.samples() == 1 {
&motion_blur_pipeline.layout
} else {
&motion_blur_pipeline.layout_msaa
};

let bind_group = render_context
.render_device()
.create_bind_group(&BindGroupDescriptor {
label: Some("motion_blur_bind_group"),
layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureView(post_process.source),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::TextureView(
&prepass_motion_vectors_texture.default_view,
),
},
BindGroupEntry {
binding: 2,
resource: BindingResource::TextureView(&prepass_depth_texture.default_view),
},
BindGroupEntry {
binding: 3,
resource: BindingResource::Sampler(&motion_blur_pipeline.sampler),
},
BindGroupEntry {
binding: 4,
resource: settings_binding.clone(),
},
],
});

let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("motion_blur_pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: post_process.destination,
resolve_target: None,
ops: Operations::default(),
})],
depth_stencil_attachment: None,
});

render_pass.set_render_pipeline(pipeline);
render_pass.set_bind_group(0, &bind_group, &[]);
render_pass.draw(0..3, 0..1);

Ok(())
}
}