Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/3d/irradiance_volumes.rs
6592 views
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
core_pipeline::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.wgsl";
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<Scene>,
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<Scene>,
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(AmbientLight {
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(SceneRoot(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: 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
shadows_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
SceneRoot(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<&mut Visibility, (With<MainObject>, With<Mesh3d>, Without<SceneRoot>)>,
373
mut fox_query: Query<&mut Visibility, (With<MainObject>, With<SceneRoot>)>,
374
) {
375
if !keyboard.just_pressed(KeyCode::Tab) {
376
return;
377
}
378
let Some(mut sphere_visibility) = sphere_query.iter_mut().next() else {
379
return;
380
};
381
let Some(mut fox_visibility) = fox_query.iter_mut().next() else {
382
return;
383
};
384
385
match app_status.model {
386
ExampleModel::Sphere => {
387
*sphere_visibility = Visibility::Hidden;
388
*fox_visibility = Visibility::Visible;
389
app_status.model = ExampleModel::Fox;
390
}
391
ExampleModel::Fox => {
392
*sphere_visibility = Visibility::Visible;
393
*fox_visibility = Visibility::Hidden;
394
app_status.model = ExampleModel::Sphere;
395
}
396
}
397
}
398
399
impl Default for AppStatus {
400
fn default() -> Self {
401
Self {
402
irradiance_volume_present: true,
403
rotating: true,
404
model: ExampleModel::Sphere,
405
voxels_visible: false,
406
}
407
}
408
}
409
410
// Turns on and off the irradiance volume as requested by the user.
411
fn toggle_irradiance_volumes(
412
mut commands: Commands,
413
keyboard: Res<ButtonInput<KeyCode>>,
414
light_probe_query: Query<Entity, With<LightProbe>>,
415
mut app_status: ResMut<AppStatus>,
416
assets: Res<ExampleAssets>,
417
mut ambient_light: ResMut<AmbientLight>,
418
) {
419
if !keyboard.just_pressed(KeyCode::Space) {
420
return;
421
};
422
423
let Some(light_probe) = light_probe_query.iter().next() else {
424
return;
425
};
426
427
if app_status.irradiance_volume_present {
428
commands.entity(light_probe).remove::<IrradianceVolume>();
429
ambient_light.brightness = AMBIENT_LIGHT_BRIGHTNESS * IRRADIANCE_VOLUME_INTENSITY;
430
app_status.irradiance_volume_present = false;
431
} else {
432
commands.entity(light_probe).insert(IrradianceVolume {
433
voxels: assets.irradiance_volume.clone(),
434
intensity: IRRADIANCE_VOLUME_INTENSITY,
435
..default()
436
});
437
ambient_light.brightness = 0.0;
438
app_status.irradiance_volume_present = true;
439
}
440
}
441
442
fn toggle_rotation(keyboard: Res<ButtonInput<KeyCode>>, mut app_status: ResMut<AppStatus>) {
443
if keyboard.just_pressed(KeyCode::Enter) {
444
app_status.rotating = !app_status.rotating;
445
}
446
}
447
448
// Handles clicks on the plane that reposition the object.
449
fn handle_mouse_clicks(
450
buttons: Res<ButtonInput<MouseButton>>,
451
windows: Query<&Window, With<PrimaryWindow>>,
452
cameras: Query<(&Camera, &GlobalTransform)>,
453
mut main_objects: Query<&mut Transform, With<MainObject>>,
454
) {
455
if !buttons.pressed(MouseButton::Left) {
456
return;
457
}
458
let Some(mouse_position) = windows.iter().next().and_then(Window::cursor_position) else {
459
return;
460
};
461
let Some((camera, camera_transform)) = cameras.iter().next() else {
462
return;
463
};
464
465
// Figure out where the user clicked on the plane.
466
let Ok(ray) = camera.viewport_to_world(camera_transform, mouse_position) else {
467
return;
468
};
469
let Some(ray_distance) = ray.intersect_plane(Vec3::ZERO, InfinitePlane3d::new(Vec3::Y)) else {
470
return;
471
};
472
let plane_intersection = ray.origin + ray.direction.normalize() * ray_distance;
473
474
// Move all the main objects.
475
for mut transform in main_objects.iter_mut() {
476
transform.translation = vec3(
477
plane_intersection.x,
478
transform.translation.y,
479
plane_intersection.z,
480
);
481
}
482
}
483
484
impl FromWorld for ExampleAssets {
485
fn from_world(world: &mut World) -> Self {
486
let fox_animation =
487
world.load_asset(GltfAssetLabel::Animation(1).from_asset("models/animated/Fox.glb"));
488
let (fox_animation_graph, fox_animation_node) =
489
AnimationGraph::from_clip(fox_animation.clone());
490
491
ExampleAssets {
492
main_sphere: world.add_asset(Sphere::default().mesh().uv(32, 18)),
493
fox: world.load_asset(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")),
494
main_sphere_material: world.add_asset(Color::from(SILVER)),
495
main_scene: world.load_asset(
496
GltfAssetLabel::Scene(0)
497
.from_asset("models/IrradianceVolumeExample/IrradianceVolumeExample.glb"),
498
),
499
irradiance_volume: world.load_asset("irradiance_volumes/Example.vxgi.ktx2"),
500
fox_animation_graph: world.add_asset(fox_animation_graph),
501
fox_animation_node,
502
voxel_cube: world.add_asset(Cuboid::default()),
503
// Just use a specular map for the skybox since it's not too blurry.
504
// In reality you wouldn't do this--you'd use a real skybox texture--but
505
// reusing the textures like this saves space in the Bevy repository.
506
skybox: world.load_asset("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
507
}
508
}
509
}
510
511
// Plays the animation on the fox.
512
fn play_animations(
513
mut commands: Commands,
514
assets: Res<ExampleAssets>,
515
mut players: Query<(Entity, &mut AnimationPlayer), Without<AnimationGraphHandle>>,
516
) {
517
for (entity, mut player) in players.iter_mut() {
518
commands
519
.entity(entity)
520
.insert(AnimationGraphHandle(assets.fox_animation_graph.clone()));
521
player.play(assets.fox_animation_node).repeat();
522
}
523
}
524
525
fn create_cubes(
526
image_assets: Res<Assets<Image>>,
527
mut commands: Commands,
528
irradiance_volumes: Query<(&IrradianceVolume, &GlobalTransform)>,
529
voxel_cube_parents: Query<Entity, With<VoxelCubeParent>>,
530
voxel_cubes: Query<Entity, With<VoxelCube>>,
531
example_assets: Res<ExampleAssets>,
532
mut voxel_visualization_material_assets: ResMut<Assets<VoxelVisualizationMaterial>>,
533
) {
534
// If voxel cubes have already been spawned, don't do anything.
535
if !voxel_cubes.is_empty() {
536
return;
537
}
538
539
let Some(voxel_cube_parent) = voxel_cube_parents.iter().next() else {
540
return;
541
};
542
543
for (irradiance_volume, global_transform) in irradiance_volumes.iter() {
544
let Some(image) = image_assets.get(&irradiance_volume.voxels) else {
545
continue;
546
};
547
548
let resolution = image.texture_descriptor.size;
549
550
let voxel_cube_material = voxel_visualization_material_assets.add(ExtendedMaterial {
551
base: StandardMaterial::from(Color::from(RED)),
552
extension: VoxelVisualizationExtension {
553
irradiance_volume_info: VoxelVisualizationIrradianceVolumeInfo {
554
world_from_voxel: VOXEL_FROM_WORLD.inverse(),
555
voxel_from_world: VOXEL_FROM_WORLD,
556
resolution: uvec3(
557
resolution.width,
558
resolution.height,
559
resolution.depth_or_array_layers,
560
),
561
intensity: IRRADIANCE_VOLUME_INTENSITY,
562
},
563
},
564
});
565
566
let scale = vec3(
567
1.0 / resolution.width as f32,
568
1.0 / resolution.height as f32,
569
1.0 / resolution.depth_or_array_layers as f32,
570
);
571
572
// Spawn a cube for each voxel.
573
for z in 0..resolution.depth_or_array_layers {
574
for y in 0..resolution.height {
575
for x in 0..resolution.width {
576
let uvw = (uvec3(x, y, z).as_vec3() + 0.5) * scale - 0.5;
577
let pos = global_transform.transform_point(uvw);
578
let voxel_cube = commands
579
.spawn((
580
Mesh3d(example_assets.voxel_cube.clone()),
581
MeshMaterial3d(voxel_cube_material.clone()),
582
Transform::from_scale(Vec3::splat(VOXEL_CUBE_SCALE))
583
.with_translation(pos),
584
))
585
.insert(VoxelCube)
586
.insert(NotShadowCaster)
587
.id();
588
589
commands.entity(voxel_cube_parent).add_child(voxel_cube);
590
}
591
}
592
}
593
}
594
}
595
596
// Draws a gizmo showing the bounds of the irradiance volume.
597
fn draw_gizmo(
598
mut gizmos: Gizmos,
599
irradiance_volume_query: Query<&GlobalTransform, With<IrradianceVolume>>,
600
app_status: Res<AppStatus>,
601
) {
602
if app_status.voxels_visible {
603
for transform in irradiance_volume_query.iter() {
604
gizmos.cuboid(*transform, GIZMO_COLOR);
605
}
606
}
607
}
608
609
// Handles a request from the user to toggle the voxel visibility on and off.
610
fn toggle_voxel_visibility(
611
keyboard: Res<ButtonInput<KeyCode>>,
612
mut app_status: ResMut<AppStatus>,
613
mut voxel_cube_parent_query: Query<&mut Visibility, With<VoxelCubeParent>>,
614
) {
615
if !keyboard.just_pressed(KeyCode::Backspace) {
616
return;
617
}
618
619
app_status.voxels_visible = !app_status.voxels_visible;
620
621
for mut visibility in voxel_cube_parent_query.iter_mut() {
622
*visibility = if app_status.voxels_visible {
623
Visibility::Visible
624
} else {
625
Visibility::Hidden
626
};
627
}
628
}
629
630
impl MaterialExtension for VoxelVisualizationExtension {
631
fn fragment_shader() -> ShaderRef {
632
SHADER_ASSET_PATH.into()
633
}
634
}
635
636