Skip to main content

bevy_post_process/motion_blur/
mod.rs

1//! Per-object, per-pixel motion blur.
2//!
3//! Add the [`MotionBlur`] component to a camera to enable motion blur.
4
5use crate::{
6    bloom::bloom,
7    motion_blur::pipeline::{MotionBlurPipeline, MotionBlurPipelineId},
8};
9use bevy_app::{App, Plugin};
10use bevy_asset::embedded_asset;
11use bevy_camera::{Camera, Camera3d};
12use bevy_core_pipeline::{
13    prepass::{MotionVectorPrepass, ViewPrepassTextures},
14    schedule::{Core3d, Core3dSystems},
15};
16use bevy_ecs::{
17    component::Component,
18    query::{QueryItem, With},
19    reflect::ReflectComponent,
20    schedule::IntoScheduleConfigs,
21    system::{Query, Res},
22};
23use bevy_reflect::{std_traits::ReflectDefault, Reflect};
24use bevy_render::{
25    diagnostic::RecordDiagnostics,
26    extract_component::{
27        ComponentUniforms, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin,
28    },
29    globals::GlobalsBuffer,
30    render_resource::{
31        BindGroupEntries, LoadOp, Operations, PipelineCache, RenderPassColorAttachment,
32        RenderPassDescriptor, ShaderType, SpecializedRenderPipelines, StoreOp, TextureUsages,
33    },
34    renderer::{RenderContext, ViewQuery},
35    sync_component::SyncComponent,
36    view::{prepare_view_targets, Msaa, ViewDepthTexture, ViewTarget},
37    GpuResourceAppExt, Render, RenderApp, RenderStartup, RenderSystems,
38};
39
40pub mod pipeline;
41
42/// A component that enables and configures motion blur when added to a camera.
43///
44/// Motion blur is an effect that simulates how moving objects blur as they change position during
45/// the exposure of film, a sensor, or an eyeball.
46///
47/// Because rendering simulates discrete steps in time, we use per-pixel motion vectors to estimate
48/// the path of objects between frames. This kind of implementation has some artifacts:
49/// - Fast moving objects in front of a stationary object or when in front of empty space, will not
50///   have their edges blurred.
51/// - Transparent objects do not write to depth or motion vectors, so they cannot be blurred.
52///
53/// Other approaches, such as *A Reconstruction Filter for Plausible Motion Blur* produce more
54/// correct results, but are more expensive and complex, and have other kinds of artifacts. This
55/// implementation is relatively inexpensive and effective.
56///
57/// # Usage
58///
59/// Add the [`MotionBlur`] component to a camera to enable and configure motion blur for that
60/// camera.
61///
62/// ```
63/// # use bevy_post_process::motion_blur::MotionBlur;
64/// # use bevy_camera::Camera3d;
65/// # use bevy_ecs::prelude::*;
66/// # fn test(mut commands: Commands) {
67/// commands.spawn((
68///     Camera3d::default(),
69///     MotionBlur::default(),
70/// ));
71/// # }
72/// ````
73#[derive(Reflect, Component, Clone)]
74#[reflect(Component, Default, Clone)]
75#[require(MotionVectorPrepass)]
76pub struct MotionBlur {
77    /// The strength of motion blur from `0.0` to `1.0`.
78    ///
79    /// The shutter angle describes the fraction of a frame that a camera's shutter is open and
80    /// exposing the film/sensor. For 24fps cinematic film, a shutter angle of 0.5 (180 degrees) is
81    /// common. This means that the shutter was open for half of the frame, or 1/48th of a second.
82    /// The lower the shutter angle, the less exposure time and thus less blur.
83    ///
84    /// A value greater than one is non-physical and results in an object's blur stretching further
85    /// than it traveled in that frame. This might be a desirable effect for artistic reasons, but
86    /// consider allowing users to opt out of this.
87    ///
88    /// This value is intentionally tied to framerate to avoid the aforementioned non-physical
89    /// over-blurring. If you want to emulate a cinematic look, your options are:
90    ///   - Framelimit your app to 24fps, and set the shutter angle to 0.5 (180 deg). Note that
91    ///     depending on artistic intent or the action of a scene, it is common to set the shutter
92    ///     angle between 0.125 (45 deg) and 0.5 (180 deg). This is the most faithful way to
93    ///     reproduce the look of film.
94    ///   - Set the shutter angle greater than one. For example, to emulate the blur strength of
95    ///     film while rendering at 60fps, you would set the shutter angle to `60/24 * 0.5 = 1.25`.
96    ///     Note that this will result in artifacts where the motion of objects will stretch further
97    ///     than they moved between frames; users may find this distracting.
98    pub shutter_angle: f32,
99    /// The quality of motion blur, corresponding to the number of per-pixel samples taken in each
100    /// direction during blur.
101    ///
102    /// Setting this to `1` results in each pixel being sampled once in the leading direction, once
103    /// in the trailing direction, and once in the middle, for a total of 3 samples (`1 * 2 + 1`).
104    /// Setting this to `3` will result in `3 * 2 + 1 = 7` samples. Setting this to `0` is
105    /// equivalent to disabling motion blur.
106    pub samples: u32,
107}
108
109impl Default for MotionBlur {
110    fn default() -> Self {
111        Self {
112            shutter_angle: 0.5,
113            samples: 1,
114        }
115    }
116}
117
118impl SyncComponent for MotionBlur {
119    type Target = MotionBlurUniform;
120}
121
122impl ExtractComponent for MotionBlur {
123    type QueryData = &'static Self;
124    type QueryFilter = With<Camera>;
125    type Out = MotionBlurUniform;
126
127    fn extract_component(item: QueryItem<Self::QueryData>) -> Option<Self::Out> {
128        Some(MotionBlurUniform {
129            shutter_angle: item.shutter_angle,
130            samples: item.samples,
131            #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
132            _webgl2_padding: Default::default(),
133        })
134    }
135}
136
137#[doc(hidden)]
138#[derive(Component, ShaderType, Clone)]
139pub struct MotionBlurUniform {
140    shutter_angle: f32,
141    samples: u32,
142    #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
143    // WebGL2 structs must be 16 byte aligned.
144    _webgl2_padding: bevy_math::Vec2,
145}
146
147/// Adds support for per-object motion blur to the app. See [`MotionBlur`] for details.
148#[derive(Default)]
149pub struct MotionBlurPlugin;
150impl Plugin for MotionBlurPlugin {
151    fn build(&self, app: &mut App) {
152        embedded_asset!(app, "motion_blur.wgsl");
153
154        app.add_plugins((
155            ExtractComponentPlugin::<MotionBlur>::default(),
156            UniformComponentPlugin::<MotionBlurUniform>::default(),
157        ));
158
159        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
160            return;
161        };
162
163        render_app
164            .init_gpu_resource::<SpecializedRenderPipelines<MotionBlurPipeline>>()
165            .add_systems(RenderStartup, pipeline::init_motion_blur_pipeline)
166            .add_systems(
167                Render,
168                (
169                    pipeline::prepare_motion_blur_pipelines.in_set(RenderSystems::Prepare),
170                    prepare_view_depth_texture_usages_for_motion_blur
171                        .in_set(RenderSystems::PrepareViews)
172                        .after(prepare_view_targets)
173                        .ambiguous_with_all(),
174                ),
175            );
176
177        render_app.add_systems(
178            Core3d,
179            motion_blur.before(bloom).in_set(Core3dSystems::PostProcess),
180        );
181    }
182}
183
184pub fn motion_blur(
185    view: ViewQuery<(
186        &ViewTarget,
187        &MotionBlurPipelineId,
188        &ViewPrepassTextures,
189        &ViewDepthTexture,
190        &MotionBlurUniform,
191        &Msaa,
192    )>,
193    motion_blur_pipeline: Res<MotionBlurPipeline>,
194    pipeline_cache: Res<PipelineCache>,
195    settings_uniforms: Res<ComponentUniforms<MotionBlurUniform>>,
196    globals_buffer: Res<GlobalsBuffer>,
197    mut ctx: RenderContext,
198) {
199    let (view_target, pipeline_id, prepass_textures, depth, motion_blur_uniform, msaa) =
200        view.into_inner();
201
202    if motion_blur_uniform.samples == 0 || motion_blur_uniform.shutter_angle <= 0.0 {
203        return; // We can skip running motion blur in these cases.
204    }
205
206    let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline_id.0) else {
207        return;
208    };
209
210    let Some(settings_binding) = settings_uniforms.uniforms().binding() else {
211        return;
212    };
213    let Some(prepass_motion_vectors_texture) = &prepass_textures.motion_vectors else {
214        return;
215    };
216    let Some(globals_uniforms) = globals_buffer.buffer.binding() else {
217        return;
218    };
219
220    let post_process = view_target.post_process_write();
221
222    let layout = if msaa.samples() == 1 {
223        &motion_blur_pipeline.layout
224    } else {
225        &motion_blur_pipeline.layout_msaa
226    };
227
228    let bind_group = ctx.render_device().create_bind_group(
229        Some("motion_blur_bind_group"),
230        &pipeline_cache.get_bind_group_layout(layout),
231        &BindGroupEntries::sequential((
232            post_process.source,
233            &prepass_motion_vectors_texture.texture.default_view,
234            depth.view(),
235            &motion_blur_pipeline.sampler,
236            settings_binding.clone(),
237            globals_uniforms.clone(),
238        )),
239    );
240
241    let diagnostics = ctx.diagnostic_recorder();
242    let diagnostics = diagnostics.as_deref();
243
244    let mut render_pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
245        label: Some("motion_blur"),
246        color_attachments: &[Some(RenderPassColorAttachment {
247            view: post_process.destination,
248            depth_slice: None,
249            resolve_target: None,
250            ops: Operations {
251                load: LoadOp::Clear(Default::default()),
252                store: StoreOp::Store,
253            },
254        })],
255        depth_stencil_attachment: None,
256        timestamp_writes: None,
257        occlusion_query_set: None,
258        multiview_mask: None,
259    });
260    let pass_span = diagnostics.pass_span(&mut render_pass, "motion_blur");
261
262    render_pass.set_render_pipeline(pipeline);
263    render_pass.set_bind_group(0, &bind_group, &[]);
264    render_pass.draw(0..3, 0..1);
265
266    pass_span.end(&mut render_pass);
267}
268
269fn prepare_view_depth_texture_usages_for_motion_blur(
270    mut view_targets: Query<&mut Camera3d, With<MotionBlurUniform>>,
271) {
272    for mut camera in view_targets.iter_mut() {
273        camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits();
274    }
275}