1use 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 MotionBlur {
27 shutter_angle: 1.0,
28 samples: 2,
29 },
30 #[cfg(all(feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu")))]
32 Msaa::Off,
33 ));
34}
35
36#[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 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 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
284fn 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}