1use 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#[derive(Resource)]
35struct AppStatus {
36 reflection_mode: ReflectionMode,
38 rotating: bool,
40 sphere_roughness: f32,
42}
43
44#[derive(Clone, Copy, PartialEq)]
46enum ReflectionMode {
47 EnvironmentMap = 0,
49 ReflectionProbe = 1,
52 GeneratedEnvironmentMap = 2,
54}
55
56#[derive(Resource)]
58struct Cubemaps {
59 diffuse_environment_map: Handle<Image>,
61
62 specular_environment_map: Handle<Image>,
64
65 specular_reflection_probe: Handle<Image>,
67}
68
69fn main() {
70 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
94fn 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
110fn 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
118fn 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
129fn spawn_sphere(
131 commands: &mut Commands,
132 meshes: &mut Assets<Mesh>,
133 materials: &mut Assets<StandardMaterial>,
134 app_status: &AppStatus,
135) {
136 let sphere_mesh = meshes.add(Sphere::new(1.0).mesh().ico(7).unwrap());
138
139 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
152fn 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 Transform::from_scale(Vec3::splat(2.0)),
164 ));
165}
166
167fn spawn_text(commands: &mut Commands, app_status: &AppStatus) {
169 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
181fn 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
201fn 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 if !keyboard.just_pressed(KeyCode::Space) {
214 return;
215 }
216
217 app_status.reflection_mode =
219 ReflectionMode::try_from((app_status.reflection_mode as u32 + 1) % 3).unwrap();
220
221 for light_probe in light_probe_query.iter() {
223 commands.entity(light_probe).despawn();
224 }
225 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 for camera in camera_query.iter() {
239 commands
241 .entity(camera)
242 .remove::<(EnvironmentMapLight, GeneratedEnvironmentMapLight)>();
243
244 match app_status.reflection_mode {
245 ReflectionMode::EnvironmentMap | ReflectionMode::ReflectionProbe => {
247 commands
248 .entity(camera)
249 .insert(create_camera_environment_map_light(&cubemaps));
250 }
251
252 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
266fn 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
273fn 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 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
325fn 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
336fn 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
355impl 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
396fn 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 } else if keyboard.pressed(KeyCode::ArrowDown) {
406 -0.01 } else {
408 0.0 };
410
411 if roughness_delta != 0.0 {
412 app_status.sphere_roughness =
414 (app_status.sphere_roughness + roughness_delta).clamp(0.0, 1.0);
415
416 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}