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 all 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 @@ -752,6 +752,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
1 change: 1 addition & 0 deletions crates/bevy_core_pipeline/src/core_3d/mod.rs
Expand Up @@ -26,6 +26,7 @@ pub mod graph {
MainTransparentPass,
EndMainPass,
Taa,
MotionBlur,
Bloom,
Tonemapping,
Fxaa,
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_core_pipeline/src/lib.rs
Expand Up @@ -15,6 +15,7 @@ pub mod core_3d;
pub mod deferred;
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 @@ -53,6 +54,7 @@ use crate::{
deferred::copy_lighting_id::CopyDeferredLightingIdPlugin,
fullscreen_vertex_shader::FULLSCREEN_SHADER_HANDLE,
fxaa::FxaaPlugin,
motion_blur::MotionBlurPlugin,
msaa_writeback::MsaaWritebackPlugin,
prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass},
tonemapping::TonemappingPlugin,
Expand Down Expand Up @@ -89,6 +91,7 @@ impl Plugin for CorePipelinePlugin {
BloomPlugin,
FxaaPlugin,
CASPlugin,
MotionBlurPlugin,
));
}
}
168 changes: 168 additions & 0 deletions crates/bevy_core_pipeline/src/motion_blur/mod.rs
@@ -0,0 +1,168 @@
//! Per-object, per-pixel motion blur.
//!
//! Add the [`MotionBlurBundle`] to a camera to enable motion blur. See [`MotionBlur`] for more
//! documentation.

use crate::{
core_3d::graph::{Core3d, Node3d},
prepass::{DepthPrepass, MotionVectorPrepass},
};
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, Handle};
use bevy_ecs::{
bundle::Bundle, component::Component, query::With, reflect::ReflectComponent,
schedule::IntoSystemConfigs,
};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
camera::Camera,
extract_component::{ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin},
render_graph::{RenderGraphApp, ViewNodeRunner},
render_resource::{Shader, ShaderType, SpecializedRenderPipelines},
Render, RenderApp, RenderSet,
};

pub mod node;
pub mod pipeline;

/// Adds [`MotionBlur`] and the required depth and motion vector prepasses to a camera entity.
#[derive(Bundle, Default)]
pub struct MotionBlurBundle {
pub motion_blur: MotionBlur,
pub depth_prepass: DepthPrepass,
pub motion_vector_prepass: MotionVectorPrepass,
}

/// A component that enables and configures motion blur when added to a camera.
///
/// Motion blur is an effect that simulates how moving objects blur as they change position during
/// the exposure of film, a sensor, or an eyeball.
///
/// Because rendering simulates discrete steps in time, we use per-pixel motion vectors to estimate
/// the path of objects between frames. This kind of implementation has some artifacts:
/// - Fast moving objects in front of a stationary object or when in front of empty space, will not
/// have their edges blurred.
/// - Transparent objects do not write to depth or motion vectors, so they cannot be blurred.
///
/// Other approaches, such as *A Reconstruction Filter for Plausible Motion Blur* produce more
/// correct results, but are more expensive and complex, and have other kinds of artifacts. This
/// implementation is relatively inexpensive and effective.
///
/// # Usage
///
/// Add the [`MotionBlur`] component to a camera to enable and configure motion blur for that
/// camera. Motion blur also requires the depth and motion vector prepass, which can be added more
/// easily to the camera with the [`MotionBlurBundle`].
///
/// ```
/// # use bevy_core_pipeline::{core_3d::Camera3dBundle, motion_blur::MotionBlurBundle};
/// # use bevy_ecs::prelude::*;
/// # fn test(mut commands: Commands) {
/// commands.spawn((
/// Camera3dBundle::default(),
/// MotionBlurBundle::default(),
/// ));
/// # }
/// ````
#[derive(Reflect, Component, Clone, ExtractComponent, ShaderType)]
#[reflect(Component, Default)]
#[extract_component_filter(With<Camera>)]
pub struct MotionBlur {
/// The strength of motion blur from `0.0` to `1.0`.
///
/// 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 non-physical and results in an object's blur stretching further
/// than it traveled in that frame. This might be a desirable effect for artistic reasons, but
/// consider allowing users to opt out of this.
///
/// This value is intentionally tied to framerate to avoid the aforementioned non-physical
/// over-blurring. If you want to emulate a cinematic look, your options are:
/// - Framelimit your app to 24fps, and set the shutter angle to 0.5 (180 deg). Note that
/// depending on artistic intent or the action of a scene, it is common to set the shutter
/// angle between 0.125 (45 deg) and 0.5 (180 deg). This is the most faithful way to
/// reproduce the look of film.
/// - Set the shutter angle greater than one. For example, to emulate the blur strength of
/// film while rendering at 60fps, you would set the shutter angle to `60/24 * 0.5 = 1.25`.
/// Note that this will result in artifacts where the motion of objects will stretch further
/// than they moved between frames; users may find this distracting.
pub shutter_angle: f32,
/// The quality of motion blur, corresponding to the number of per-pixel samples taken in each
/// direction during blur.
///
/// Setting this to `1` results in each pixel being sampled once in the leading direction, once
/// in the trailing direction, and once in the middle, for a total of 3 samples (`1 * 2 + 1`).
/// Setting this to `3` will result in `3 * 2 + 1 = 7` samples. Setting this to `0` is
/// equivalent to disabling motion blur.
pub samples: u32,
#[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: 0.5,
samples: 1,
#[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. See [`MotionBlur`] for details.
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 Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};

render_app
.init_resource::<SpecializedRenderPipelines<pipeline::MotionBlurPipeline>>()
.add_systems(
Render,
pipeline::prepare_motion_blur_pipelines.in_set(RenderSet::Prepare),
);

render_app
.add_render_graph_node::<ViewNodeRunner<node::MotionBlurNode>>(
Core3d,
Node3d::MotionBlur,
)
.add_render_graph_edges(
Core3d,
(
Node3d::EndMainPass,
Node3d::MotionBlur,
Node3d::Bloom, // we want blurred areas to bloom and tonemap properly.
),
);
}

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

render_app.init_resource::<pipeline::MotionBlurPipeline>();
}
}
149 changes: 149 additions & 0 deletions crates/bevy_core_pipeline/src/motion_blur/motion_blur.wgsl
@@ -0,0 +1,149 @@
#import bevy_pbr::prepass_utils
#import bevy_pbr::utils
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
#import bevy_render::globals::Globals

#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 MotionBlur {
shutter_angle: f32,
samples: u32,
#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: MotionBlur;
@group(0) @binding(5) var<uniform> globals: Globals;

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

#ifdef MULTISAMPLED
let base_color = textureLoad(screen_texture, frag_coords, i32(sample_index));
#else
let base_color = textureSample(screen_texture, texture_sampler, in.uv);
#endif

let shutter_angle = settings.shutter_angle;

#ifdef MULTISAMPLED
let this_motion_vector = textureLoad(motion_vectors, frag_coords, i32(sample_index)).rg;
#else
let this_motion_vector = textureSample(motion_vectors, texture_sampler, in.uv).rg;
#endif

#ifdef NO_DEPTH_TEXTURE_SUPPORT
let this_depth = 0.0;
let depth_supported = false;
#else
let depth_supported = true;
#ifdef MULTISAMPLED
let this_depth = textureLoad(depth, frag_coords, i32(sample_index));
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
#else
let this_depth = textureSample(depth, texture_sampler, in.uv);
#endif
#endif

// The exposure vector is the distance that this fragment moved while the camera shutter was
// open. This is the motion vector (total distance traveled) multiplied by the shutter angle (a
// fraction). In film, the shutter angle is commonly 0.5 or "180 degrees" (out of 360 total).
// This means that for a frame time of 20ms, the shutter is only open for 10ms.
//
// Using a shutter angle larger than 1.0 is non-physical, objects would need to move further
// than they physically travelled during a frame, which is not possible. Note: we allow values
// larger than 1.0 because it may be desired for artistic reasons.
let exposure_vector = shutter_angle * this_motion_vector;

var accumulator: vec4<f32>;
var weight_total = 0.0;
let n_samples = i32(settings.samples);
let noise = utils::interleaved_gradient_noise(vec2<f32>(frag_coords), globals.frame_count); // 0 to 1

for (var i = -n_samples; i < n_samples; i++) {
// The current sample step vector, from in.uv
let step_vector = 0.5 * exposure_vector * (f32(i) + noise) / f32(n_samples);
var sample_uv = in.uv + step_vector;
let sample_coords = vec2<i32>(sample_uv * texture_size);

#ifdef MULTISAMPLED
let sample_color = textureLoad(screen_texture, sample_coords, i32(sample_index));
#else
let sample_color = textureSample(screen_texture, texture_sampler, sample_uv);
#endif
#ifdef MULTISAMPLED
let sample_motion = textureLoad(motion_vectors, sample_coords, i32(sample_index)).rg;
#else
let sample_motion = textureSample(motion_vectors, texture_sampler, sample_uv).rg;
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
#endif
#ifdef NO_DEPTH_TEXTURE_SUPPORT
let sample_depth = 0.0;
#else
#ifdef MULTISAMPLED
let sample_depth = textureLoad(depth, sample_coords, i32(sample_index));
#else
let sample_depth = textureSample(depth, texture_sampler, sample_uv);
#endif
#endif

var weight = 1.0;
let is_sample_in_fg = !(depth_supported && sample_depth < this_depth && sample_depth > 0.0);
if is_sample_in_fg {
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
// The following weight calculation is used to eliminate ghosting artifacts that are
// common in motion-vector-based motion blur implementations. While some resources
// recommend using depth, I've found that sampling the velocity results in significantly
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
// better results. Unlike a depth heuristic, this is not scale dependent.
//
// The most distracting artifacts occur when a stationary foreground object is
// incorrectly sampled while blurring a moving background object, causing the stationary
// object to blur when it should be sharp ("background bleeding"). This is most obvious
// when the camera is tracking a fast moving object. The tracked object should be sharp,
// and should not bleed into the motion blurred background.
//
// To attenuate these incorrect samples, we compare the motion of the fragment being
// blurred to the UV being sampled, to answer the question "is it possible that this
// sample was occluding the fragment?"
//
// Note to future maintainers: proceed with caution when making any changes here, and
// ensure you check all occlusion/disocclusion scenarios and fullscreen camera rotation
// blur for regressions.
let frag_speed = length(step_vector);
let sample_speed = length(sample_motion) / 2.0; // Halved because the sample is centered
let cos_angle = dot(step_vector, sample_motion) / (frag_speed * sample_speed * 2.0);
let motion_similarity = clamp(abs(cos_angle), 0.0, 1.0);
if sample_speed * motion_similarity < frag_speed {
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
// Project the sample's motion onto the frag's motion vector. If the sample did not
// cover enough distance to reach the original frag, there is no way it could have
// influenced this frag at all, and should be discarded.
weight = 0.0;
}
}
weight_total += weight;
accumulator += weight * sample_color;
}

let has_moved_less_than_a_pixel =
dot(this_motion_vector * texture_size, this_motion_vector * texture_size) < 1.0;
// In case no samples were accepted, fall back to base color.
// We also fall back if motion is small, to not break antialiasing.
if weight_total <= 0.0 || has_moved_less_than_a_pixel {
accumulator = base_color;
weight_total = 1.0;
}
return accumulator / weight_total;
}
aevyrie marked this conversation as resolved.
Show resolved Hide resolved