docs/bevy/examples/3d/irradiance_volumes.rs 19.9 K · 637 lines · raw · history

1 //! This example shows how irradiance volumes affect the indirect lighting of
2 //! objects in a scene.
3 //!
4 //! The controls are as follows:
5 //!
6 //! * Space toggles the irradiance volume on and off.
7 //!
8 //! * Enter toggles the camera rotation on and off.
9 //!
10 //! * Tab switches the object between a plain sphere and a running fox.
11 //!
12 //! * Backspace shows and hides the voxel cubes.
13 //!
14 //! * Clicking anywhere moves the object.
15
16 use bevy::{
17 color::palettes::css::*,
18 light::Skybox,
19 light::{IrradianceVolume, NotShadowCaster},
20 math::{uvec3, vec3},
21 pbr::{ExtendedMaterial, MaterialExtension},
22 prelude::*,
23 render::render_resource::{AsBindGroup, ShaderType},
24 shader::ShaderRef,
25 window::PrimaryWindow,
26 };
27
28 /// This example uses a shader source file from the assets subdirectory
29 const SHADER_ASSET_PATH: &str = "shaders/irradiance_volume_voxel_visualization.wesl";
30
31 // Rotation speed in radians per frame.
32 const ROTATION_SPEED: f32 = 0.2;
33
34 const FOX_SCALE: f32 = 0.05;
35 const SPHERE_SCALE: f32 = 2.0;
36
37 const IRRADIANCE_VOLUME_INTENSITY: f32 = 1800.0;
38
39 const AMBIENT_LIGHT_BRIGHTNESS: f32 = 0.06;
40
41 const VOXEL_CUBE_SCALE: f32 = 0.4;
42
43 static DISABLE_IRRADIANCE_VOLUME_HELP_TEXT: &str = "Space: Disable the irradiance volume";
44 static ENABLE_IRRADIANCE_VOLUME_HELP_TEXT: &str = "Space: Enable the irradiance volume";
45
46 static HIDE_VOXELS_HELP_TEXT: &str = "Backspace: Hide the voxels";
47 static SHOW_VOXELS_HELP_TEXT: &str = "Backspace: Show the voxels";
48
49 static STOP_ROTATION_HELP_TEXT: &str = "Enter: Stop rotation";
50 static START_ROTATION_HELP_TEXT: &str = "Enter: Start rotation";
51
52 static SWITCH_TO_FOX_HELP_TEXT: &str = "Tab: Switch to a skinned mesh";
53 static SWITCH_TO_SPHERE_HELP_TEXT: &str = "Tab: Switch to a plain sphere mesh";
54
55 static CLICK_TO_MOVE_HELP_TEXT: &str = "Left click: Move the object";
56
57 static GIZMO_COLOR: Color = Color::Srgba(YELLOW);
58
59 static VOXEL_FROM_WORLD: Mat4 = Mat4::from_cols_array_2d(&[
60 [-42.317566, 0.0, 0.0, 0.0],
61 [0.0, 0.0, 44.601563, 0.0],
62 [0.0, 16.73776, 0.0, 0.0],
63 [0.0, 6.544792, 0.0, 1.0],
64 ]);
65
66 // The mode the application is in.
67 #[derive(Resource)]
68 struct AppStatus {
69 // Whether the user wants the irradiance volume to be applied.
70 irradiance_volume_present: bool,
71 // Whether the user wants the unskinned sphere mesh or the skinned fox mesh.
72 model: ExampleModel,
73 // Whether the user has requested the scene to rotate.
74 rotating: bool,
75 // Whether the user has requested the voxels to be displayed.
76 voxels_visible: bool,
77 }
78
79 // Which model the user wants to display.
80 #[derive(Clone, Copy, PartialEq)]
81 enum ExampleModel {
82 // The plain sphere.
83 Sphere,
84 // The fox, which is skinned.
85 Fox,
86 }
87
88 // Handles to all the assets used in this example.
89 #[derive(Resource)]
90 struct ExampleAssets {
91 // The glTF scene containing the colored floor.
92 main_scene: Handle<WorldAsset>,
93
94 // The 3D texture containing the irradiance volume.
95 irradiance_volume: Handle<Image>,
96
97 // The plain sphere mesh.
98 main_sphere: Handle<Mesh>,
99
100 // The material used for the sphere.
101 main_sphere_material: Handle<StandardMaterial>,
102
103 // The glTF scene containing the animated fox.
104 fox: Handle<WorldAsset>,
105
106 // The graph containing the animation that the fox will play.
107 fox_animation_graph: Handle<AnimationGraph>,
108
109 // The node within the animation graph containing the animation.
110 fox_animation_node: AnimationNodeIndex,
111
112 // The voxel cube mesh.
113 voxel_cube: Handle<Mesh>,
114
115 // The skybox.
116 skybox: Handle<Image>,
117 }
118
119 // The sphere and fox both have this component.
120 #[derive(Component)]
121 struct MainObject;
122
123 // Marks each of the voxel cubes.
124 #[derive(Component)]
125 struct VoxelCube;
126
127 // Marks the voxel cube parent object.
128 #[derive(Component)]
129 struct VoxelCubeParent;
130
131 type VoxelVisualizationMaterial = ExtendedMaterial<StandardMaterial, VoxelVisualizationExtension>;
132
133 #[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
134 struct VoxelVisualizationExtension {
135 #[uniform(100)]
136 irradiance_volume_info: VoxelVisualizationIrradianceVolumeInfo,
137 }
138
139 #[derive(ShaderType, Debug, Clone)]
140 struct VoxelVisualizationIrradianceVolumeInfo {
141 world_from_voxel: Mat4,
142 voxel_from_world: Mat4,
143 resolution: UVec3,
144 intensity: f32,
145 }
146
147 fn main() {
148 // Create the example app.
149 App::new()
150 .add_plugins(DefaultPlugins.set(WindowPlugin {
151 primary_window: Some(Window {
152 title: "Bevy Irradiance Volumes Example".into(),
153 ..default()
154 }),
155 ..default()
156 }))
157 .add_plugins(MaterialPlugin::<VoxelVisualizationMaterial>::default())
158 .init_resource::<AppStatus>()
159 .init_resource::<ExampleAssets>()
160 .insert_resource(GlobalAmbientLight {
161 color: Color::WHITE,
162 brightness: 0.0,
163 ..default()
164 })
165 .add_systems(Startup, setup)
166 .add_systems(PreUpdate, create_cubes)
167 .add_systems(Update, rotate_camera)
168 .add_systems(Update, play_animations)
169 .add_systems(
170 Update,
171 handle_mouse_clicks
172 .after(rotate_camera)
173 .after(play_animations),
174 )
175 .add_systems(
176 Update,
177 change_main_object
178 .after(rotate_camera)
179 .after(play_animations),
180 )
181 .add_systems(
182 Update,
183 toggle_irradiance_volumes
184 .after(rotate_camera)
185 .after(play_animations),
186 )
187 .add_systems(
188 Update,
189 toggle_voxel_visibility
190 .after(rotate_camera)
191 .after(play_animations),
192 )
193 .add_systems(
194 Update,
195 toggle_rotation.after(rotate_camera).after(play_animations),
196 )
197 .add_systems(
198 Update,
199 draw_gizmo
200 .after(handle_mouse_clicks)
201 .after(change_main_object)
202 .after(toggle_irradiance_volumes)
203 .after(toggle_voxel_visibility)
204 .after(toggle_rotation),
205 )
206 .add_systems(
207 Update,
208 update_text
209 .after(handle_mouse_clicks)
210 .after(change_main_object)
211 .after(toggle_irradiance_volumes)
212 .after(toggle_voxel_visibility)
213 .after(toggle_rotation),
214 )
215 .run();
216 }
217
218 // Spawns all the scene objects.
219 fn setup(mut commands: Commands, assets: Res<ExampleAssets>, app_status: Res<AppStatus>) {
220 spawn_main_scene(&mut commands, &assets);
221 spawn_camera(&mut commands, &assets);
222 spawn_irradiance_volume(&mut commands, &assets);
223 spawn_light(&mut commands);
224 spawn_sphere(&mut commands, &assets);
225 spawn_voxel_cube_parent(&mut commands);
226 spawn_fox(&mut commands, &assets);
227 spawn_text(&mut commands, &app_status);
228 }
229
230 fn spawn_main_scene(commands: &mut Commands, assets: &ExampleAssets) {
231 commands.spawn(WorldAssetRoot(assets.main_scene.clone()));
232 }
233
234 fn spawn_camera(commands: &mut Commands, assets: &ExampleAssets) {
235 commands.spawn((
236 Camera3d::default(),
237 Transform::from_xyz(-10.012, 4.8605, 13.281).looking_at(Vec3::ZERO, Vec3::Y),
238 Skybox {
239 image: Some(assets.skybox.clone()),
240 brightness: 150.0,
241 ..default()
242 },
243 ));
244 }
245
246 fn spawn_irradiance_volume(commands: &mut Commands, assets: &ExampleAssets) {
247 commands.spawn((
248 Transform::from_matrix(VOXEL_FROM_WORLD),
249 IrradianceVolume {
250 voxels: assets.irradiance_volume.clone(),
251 intensity: IRRADIANCE_VOLUME_INTENSITY,
252 ..default()
253 },
254 ));
255 }
256
257 fn spawn_light(commands: &mut Commands) {
258 commands.spawn((
259 PointLight {
260 intensity: 250000.0,
261 shadow_maps_enabled: true,
262 ..default()
263 },
264 Transform::from_xyz(4.0762, 5.9039, 1.0055),
265 ));
266 }
267
268 fn spawn_sphere(commands: &mut Commands, assets: &ExampleAssets) {
269 commands
270 .spawn((
271 Mesh3d(assets.main_sphere.clone()),
272 MeshMaterial3d(assets.main_sphere_material.clone()),
273 Transform::from_xyz(0.0, SPHERE_SCALE, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
274 ))
275 .insert(MainObject);
276 }
277
278 fn spawn_voxel_cube_parent(commands: &mut Commands) {
279 commands.spawn((Visibility::Hidden, Transform::default(), VoxelCubeParent));
280 }
281
282 fn spawn_fox(commands: &mut Commands, assets: &ExampleAssets) {
283 commands.spawn((
284 WorldAssetRoot(assets.fox.clone()),
285 Visibility::Hidden,
286 Transform::from_scale(Vec3::splat(FOX_SCALE)),
287 MainObject,
288 ));
289 }
290
291 fn spawn_text(commands: &mut Commands, app_status: &AppStatus) {
292 commands.spawn((
293 app_status.create_text(),
294 Node {
295 position_type: PositionType::Absolute,
296 bottom: px(12),
297 left: px(12),
298 ..default()
299 },
300 ));
301 }
302
303 // A system that updates the help text.
304 fn update_text(mut text_query: Query<&mut Text>, app_status: Res<AppStatus>) {
305 for mut text in text_query.iter_mut() {
306 *text = app_status.create_text();
307 }
308 }
309
310 impl AppStatus {
311 // Constructs the help text at the bottom of the screen based on the
312 // application status.
313 fn create_text(&self) -> Text {
314 let irradiance_volume_help_text = if self.irradiance_volume_present {
315 DISABLE_IRRADIANCE_VOLUME_HELP_TEXT
316 } else {
317 ENABLE_IRRADIANCE_VOLUME_HELP_TEXT
318 };
319
320 let voxels_help_text = if self.voxels_visible {
321 HIDE_VOXELS_HELP_TEXT
322 } else {
323 SHOW_VOXELS_HELP_TEXT
324 };
325
326 let rotation_help_text = if self.rotating {
327 STOP_ROTATION_HELP_TEXT
328 } else {
329 START_ROTATION_HELP_TEXT
330 };
331
332 let switch_mesh_help_text = match self.model {
333 ExampleModel::Sphere => SWITCH_TO_FOX_HELP_TEXT,
334 ExampleModel::Fox => SWITCH_TO_SPHERE_HELP_TEXT,
335 };
336
337 format!(
338 "{CLICK_TO_MOVE_HELP_TEXT}\n\
339 {voxels_help_text}\n\
340 {irradiance_volume_help_text}\n\
341 {rotation_help_text}\n\
342 {switch_mesh_help_text}"
343 )
344 .into()
345 }
346 }
347
348 // Rotates the camera a bit every frame.
349 fn rotate_camera(
350 mut camera_query: Query<&mut Transform, With<Camera3d>>,
351 time: Res<Time>,
352 app_status: Res<AppStatus>,
353 ) {
354 if !app_status.rotating {
355 return;
356 }
357
358 for mut transform in camera_query.iter_mut() {
359 transform.translation = Vec2::from_angle(ROTATION_SPEED * time.delta_secs())
360 .rotate(transform.translation.xz())
361 .extend(transform.translation.y)
362 .xzy();
363 transform.look_at(Vec3::ZERO, Vec3::Y);
364 }
365 }
366
367 // Toggles between the unskinned sphere model and the skinned fox model if the
368 // user requests it.
369 fn change_main_object(
370 keyboard: Res<ButtonInput<KeyCode>>,
371 mut app_status: ResMut<AppStatus>,
372 mut sphere_query: Query<
373 &mut Visibility,
374 (With<MainObject>, With<Mesh3d>, Without<WorldAssetRoot>),
375 >,
376 mut fox_query: Query<&mut Visibility, (With<MainObject>, With<WorldAssetRoot>)>,
377 ) {
378 if !keyboard.just_pressed(KeyCode::Tab) {
379 return;
380 }
381 let Some(mut sphere_visibility) = sphere_query.iter_mut().next() else {
382 return;
383 };
384 let Some(mut fox_visibility) = fox_query.iter_mut().next() else {
385 return;
386 };
387
388 match app_status.model {
389 ExampleModel::Sphere => {
390 *sphere_visibility = Visibility::Hidden;
391 *fox_visibility = Visibility::Visible;
392 app_status.model = ExampleModel::Fox;
393 }
394 ExampleModel::Fox => {
395 *sphere_visibility = Visibility::Visible;
396 *fox_visibility = Visibility::Hidden;
397 app_status.model = ExampleModel::Sphere;
398 }
399 }
400 }
401
402 impl Default for AppStatus {
403 fn default() -> Self {
404 Self {
405 irradiance_volume_present: true,
406 rotating: true,
407 model: ExampleModel::Sphere,
408 voxels_visible: false,
409 }
410 }
411 }
412
413 // Turns on and off the irradiance volume as requested by the user.
414 fn toggle_irradiance_volumes(
415 mut commands: Commands,
416 keyboard: Res<ButtonInput<KeyCode>>,
417 light_probe_query: Query<Entity, With<LightProbe>>,
418 mut app_status: ResMut<AppStatus>,
419 assets: Res<ExampleAssets>,
420 mut ambient_light: ResMut<GlobalAmbientLight>,
421 ) {
422 if !keyboard.just_pressed(KeyCode::Space) {
423 return;
424 };
425
426 let Some(light_probe) = light_probe_query.iter().next() else {
427 return;
428 };
429
430 if app_status.irradiance_volume_present {
431 commands.entity(light_probe).remove::<IrradianceVolume>();
432 ambient_light.brightness = AMBIENT_LIGHT_BRIGHTNESS * IRRADIANCE_VOLUME_INTENSITY;
433 app_status.irradiance_volume_present = false;
434 } else {
435 commands.entity(light_probe).insert(IrradianceVolume {
436 voxels: assets.irradiance_volume.clone(),
437 intensity: IRRADIANCE_VOLUME_INTENSITY,
438 ..default()
439 });
440 ambient_light.brightness = 0.0;
441 app_status.irradiance_volume_present = true;
442 }
443 }
444
445 fn toggle_rotation(keyboard: Res<ButtonInput<KeyCode>>, mut app_status: ResMut<AppStatus>) {
446 if keyboard.just_pressed(KeyCode::Enter) {
447 app_status.rotating = !app_status.rotating;
448 }
449 }
450
451 // Handles clicks on the plane that reposition the object.
452 fn handle_mouse_clicks(
453 buttons: Res<ButtonInput<MouseButton>>,
454 windows: Query<&Window, With<PrimaryWindow>>,
455 cameras: Query<(&Camera, &GlobalTransform)>,
456 mut main_objects: Query<&mut Transform, With<MainObject>>,
457 ) {
458 if !buttons.pressed(MouseButton::Left) {
459 return;
460 }
461 let Some(mouse_position) = windows.iter().next().and_then(Window::cursor_position) else {
462 return;
463 };
464 let Some((camera, camera_transform)) = cameras.iter().next() else {
465 return;
466 };
467
468 // Figure out where the user clicked on the plane.
469 let Ok(ray) = camera.viewport_to_world(camera_transform, mouse_position) else {
470 return;
471 };
472 let Some(plane_intersection) =
473 ray.plane_intersection_point(Vec3::ZERO, InfinitePlane3d::new(Vec3::Y))
474 else {
475 return;
476 };
477 // Move all the main objects.
478 for mut transform in main_objects.iter_mut() {
479 transform.translation = vec3(
480 plane_intersection.x,
481 transform.translation.y,
482 plane_intersection.z,
483 );
484 }
485 }
486
487 impl FromWorld for ExampleAssets {
488 fn from_world(world: &mut World) -> Self {
489 let fox_animation =
490 world.load_asset(GltfAssetLabel::Animation(1).from_asset("models/animated/Fox.glb"));
491 let (fox_animation_graph, fox_animation_node) =
492 AnimationGraph::from_clip(fox_animation.clone());
493
494 ExampleAssets {
495 main_sphere: world.add_asset(Sphere::default().mesh().uv(32, 18)),
496 fox: world.load_asset(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")),
497 main_sphere_material: world.add_asset(Color::from(SILVER)),
498 main_scene: world.load_asset(
499 GltfAssetLabel::Scene(0)
500 .from_asset("models/IrradianceVolumeExample/IrradianceVolumeExample.glb"),
501 ),
502 irradiance_volume: world.load_asset("irradiance_volumes/Example.vxgi.ktx2"),
503 fox_animation_graph: world.add_asset(fox_animation_graph),
504 fox_animation_node,
505 voxel_cube: world.add_asset(Cuboid::default()),
506 // Just use a specular map for the skybox since it's not too blurry.
507 // In reality you wouldn't do this--you'd use a real skybox texture--but
508 // reusing the textures like this saves space in the Bevy repository.
509 skybox: world.load_asset("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
510 }
511 }
512 }
513
514 // Plays the animation on the fox.
515 fn play_animations(
516 mut commands: Commands,
517 assets: Res<ExampleAssets>,
518 mut players: Query<(Entity, &mut AnimationPlayer), Without<AnimationGraphHandle>>,
519 ) {
520 for (entity, mut player) in players.iter_mut() {
521 commands
522 .entity(entity)
523 .insert(AnimationGraphHandle(assets.fox_animation_graph.clone()));
524 player.play(assets.fox_animation_node).repeat();
525 }
526 }
527
528 fn create_cubes(
529 image_assets: Res<Assets<Image>>,
530 mut commands: Commands,
531 irradiance_volumes: Query<(&IrradianceVolume, &GlobalTransform)>,
532 voxel_cube_parents: Query<Entity, With<VoxelCubeParent>>,
533 voxel_cubes: Query<Entity, With<VoxelCube>>,
534 example_assets: Res<ExampleAssets>,
535 mut voxel_visualization_material_assets: ResMut<Assets<VoxelVisualizationMaterial>>,
536 ) {
537 // If voxel cubes have already been spawned, don't do anything.
538 if !voxel_cubes.is_empty() {
539 return;
540 }
541
542 let Some(voxel_cube_parent) = voxel_cube_parents.iter().next() else {
543 return;
544 };
545
546 for (irradiance_volume, global_transform) in irradiance_volumes.iter() {
547 let Some(image) = image_assets.get(&irradiance_volume.voxels) else {
548 continue;
549 };
550
551 let resolution = image.texture_descriptor.size;
552
553 let voxel_cube_material = voxel_visualization_material_assets.add(ExtendedMaterial {
554 base: StandardMaterial::from(Color::from(RED)),
555 extension: VoxelVisualizationExtension {
556 irradiance_volume_info: VoxelVisualizationIrradianceVolumeInfo {
557 world_from_voxel: VOXEL_FROM_WORLD.inverse(),
558 voxel_from_world: VOXEL_FROM_WORLD,
559 resolution: uvec3(
560 resolution.width,
561 resolution.height,
562 resolution.depth_or_array_layers,
563 ),
564 intensity: IRRADIANCE_VOLUME_INTENSITY,
565 },
566 },
567 });
568
569 let scale = vec3(
570 1.0 / resolution.width as f32,
571 1.0 / resolution.height as f32,
572 1.0 / resolution.depth_or_array_layers as f32,
573 );
574
575 // Spawn a cube for each voxel.
576 for z in 0..resolution.depth_or_array_layers {
577 for y in 0..resolution.height {
578 for x in 0..resolution.width {
579 let uvw = (uvec3(x, y, z).as_vec3() + 0.5) * scale - 0.5;
580 let pos = global_transform.transform_point(uvw);
581 let voxel_cube = commands
582 .spawn((
583 Mesh3d(example_assets.voxel_cube.clone()),
584 MeshMaterial3d(voxel_cube_material.clone()),
585 Transform::from_scale(Vec3::splat(VOXEL_CUBE_SCALE))
586 .with_translation(pos),
587 ))
588 .insert(VoxelCube)
589 .insert(NotShadowCaster)
590 .id();
591
592 commands.entity(voxel_cube_parent).add_child(voxel_cube);
593 }
594 }
595 }
596 }
597 }
598
599 // Draws a gizmo showing the bounds of the irradiance volume.
600 fn draw_gizmo(
601 mut gizmos: Gizmos,
602 irradiance_volume_query: Query<&GlobalTransform, With<IrradianceVolume>>,
603 app_status: Res<AppStatus>,
604 ) {
605 if app_status.voxels_visible {
606 for transform in irradiance_volume_query.iter() {
607 gizmos.cube(*transform, GIZMO_COLOR);
608 }
609 }
610 }
611
612 // Handles a request from the user to toggle the voxel visibility on and off.
613 fn toggle_voxel_visibility(
614 keyboard: Res<ButtonInput<KeyCode>>,
615 mut app_status: ResMut<AppStatus>,
616 mut voxel_cube_parent_query: Query<&mut Visibility, With<VoxelCubeParent>>,
617 ) {
618 if !keyboard.just_pressed(KeyCode::Backspace) {
619 return;
620 }
621
622 app_status.voxels_visible = !app_status.voxels_visible;
623
624 for mut visibility in voxel_cube_parent_query.iter_mut() {
625 *visibility = if app_status.voxels_visible {
626 Visibility::Visible
627 } else {
628 Visibility::Hidden
629 };
630 }
631 }
632
633 impl MaterialExtension for VoxelVisualizationExtension {
634 fn fragment_shader() -> ShaderRef {
635 SHADER_ASSET_PATH.into()
636 }
637 }