motion_blur/
motion_blur.rs

1//! Demonstrates how to enable per-object motion blur. This rendering feature can be configured per
2//! camera using the [`MotionBlur`] component.z
3
4use bevy::{
5    image::{ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor},
6    math::ops,
7    post_process::motion_blur::MotionBlur,
8    prelude::*,
9};
10
11fn main() {
12    let mut app = App::new();
13
14    app.add_plugins(DefaultPlugins)
15        .add_systems(Startup, (setup_camera, setup_scene, setup_ui))
16        .add_systems(Update, (keyboard_inputs, move_cars, move_camera).chain())
17        .run();
18}
19
20fn setup_camera(mut commands: Commands) {
21    commands.spawn((
22        Camera3d::default(),
23        // Add the `MotionBlur` component to a camera to enable motion blur.
24        // Motion blur requires the depth and motion vector prepass, which this bundle adds.
25        // Configure the amount and quality of motion blur per-camera using this component.
26        MotionBlur {
27            shutter_angle: 1.0,
28            samples: 2,
29        },
30        // MSAA and Motion Blur together are not compatible on WebGL
31        #[cfg(all(feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu")))]
32        Msaa::Off,
33    ));
34}
35
36// Everything past this point is used to build the example, but isn't required to use motion blur.
37
38#[derive(Resource)]
39enum CameraMode {
40    Track,
41    Chase,
42}
43
44#[derive(Component)]
45struct Moves(f32);
46
47#[derive(Component)]
48struct CameraTracked;
49
50#[derive(Component)]
51struct Rotates;
52
53fn setup_scene(
54    asset_server: Res<AssetServer>,
55    mut images: ResMut<Assets<Image>>,
56    mut commands: Commands,
57    mut meshes: ResMut<Assets<Mesh>>,
58    mut materials: ResMut<Assets<StandardMaterial>>,
59) {
60    commands.insert_resource(AmbientLight {
61        color: Color::WHITE,
62        brightness: 300.0,
63        ..default()
64    });
65    commands.insert_resource(CameraMode::Chase);
66    commands.spawn((
67        DirectionalLight {
68            illuminance: 3_000.0,
69            shadows_enabled: true,
70            ..default()
71        },
72        Transform::default().looking_to(Vec3::new(-1.0, -0.7, -1.0), Vec3::X),
73    ));
74    // Sky
75    commands.spawn((
76        Mesh3d(meshes.add(Sphere::default())),
77        MeshMaterial3d(materials.add(StandardMaterial {
78            unlit: true,
79            base_color: Color::linear_rgb(0.1, 0.6, 1.0),
80            ..default()
81        })),
82        Transform::default().with_scale(Vec3::splat(-4000.0)),
83    ));
84    // Ground
85    let mut plane: Mesh = Plane3d::default().into();
86    let uv_size = 4000.0;
87    let uvs = vec![[uv_size, 0.0], [0.0, 0.0], [0.0, uv_size], [uv_size; 2]];
88    plane.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
89    commands.spawn((
90        Mesh3d(meshes.add(plane)),
91        MeshMaterial3d(materials.add(StandardMaterial {
92            base_color: Color::WHITE,
93            perceptual_roughness: 1.0,
94            base_color_texture: Some(images.add(uv_debug_texture())),
95            ..default()
96        })),
97        Transform::from_xyz(0.0, -0.65, 0.0).with_scale(Vec3::splat(80.)),
98    ));
99
100    spawn_cars(&asset_server, &mut meshes, &mut materials, &mut commands);
101    spawn_trees(&mut meshes, &mut materials, &mut commands);
102    spawn_barriers(&mut meshes, &mut materials, &mut commands);
103}
104
105fn spawn_cars(
106    asset_server: &AssetServer,
107    meshes: &mut Assets<Mesh>,
108    materials: &mut Assets<StandardMaterial>,
109    commands: &mut Commands,
110) {
111    const N_CARS: usize = 20;
112    let box_mesh = meshes.add(Cuboid::new(0.3, 0.15, 0.55));
113    let cylinder = meshes.add(Cylinder::default());
114    let logo = asset_server.load("branding/icon.png");
115    let wheel_matl = materials.add(StandardMaterial {
116        base_color: Color::WHITE,
117        base_color_texture: Some(logo.clone()),
118        ..default()
119    });
120
121    let mut matl = |color| {
122        materials.add(StandardMaterial {
123            base_color: color,
124            ..default()
125        })
126    };
127
128    let colors = [
129        matl(Color::linear_rgb(1.0, 0.0, 0.0)),
130        matl(Color::linear_rgb(1.0, 1.0, 0.0)),
131        matl(Color::BLACK),
132        matl(Color::linear_rgb(0.0, 0.0, 1.0)),
133        matl(Color::linear_rgb(0.0, 1.0, 0.0)),
134        matl(Color::linear_rgb(1.0, 0.0, 1.0)),
135        matl(Color::linear_rgb(0.5, 0.5, 0.0)),
136        matl(Color::linear_rgb(1.0, 0.5, 0.0)),
137    ];
138
139    let make_wheel = |x: f32, z: f32| {
140        (
141            Mesh3d(cylinder.clone()),
142            MeshMaterial3d(wheel_matl.clone()),
143            Transform::from_xyz(0.14 * x, -0.045, 0.15 * z)
144                .with_scale(Vec3::new(0.15, 0.04, 0.15))
145                .with_rotation(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2)),
146            Rotates,
147        )
148    };
149
150    for i in 0..N_CARS {
151        let color = colors[i % colors.len()].clone();
152        commands
153            .spawn((
154                Mesh3d(box_mesh.clone()),
155                MeshMaterial3d(color.clone()),
156                Transform::from_scale(Vec3::splat(0.5)),
157                Moves(i as f32 * 2.0),
158                children![
159                    (
160                        Mesh3d(box_mesh.clone()),
161                        MeshMaterial3d(color),
162                        Transform::from_xyz(0.0, 0.08, 0.03).with_scale(Vec3::new(1.0, 1.0, 0.5)),
163                    ),
164                    make_wheel(1.0, 1.0),
165                    make_wheel(1.0, -1.0),
166                    make_wheel(-1.0, 1.0),
167                    make_wheel(-1.0, -1.0)
168                ],
169            ))
170            .insert_if(CameraTracked, || i == 0);
171    }
172}
173
174fn spawn_barriers(
175    meshes: &mut Assets<Mesh>,
176    materials: &mut Assets<StandardMaterial>,
177    commands: &mut Commands,
178) {
179    const N_CONES: usize = 100;
180    let capsule = meshes.add(Capsule3d::default());
181    let matl = materials.add(StandardMaterial {
182        base_color: Color::srgb_u8(255, 87, 51),
183        reflectance: 1.0,
184        ..default()
185    });
186    let mut spawn_with_offset = |offset: f32| {
187        for i in 0..N_CONES {
188            let pos = race_track_pos(
189                offset,
190                (i as f32) / (N_CONES as f32) * std::f32::consts::PI * 2.0,
191            );
192            commands.spawn((
193                Mesh3d(capsule.clone()),
194                MeshMaterial3d(matl.clone()),
195                Transform::from_xyz(pos.x, -0.65, pos.y).with_scale(Vec3::splat(0.07)),
196            ));
197        }
198    };
199    spawn_with_offset(0.04);
200    spawn_with_offset(-0.04);
201}
202
203fn spawn_trees(
204    meshes: &mut Assets<Mesh>,
205    materials: &mut Assets<StandardMaterial>,
206    commands: &mut Commands,
207) {
208    const N_TREES: usize = 30;
209    let capsule = meshes.add(Capsule3d::default());
210    let sphere = meshes.add(Sphere::default());
211    let leaves = materials.add(Color::linear_rgb(0.0, 1.0, 0.0));
212    let trunk = materials.add(Color::linear_rgb(0.4, 0.2, 0.2));
213
214    let mut spawn_with_offset = |offset: f32| {
215        for i in 0..N_TREES {
216            let pos = race_track_pos(
217                offset,
218                (i as f32) / (N_TREES as f32) * std::f32::consts::PI * 2.0,
219            );
220            let [x, z] = pos.into();
221            commands.spawn((
222                Mesh3d(sphere.clone()),
223                MeshMaterial3d(leaves.clone()),
224                Transform::from_xyz(x, -0.3, z).with_scale(Vec3::splat(0.3)),
225            ));
226            commands.spawn((
227                Mesh3d(capsule.clone()),
228                MeshMaterial3d(trunk.clone()),
229                Transform::from_xyz(x, -0.5, z).with_scale(Vec3::new(0.05, 0.3, 0.05)),
230            ));
231        }
232    };
233    spawn_with_offset(0.07);
234    spawn_with_offset(-0.07);
235}
236
237fn setup_ui(mut commands: Commands) {
238    commands.spawn((
239        Text::default(),
240        Node {
241            position_type: PositionType::Absolute,
242            top: px(12),
243            left: px(12),
244            ..default()
245        },
246        children![
247            TextSpan::default(),
248            TextSpan::default(),
249            TextSpan::new("1/2: -/+ shutter angle (blur amount)\n"),
250            TextSpan::new("3/4: -/+ sample count (blur quality)\n"),
251            TextSpan::new("Spacebar: cycle camera\n"),
252        ],
253    ));
254}
255
256fn keyboard_inputs(
257    mut motion_blur: Single<&mut MotionBlur>,
258    presses: Res<ButtonInput<KeyCode>>,
259    text: Single<Entity, With<Text>>,
260    mut writer: TextUiWriter,
261    mut camera: ResMut<CameraMode>,
262) {
263    if presses.just_pressed(KeyCode::Digit1) {
264        motion_blur.shutter_angle -= 0.25;
265    } else if presses.just_pressed(KeyCode::Digit2) {
266        motion_blur.shutter_angle += 0.25;
267    } else if presses.just_pressed(KeyCode::Digit3) {
268        motion_blur.samples = motion_blur.samples.saturating_sub(1);
269    } else if presses.just_pressed(KeyCode::Digit4) {
270        motion_blur.samples += 1;
271    } else if presses.just_pressed(KeyCode::Space) {
272        *camera = match *camera {
273            CameraMode::Track => CameraMode::Chase,
274            CameraMode::Chase => CameraMode::Track,
275        };
276    }
277    motion_blur.shutter_angle = motion_blur.shutter_angle.clamp(0.0, 1.0);
278    motion_blur.samples = motion_blur.samples.clamp(0, 64);
279    let entity = *text;
280    *writer.text(entity, 1) = format!("Shutter angle: {:.2}\n", motion_blur.shutter_angle);
281    *writer.text(entity, 2) = format!("Samples: {:.5}\n", motion_blur.samples);
282}
283
284/// Parametric function for a looping race track. `offset` will return the point offset
285/// perpendicular to the track at the given point.
286fn race_track_pos(offset: f32, t: f32) -> Vec2 {
287    let x_tweak = 2.0;
288    let y_tweak = 3.0;
289    let scale = 8.0;
290    let x0 = ops::sin(x_tweak * t);
291    let y0 = ops::cos(y_tweak * t);
292    let dx = x_tweak * ops::cos(x_tweak * t);
293    let dy = y_tweak * -ops::sin(y_tweak * t);
294    let dl = ops::hypot(dx, dy);
295    let x = x0 + offset * dy / dl;
296    let y = y0 - offset * dx / dl;
297    Vec2::new(x, y) * scale
298}
299
300fn move_cars(
301    time: Res<Time>,
302    mut movables: Query<(&mut Transform, &Moves, &Children)>,
303    mut spins: Query<&mut Transform, (Without<Moves>, With<Rotates>)>,
304) {
305    for (mut transform, moves, children) in &mut movables {
306        let time = time.elapsed_secs() * 0.25;
307        let t = time + 0.5 * moves.0;
308        let dx = ops::cos(t);
309        let dz = -ops::sin(3.0 * t);
310        let speed_variation = (dx * dx + dz * dz).sqrt() * 0.15;
311        let t = t + speed_variation;
312        let prev = transform.translation;
313        transform.translation.x = race_track_pos(0.0, t).x;
314        transform.translation.z = race_track_pos(0.0, t).y;
315        transform.translation.y = -0.59;
316        let delta = transform.translation - prev;
317        transform.look_to(delta, Vec3::Y);
318        for child in children.iter() {
319            let Ok(mut wheel) = spins.get_mut(child) else {
320                continue;
321            };
322            let radius = wheel.scale.x;
323            let circumference = 2.0 * std::f32::consts::PI * radius;
324            let angle = delta.length() / circumference * std::f32::consts::PI * 2.0;
325            wheel.rotate_local_y(angle);
326        }
327    }
328}
329
330fn move_camera(
331    camera: Single<(&mut Transform, &mut Projection), Without<CameraTracked>>,
332    tracked: Single<&Transform, With<CameraTracked>>,
333    mode: Res<CameraMode>,
334) {
335    let (mut transform, mut projection) = camera.into_inner();
336    match *mode {
337        CameraMode::Track => {
338            transform.look_at(tracked.translation, Vec3::Y);
339            transform.translation = Vec3::new(15.0, -0.5, 0.0);
340            if let Projection::Perspective(perspective) = &mut *projection {
341                perspective.fov = 0.05;
342            }
343        }
344        CameraMode::Chase => {
345            transform.translation =
346                tracked.translation + Vec3::new(0.0, 0.15, 0.0) + tracked.back() * 0.6;
347            transform.look_to(tracked.forward(), Vec3::Y);
348            if let Projection::Perspective(perspective) = &mut *projection {
349                perspective.fov = 1.0;
350            }
351        }
352    }
353}
354
355fn uv_debug_texture() -> Image {
356    use bevy::{asset::RenderAssetUsages, render::render_resource::*};
357    const TEXTURE_SIZE: usize = 7;
358
359    let mut palette = [
360        164, 164, 164, 255, 168, 168, 168, 255, 153, 153, 153, 255, 139, 139, 139, 255, 153, 153,
361        153, 255, 177, 177, 177, 255, 159, 159, 159, 255,
362    ];
363
364    let mut texture_data = [0; TEXTURE_SIZE * TEXTURE_SIZE * 4];
365    for y in 0..TEXTURE_SIZE {
366        let offset = TEXTURE_SIZE * y * 4;
367        texture_data[offset..(offset + TEXTURE_SIZE * 4)].copy_from_slice(&palette);
368        palette.rotate_right(12);
369    }
370
371    let mut img = Image::new_fill(
372        Extent3d {
373            width: TEXTURE_SIZE as u32,
374            height: TEXTURE_SIZE as u32,
375            depth_or_array_layers: 1,
376        },
377        TextureDimension::D2,
378        &texture_data,
379        TextureFormat::Rgba8UnormSrgb,
380        RenderAssetUsages::RENDER_WORLD,
381    );
382    img.sampler = ImageSampler::Descriptor(ImageSamplerDescriptor {
383        address_mode_u: ImageAddressMode::Repeat,
384        address_mode_v: ImageAddressMode::MirrorRepeat,
385        mag_filter: ImageFilterMode::Nearest,
386        ..ImageSamplerDescriptor::linear()
387    });
388    img
389}