reflection_probes/
reflection_probes.rs

1//! This example shows how to place reflection probes in the scene.
2//!
3//! Press Space to cycle through the reflection modes:
4//!
5//! 1. A pre-generated [`EnvironmentMapLight`] acting as a reflection probe, with both the skybox and cubes
6//! 2. A runtime-generated [`GeneratedEnvironmentMapLight`] acting as a reflection probe with just the skybox
7//! 3. A pre-generated [`EnvironmentMapLight`] with just the skybox
8//!
9//! Press Enter to pause or resume rotation.
10//!
11//! Reflection probes don't work on WebGL 2 or WebGPU.
12
13use bevy::{
14    camera::Exposure,
15    core_pipeline::{tonemapping::Tonemapping, Skybox},
16    pbr::generate::generate_environment_map_light,
17    prelude::*,
18    render::{render_resource::TextureUsages, view::Hdr},
19};
20
21use std::{
22    f32::consts::PI,
23    fmt::{Display, Formatter, Result as FmtResult},
24};
25
26static STOP_ROTATION_HELP_TEXT: &str = "Press Enter to stop rotation";
27static START_ROTATION_HELP_TEXT: &str = "Press Enter to start rotation";
28
29static REFLECTION_MODE_HELP_TEXT: &str = "Press Space to switch reflection mode";
30
31const ENV_MAP_INTENSITY: f32 = 5000.0;
32
33// The mode the application is in.
34#[derive(Resource)]
35struct AppStatus {
36    // Which environment maps the user has requested to display.
37    reflection_mode: ReflectionMode,
38    // Whether the user has requested the scene to rotate.
39    rotating: bool,
40    // The current roughness of the central sphere
41    sphere_roughness: f32,
42}
43
44// Which environment maps the user has requested to display.
45#[derive(Clone, Copy, PartialEq)]
46enum ReflectionMode {
47    // Only a world environment map is shown.
48    EnvironmentMap = 0,
49    // Both a world environment map and a reflection probe are present. The
50    // reflection probe is shown in the sphere.
51    ReflectionProbe = 1,
52    // A generated environment map is shown.
53    GeneratedEnvironmentMap = 2,
54}
55
56// The various reflection maps.
57#[derive(Resource)]
58struct Cubemaps {
59    // The blurry diffuse cubemap that reflects the world, but not the cubes.
60    diffuse_environment_map: Handle<Image>,
61
62    // The specular cubemap mip chain that reflects the world, but not the cubes.
63    specular_environment_map: Handle<Image>,
64
65    // The specular cubemap mip chain that reflects both the world and the cubes.
66    specular_reflection_probe: Handle<Image>,
67}
68
69fn main() {
70    // Create the app.
71    App::new()
72        .add_plugins(DefaultPlugins)
73        .init_resource::<AppStatus>()
74        .init_resource::<Cubemaps>()
75        .add_systems(Startup, setup)
76        .add_systems(PreUpdate, add_environment_map_to_camera)
77        .add_systems(
78            Update,
79            change_reflection_type.before(generate_environment_map_light),
80        )
81        .add_systems(Update, toggle_rotation)
82        .add_systems(Update, change_sphere_roughness)
83        .add_systems(
84            Update,
85            rotate_camera
86                .after(toggle_rotation)
87                .after(change_reflection_type),
88        )
89        .add_systems(Update, update_text.after(rotate_camera))
90        .add_systems(Update, setup_environment_map_usage)
91        .run();
92}
93
94// Spawns all the scene objects.
95fn setup(
96    mut commands: Commands,
97    mut meshes: ResMut<Assets<Mesh>>,
98    mut materials: ResMut<Assets<StandardMaterial>>,
99    asset_server: Res<AssetServer>,
100    app_status: Res<AppStatus>,
101    cubemaps: Res<Cubemaps>,
102) {
103    spawn_camera(&mut commands);
104    spawn_sphere(&mut commands, &mut meshes, &mut materials, &app_status);
105    spawn_reflection_probe(&mut commands, &cubemaps);
106    spawn_scene(&mut commands, &asset_server);
107    spawn_text(&mut commands, &app_status);
108}
109
110// Spawns the cubes, light, and camera.
111fn spawn_scene(commands: &mut Commands, asset_server: &AssetServer) {
112    commands.spawn((
113        SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/cubes/Cubes.glb"))),
114        CubesScene,
115    ));
116}
117
118// Spawns the camera.
119fn spawn_camera(commands: &mut Commands) {
120    commands.spawn((
121        Camera3d::default(),
122        Hdr,
123        Exposure { ev100: 11.0 },
124        Tonemapping::AcesFitted,
125        Transform::from_xyz(-3.883, 0.325, 2.781).looking_at(Vec3::ZERO, Vec3::Y),
126    ));
127}
128
129// Creates the sphere mesh and spawns it.
130fn spawn_sphere(
131    commands: &mut Commands,
132    meshes: &mut Assets<Mesh>,
133    materials: &mut Assets<StandardMaterial>,
134    app_status: &AppStatus,
135) {
136    // Create a sphere mesh.
137    let sphere_mesh = meshes.add(Sphere::new(1.0).mesh().ico(7).unwrap());
138
139    // Create a sphere.
140    commands.spawn((
141        Mesh3d(sphere_mesh.clone()),
142        MeshMaterial3d(materials.add(StandardMaterial {
143            base_color: Srgba::hex("#ffffff").unwrap().into(),
144            metallic: 1.0,
145            perceptual_roughness: app_status.sphere_roughness,
146            ..StandardMaterial::default()
147        })),
148        SphereMaterial,
149    ));
150}
151
152// Spawns the reflection probe.
153fn spawn_reflection_probe(commands: &mut Commands, cubemaps: &Cubemaps) {
154    commands.spawn((
155        LightProbe,
156        EnvironmentMapLight {
157            diffuse_map: cubemaps.diffuse_environment_map.clone(),
158            specular_map: cubemaps.specular_reflection_probe.clone(),
159            intensity: ENV_MAP_INTENSITY,
160            ..default()
161        },
162        // 2.0 because the sphere's radius is 1.0 and we want to fully enclose it.
163        Transform::from_scale(Vec3::splat(2.0)),
164    ));
165}
166
167// Spawns the help text.
168fn spawn_text(commands: &mut Commands, app_status: &AppStatus) {
169    // Create the text.
170    commands.spawn((
171        app_status.create_text(),
172        Node {
173            position_type: PositionType::Absolute,
174            bottom: px(12),
175            left: px(12),
176            ..default()
177        },
178    ));
179}
180
181// Adds a world environment map to the camera. This separate system is needed because the camera is
182// managed by the scene spawner, as it's part of the glTF file with the cubes, so we have to add
183// the environment map after the fact.
184fn add_environment_map_to_camera(
185    mut commands: Commands,
186    query: Query<Entity, Added<Camera3d>>,
187    cubemaps: Res<Cubemaps>,
188) {
189    for camera_entity in query.iter() {
190        commands
191            .entity(camera_entity)
192            .insert(create_camera_environment_map_light(&cubemaps))
193            .insert(Skybox {
194                image: cubemaps.specular_environment_map.clone(),
195                brightness: ENV_MAP_INTENSITY,
196                ..default()
197            });
198    }
199}
200
201// A system that handles switching between different reflection modes.
202fn change_reflection_type(
203    mut commands: Commands,
204    light_probe_query: Query<Entity, With<LightProbe>>,
205    cubes_scene_query: Query<Entity, With<CubesScene>>,
206    camera_query: Query<Entity, With<Camera3d>>,
207    keyboard: Res<ButtonInput<KeyCode>>,
208    mut app_status: ResMut<AppStatus>,
209    cubemaps: Res<Cubemaps>,
210    asset_server: Res<AssetServer>,
211) {
212    // Only do anything if space was pressed.
213    if !keyboard.just_pressed(KeyCode::Space) {
214        return;
215    }
216
217    // Advance to the next reflection mode.
218    app_status.reflection_mode =
219        ReflectionMode::try_from((app_status.reflection_mode as u32 + 1) % 3).unwrap();
220
221    // Remove light probes
222    for light_probe in light_probe_query.iter() {
223        commands.entity(light_probe).despawn();
224    }
225    // Remove existing cube scenes
226    for scene_entity in cubes_scene_query.iter() {
227        commands.entity(scene_entity).despawn();
228    }
229    match app_status.reflection_mode {
230        ReflectionMode::EnvironmentMap | ReflectionMode::GeneratedEnvironmentMap => {}
231        ReflectionMode::ReflectionProbe => {
232            spawn_reflection_probe(&mut commands, &cubemaps);
233            spawn_scene(&mut commands, &asset_server);
234        }
235    }
236
237    // Update the environment-map components on the camera entity/entities
238    for camera in camera_query.iter() {
239        // Remove any existing environment-map components
240        commands
241            .entity(camera)
242            .remove::<(EnvironmentMapLight, GeneratedEnvironmentMapLight)>();
243
244        match app_status.reflection_mode {
245            // A baked or reflection-probe environment map
246            ReflectionMode::EnvironmentMap | ReflectionMode::ReflectionProbe => {
247                commands
248                    .entity(camera)
249                    .insert(create_camera_environment_map_light(&cubemaps));
250            }
251
252            // GPU-filtered environment map generated at runtime
253            ReflectionMode::GeneratedEnvironmentMap => {
254                commands
255                    .entity(camera)
256                    .insert(GeneratedEnvironmentMapLight {
257                        environment_map: cubemaps.specular_environment_map.clone(),
258                        intensity: ENV_MAP_INTENSITY,
259                        ..default()
260                    });
261            }
262        }
263    }
264}
265
266// A system that handles enabling and disabling rotation.
267fn toggle_rotation(keyboard: Res<ButtonInput<KeyCode>>, mut app_status: ResMut<AppStatus>) {
268    if keyboard.just_pressed(KeyCode::Enter) {
269        app_status.rotating = !app_status.rotating;
270    }
271}
272
273// A system that updates the help text.
274fn update_text(mut text_query: Query<&mut Text>, app_status: Res<AppStatus>) {
275    for mut text in text_query.iter_mut() {
276        *text = app_status.create_text();
277    }
278}
279
280impl TryFrom<u32> for ReflectionMode {
281    type Error = ();
282
283    fn try_from(value: u32) -> Result<Self, Self::Error> {
284        match value {
285            0 => Ok(ReflectionMode::EnvironmentMap),
286            1 => Ok(ReflectionMode::ReflectionProbe),
287            2 => Ok(ReflectionMode::GeneratedEnvironmentMap),
288            _ => Err(()),
289        }
290    }
291}
292
293impl Display for ReflectionMode {
294    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
295        let text = match *self {
296            ReflectionMode::EnvironmentMap => "Environment map",
297            ReflectionMode::ReflectionProbe => "Reflection probe",
298            ReflectionMode::GeneratedEnvironmentMap => "Generated environment map",
299        };
300        formatter.write_str(text)
301    }
302}
303
304impl AppStatus {
305    // Constructs the help text at the bottom of the screen based on the
306    // application status.
307    fn create_text(&self) -> Text {
308        let rotation_help_text = if self.rotating {
309            STOP_ROTATION_HELP_TEXT
310        } else {
311            START_ROTATION_HELP_TEXT
312        };
313
314        format!(
315            "{}\n{}\nRoughness: {:.2}\n{}\nUp/Down arrows to change roughness",
316            self.reflection_mode,
317            rotation_help_text,
318            self.sphere_roughness,
319            REFLECTION_MODE_HELP_TEXT
320        )
321        .into()
322    }
323}
324
325// Creates the world environment map light, used as a fallback if no reflection
326// probe is applicable to a mesh.
327fn create_camera_environment_map_light(cubemaps: &Cubemaps) -> EnvironmentMapLight {
328    EnvironmentMapLight {
329        diffuse_map: cubemaps.diffuse_environment_map.clone(),
330        specular_map: cubemaps.specular_environment_map.clone(),
331        intensity: ENV_MAP_INTENSITY,
332        ..default()
333    }
334}
335
336// Rotates the camera a bit every frame.
337fn rotate_camera(
338    time: Res<Time>,
339    mut camera_query: Query<&mut Transform, With<Camera3d>>,
340    app_status: Res<AppStatus>,
341) {
342    if !app_status.rotating {
343        return;
344    }
345
346    for mut transform in camera_query.iter_mut() {
347        transform.translation = Vec2::from_angle(time.delta_secs() * PI / 5.0)
348            .rotate(transform.translation.xz())
349            .extend(transform.translation.y)
350            .xzy();
351        transform.look_at(Vec3::ZERO, Vec3::Y);
352    }
353}
354
355// Loads the cubemaps from the assets directory.
356impl FromWorld for Cubemaps {
357    fn from_world(world: &mut World) -> Self {
358        Cubemaps {
359            diffuse_environment_map: world
360                .load_asset("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
361            specular_environment_map: world
362                .load_asset("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
363            specular_reflection_probe: world
364                .load_asset("environment_maps/cubes_reflection_probe_specular_rgb9e5_zstd.ktx2"),
365        }
366    }
367}
368
369fn setup_environment_map_usage(cubemaps: Res<Cubemaps>, mut images: ResMut<Assets<Image>>) {
370    if let Some(image) = images.get_mut(&cubemaps.specular_environment_map)
371        && !image
372            .texture_descriptor
373            .usage
374            .contains(TextureUsages::COPY_SRC)
375    {
376        image.texture_descriptor.usage |= TextureUsages::COPY_SRC;
377    }
378}
379
380impl Default for AppStatus {
381    fn default() -> Self {
382        Self {
383            reflection_mode: ReflectionMode::ReflectionProbe,
384            rotating: false,
385            sphere_roughness: 0.2,
386        }
387    }
388}
389
390#[derive(Component)]
391struct SphereMaterial;
392
393#[derive(Component)]
394struct CubesScene;
395
396// A system that changes the sphere's roughness with up/down arrow keys
397fn change_sphere_roughness(
398    keyboard: Res<ButtonInput<KeyCode>>,
399    mut app_status: ResMut<AppStatus>,
400    mut materials: ResMut<Assets<StandardMaterial>>,
401    sphere_query: Query<&MeshMaterial3d<StandardMaterial>, With<SphereMaterial>>,
402) {
403    let roughness_delta = if keyboard.pressed(KeyCode::ArrowUp) {
404        0.01 // Decrease roughness
405    } else if keyboard.pressed(KeyCode::ArrowDown) {
406        -0.01 // Increase roughness
407    } else {
408        0.0 // No change
409    };
410
411    if roughness_delta != 0.0 {
412        // Update the app status
413        app_status.sphere_roughness =
414            (app_status.sphere_roughness + roughness_delta).clamp(0.0, 1.0);
415
416        // Update the sphere material
417        for material_handle in sphere_query.iter() {
418            if let Some(material) = materials.get_mut(&material_handle.0) {
419                material.perceptual_roughness = app_status.sphere_roughness;
420            }
421        }
422    }
423}