docs/bevy/examples/3d/mixed_lighting.rs 17.5 K · 510 lines · raw · history

1 //! Demonstrates how to combine baked and dynamic lighting.
2
3 use bevy::{
4 feathers::{theme::UiTheme, FeathersPlugins},
5 gltf::GltfMeshName,
6 pbr::Lightmap,
7 picking::{backend::HitData, pointer::PointerInteraction},
8 prelude::*,
9 ui_widgets::{radio_self_update, ValueChange},
10 world_serialization::WorldInstanceReady,
11 };
12
13 use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
14 use crate::theme::basic_example_theme;
15
16 #[path = "../helpers/radio.rs"]
17 mod radio;
18
19 #[path = "../helpers/theme.rs"]
20 mod theme;
21
22 /// How bright the lightmaps are.
23 const LIGHTMAP_EXPOSURE: f32 = 600.0;
24
25 /// How far above the ground the sphere's origin is when moved, in scene units.
26 const SPHERE_OFFSET: f32 = 0.2;
27
28 /// The settings that the user has currently chosen for the app.
29 #[derive(Clone, Default, Resource)]
30 struct AppStatus {
31 /// The lighting mode that the user currently has set: baked, mixed, or
32 /// real-time.
33 lighting_mode: LightingMode,
34 }
35
36 /// The type of lighting to use in the scene.
37 #[derive(Clone, Component, Copy, PartialEq, Default)]
38 enum LightingMode {
39 /// All light is computed ahead of time; no lighting takes place at runtime.
40 ///
41 /// In this mode, the sphere can't be moved, as the light shining on it was
42 /// precomputed. On the plus side, the sphere has indirect lighting in this
43 /// mode, as the red hue on the bottom of the sphere demonstrates.
44 Baked,
45
46 /// All light for the static objects is computed ahead of time, but the
47 /// light for the dynamic sphere is computed at runtime.
48 ///
49 /// In this mode, the sphere can be moved, and the light will be computed
50 /// for it as you do so. The sphere loses indirect illumination; notice the
51 /// lack of a red hue at the base of the sphere. However, the rest of the
52 /// scene has indirect illumination. Note also that the sphere doesn't cast
53 /// a shadow on the static objects in this mode, because shadows are part of
54 /// the lighting computation.
55 MixedDirect,
56
57 /// Indirect light for the static objects is computed ahead of time, and
58 /// direct light for all objects is computed at runtime.
59 ///
60 /// In this mode, the sphere can be moved, and the light will be computed
61 /// for it as you do so. The sphere loses indirect illumination; notice the
62 /// lack of a red hue at the base of the sphere. However, the rest of the
63 /// scene has indirect illumination. The sphere does cast a shadow on
64 /// objects in this mode, because the direct light for all objects is being
65 /// computed dynamically.
66 #[default]
67 MixedIndirect,
68
69 /// Light is computed at runtime for all objects.
70 ///
71 /// In this mode, no lightmaps are used at all. All objects are dynamically
72 /// lit, which provides maximum flexibility. However, the downside is that
73 /// global illumination is lost; note that the base of the sphere isn't red
74 /// as it is in baked mode.
75 RealTime,
76 }
77
78 /// A message that's written whenever the user changes the lighting mode.
79 ///
80 /// This is also written when the scene loads for the first time.
81 #[derive(Clone, Copy, Default, Message)]
82 struct LightingModeChanged;
83
84 #[derive(Clone, Copy, Component, Debug)]
85 struct HelpText;
86
87 /// The name of every static object in the scene that has a lightmap, as well as
88 /// the UV rect of its lightmap.
89 ///
90 /// Storing this as an array and doing a linear search through it is rather
91 /// inefficient, but we do it anyway for clarity's sake.
92 static LIGHTMAPS: [(&str, Rect); 5] = [
93 (
94 "Plane",
95 uv_rect_opengl(Vec2::splat(0.026), Vec2::splat(0.710)),
96 ),
97 (
98 "SheenChair_fabric",
99 uv_rect_opengl(vec2(0.7864, 0.02377), vec2(0.1910, 0.1912)),
100 ),
101 (
102 "SheenChair_label",
103 uv_rect_opengl(vec2(0.275, -0.016), vec2(0.858, 0.486)),
104 ),
105 (
106 "SheenChair_metal",
107 uv_rect_opengl(vec2(0.998, 0.506), vec2(-0.029, -0.067)),
108 ),
109 (
110 "SheenChair_wood",
111 uv_rect_opengl(vec2(0.787, 0.257), vec2(0.179, 0.177)),
112 ),
113 ];
114
115 static SPHERE_UV_RECT: Rect = uv_rect_opengl(vec2(0.788, 0.484), Vec2::splat(0.062));
116
117 /// The initial position of the sphere.
118 ///
119 /// When the user sets the light mode to [`LightingMode::Baked`], we reset the
120 /// position to this point.
121 const INITIAL_SPHERE_POSITION: Vec3 = vec3(0.0, 0.5233223, 0.0);
122
123 fn main() {
124 App::new()
125 .add_plugins((
126 DefaultPlugins.set(WindowPlugin {
127 primary_window: Some(Window {
128 title: "Bevy Mixed Lighting Example".into(),
129 ..default()
130 }),
131 ..default()
132 }),
133 FeathersPlugins,
134 ))
135 .add_plugins(MeshPickingPlugin)
136 .insert_resource(UiTheme(basic_example_theme(Color::BLACK)))
137 .insert_resource(GlobalAmbientLight {
138 color: ClearColor::default().0,
139 brightness: 10000.0,
140 affects_lightmapped_meshes: true,
141 })
142 .init_resource::<AppStatus>()
143 .add_message::<LightingModeChanged>()
144 .add_systems(Startup, setup)
145 .add_systems(Update, update_lightmaps)
146 .add_systems(Update, update_directional_light)
147 .add_systems(Update, make_sphere_nonpickable)
148 .add_observer(handle_lighting_mode_change)
149 .add_observer(radio_self_update)
150 .add_systems(Update, reset_sphere_position)
151 .add_systems(Update, move_sphere)
152 .add_systems(Update, adjust_help_text)
153 .run();
154 }
155
156 /// Creates the scene.
157 fn setup(mut commands: Commands, asset_server: Res<AssetServer>, app_status: Res<AppStatus>) {
158 spawn_camera(&mut commands);
159 spawn_scene(&mut commands, &asset_server);
160 spawn_buttons(&mut commands);
161 spawn_help_text(&mut commands, &app_status);
162 }
163
164 /// Spawns the 3D camera.
165 fn spawn_camera(commands: &mut Commands) {
166 commands
167 .spawn(Camera3d::default())
168 .insert(Transform::from_xyz(-0.7, 0.7, 1.0).looking_at(vec3(0.0, 0.3, 0.0), Vec3::Y));
169 }
170
171 /// Spawns the scene.
172 ///
173 /// The scene is loaded from a glTF file.
174 fn spawn_scene(commands: &mut Commands, asset_server: &AssetServer) {
175 commands
176 .spawn(WorldAssetRoot(
177 asset_server.load(
178 GltfAssetLabel::Scene(0)
179 .from_asset("models/MixedLightingExample/MixedLightingExample.gltf"),
180 ),
181 ))
182 .observe(
183 |_: On<WorldInstanceReady>,
184 mut lighting_mode_changed_writer: MessageWriter<LightingModeChanged>| {
185 // When the scene loads, send a `LightingModeChanged` event so
186 // that we set up the lightmaps.
187 lighting_mode_changed_writer.write(LightingModeChanged);
188 },
189 );
190 }
191
192 /// Spawns the buttons that allow the user to change the lighting mode.
193 fn spawn_buttons(commands: &mut Commands) {
194 commands.spawn_scene(bsn! {
195 @main_ui_node_scene()
196 Children [
197 @feathers_option_buttons(
198 "Lighting",
199 &[
200 (LightingMode::Baked, "Baked"),
201 (LightingMode::MixedDirect, "Mixed (Direct)"),
202 (LightingMode::MixedIndirect, "Mixed (Indirect)"),
203 (LightingMode::RealTime, "Real-Time"),
204 ],
205 2 // Initially set to MixedIndirect
206 )]
207 });
208 }
209
210 /// Spawns the help text at the top of the window.
211 fn spawn_help_text(commands: &mut Commands, app_status: &AppStatus) {
212 commands.spawn((
213 create_help_text(app_status),
214 Node {
215 position_type: PositionType::Absolute,
216 top: px(12),
217 left: px(12),
218 ..default()
219 },
220 HelpText,
221 ));
222 }
223
224 /// Adds lightmaps to and/or removes lightmaps from objects in the scene when
225 /// the lighting mode changes.
226 ///
227 /// This is also called right after the scene loads in order to set up the
228 /// lightmaps.
229 fn update_lightmaps(
230 mut commands: Commands,
231 asset_server: Res<AssetServer>,
232 mut materials: ResMut<Assets<StandardMaterial>>,
233 meshes: Query<(Entity, &GltfMeshName, &MeshMaterial3d<StandardMaterial>), With<Mesh3d>>,
234 mut lighting_mode_changed_reader: MessageReader<LightingModeChanged>,
235 app_status: Res<AppStatus>,
236 ) {
237 // Only run if the lighting mode changed. (Note that a change event is fired
238 // when the scene first loads.)
239 if lighting_mode_changed_reader.read().next().is_none() {
240 return;
241 }
242
243 // Select the lightmap to use, based on the lighting mode.
244 let lightmap: Option<Handle<Image>> = match app_status.lighting_mode {
245 LightingMode::Baked => {
246 Some(asset_server.load("lightmaps/MixedLightingExample-Baked.zstd.ktx2"))
247 }
248 LightingMode::MixedDirect => {
249 Some(asset_server.load("lightmaps/MixedLightingExample-MixedDirect.zstd.ktx2"))
250 }
251 LightingMode::MixedIndirect => {
252 Some(asset_server.load("lightmaps/MixedLightingExample-MixedIndirect.zstd.ktx2"))
253 }
254 LightingMode::RealTime => None,
255 };
256
257 'outer: for (entity, name, material) in &meshes {
258 // Add lightmaps to or remove lightmaps from the scenery objects in the
259 // scene (all objects but the sphere).
260 //
261 // Note that doing a linear search through the `LIGHTMAPS` array is
262 // inefficient, but we do it anyway in this example to improve clarity.
263 for (lightmap_name, uv_rect) in LIGHTMAPS {
264 if &**name != lightmap_name {
265 continue;
266 }
267
268 // Lightmap exposure defaults to zero, so we need to set it.
269 if let Some(ref mut material) = materials.get_mut(material) {
270 material.lightmap_exposure = LIGHTMAP_EXPOSURE;
271 }
272
273 // Add or remove the lightmap.
274 match lightmap {
275 Some(ref lightmap) => {
276 commands.entity(entity).insert(Lightmap {
277 image: (*lightmap).clone(),
278 uv_rect,
279 bicubic_sampling: false,
280 });
281 }
282 None => {
283 commands.entity(entity).remove::<Lightmap>();
284 }
285 }
286 continue 'outer;
287 }
288
289 // Add lightmaps to or remove lightmaps from the sphere.
290 if &**name == "Sphere" {
291 // Lightmap exposure defaults to zero, so we need to set it.
292 if let Some(ref mut material) = materials.get_mut(material) {
293 material.lightmap_exposure = LIGHTMAP_EXPOSURE;
294 }
295
296 // Add or remove the lightmap from the sphere. We only apply the
297 // lightmap in fully-baked mode.
298 match (&lightmap, app_status.lighting_mode) {
299 (Some(lightmap), LightingMode::Baked) => {
300 commands.entity(entity).insert(Lightmap {
301 image: (*lightmap).clone(),
302 uv_rect: SPHERE_UV_RECT,
303 bicubic_sampling: false,
304 });
305 }
306 _ => {
307 commands.entity(entity).remove::<Lightmap>();
308 }
309 }
310 }
311 }
312 }
313
314 /// Converts a uv rectangle from the OpenGL coordinate system (origin in the
315 /// lower left) to the Vulkan coordinate system (origin in the upper left) that
316 /// Bevy uses.
317 ///
318 /// For this particular example, the baking tool happened to use the OpenGL
319 /// coordinate system, so it was more convenient to do the conversion at compile
320 /// time than to pre-calculate and hard-code the values.
321 const fn uv_rect_opengl(gl_min: Vec2, size: Vec2) -> Rect {
322 let min = vec2(gl_min.x, 1.0 - gl_min.y - size.y);
323 Rect {
324 min,
325 max: vec2(min.x + size.x, min.y + size.y),
326 }
327 }
328
329 /// Ensures that clicking on the scene to move the sphere doesn't result in a
330 /// hit on the sphere itself.
331 fn make_sphere_nonpickable(
332 mut commands: Commands,
333 mut query: Query<(Entity, &Name), (With<Mesh3d>, Without<Pickable>)>,
334 ) {
335 for (sphere, name) in &mut query {
336 if &**name == "Sphere" {
337 commands.entity(sphere).insert(Pickable::IGNORE);
338 }
339 }
340 }
341
342 /// Updates the directional light settings as necessary when the lighting mode
343 /// changes.
344 fn update_directional_light(
345 mut lights: Query<&mut DirectionalLight>,
346 mut lighting_mode_changed_reader: MessageReader<LightingModeChanged>,
347 app_status: Res<AppStatus>,
348 ) {
349 // Only run if the lighting mode changed. (Note that a change event is fired
350 // when the scene first loads.)
351 if lighting_mode_changed_reader.read().next().is_none() {
352 return;
353 }
354
355 // Real-time direct light is used on the scenery if we're using mixed
356 // indirect or real-time mode.
357 let scenery_is_lit_in_real_time = matches!(
358 app_status.lighting_mode,
359 LightingMode::MixedIndirect | LightingMode::RealTime
360 );
361
362 for mut light in &mut lights {
363 light.affects_lightmapped_mesh_diffuse = scenery_is_lit_in_real_time;
364 // Don't bother enabling shadows if they won't show up on the scenery.
365 light.shadow_maps_enabled = scenery_is_lit_in_real_time;
366 }
367 }
368
369 /// Handles clicks on the widgets at the bottom of the screen and fires
370 /// [`LightingModeChanged`] events.
371 fn handle_lighting_mode_change(
372 event: On<ValueChange<Entity>>,
373 new_value_q: Query<&RadioButtonOptionValue<LightingMode>>,
374 mut lighting_mode_changed_writer: MessageWriter<LightingModeChanged>,
375 mut app_status: ResMut<AppStatus>,
376 ) {
377 let Ok(RadioButtonOptionValue(new_lighting_mode)) = new_value_q.get(event.value) else {
378 return;
379 };
380
381 app_status.lighting_mode = *new_lighting_mode;
382 lighting_mode_changed_writer.write(LightingModeChanged);
383 }
384
385 /// Moves the sphere to its original position when the user selects the baked
386 /// lighting mode.
387 ///
388 /// As the light from the sphere is precomputed and depends on the sphere's
389 /// original position, the sphere must be placed there in order for the lighting
390 /// to be correct.
391 fn reset_sphere_position(
392 mut objects: Query<(&Name, &mut Transform)>,
393 mut lighting_mode_changed_reader: MessageReader<LightingModeChanged>,
394 app_status: Res<AppStatus>,
395 ) {
396 // Only run if the lighting mode changed and if the lighting mode is
397 // `LightingMode::Baked`. (Note that a change event is fired when the scene
398 // first loads.)
399 if lighting_mode_changed_reader.read().next().is_none()
400 || app_status.lighting_mode != LightingMode::Baked
401 {
402 return;
403 }
404
405 for (name, mut transform) in &mut objects {
406 if &**name == "Sphere" {
407 transform.translation = INITIAL_SPHERE_POSITION;
408 break;
409 }
410 }
411 }
412
413 /// Updates the position of the sphere when the user clicks on a spot in the
414 /// scene.
415 ///
416 /// Note that the position of the sphere is locked in baked lighting mode.
417 fn move_sphere(
418 mouse_button_input: Res<ButtonInput<MouseButton>>,
419 pointers: Query<&PointerInteraction>,
420 mut meshes: Query<(&GltfMeshName, &ChildOf), With<Mesh3d>>,
421 mut transforms: Query<&mut Transform>,
422 app_status: Res<AppStatus>,
423 ) {
424 // Only run when the left button is clicked and we're not in baked lighting
425 // mode.
426 if app_status.lighting_mode == LightingMode::Baked
427 || !mouse_button_input.pressed(MouseButton::Left)
428 {
429 return;
430 }
431
432 // Find the sphere.
433 let Some(child_of) = meshes
434 .iter_mut()
435 .filter_map(|(name, child_of)| {
436 if &**name == "Sphere" {
437 Some(child_of)
438 } else {
439 None
440 }
441 })
442 .next()
443 else {
444 return;
445 };
446
447 // Grab its transform.
448 let Ok(mut transform) = transforms.get_mut(child_of.parent()) else {
449 return;
450 };
451
452 // Set its transform to the appropriate position, as determined by the
453 // picking subsystem.
454 for interaction in pointers.iter() {
455 if let Some(&(
456 _,
457 HitData {
458 position: Some(position),
459 ..
460 },
461 )) = interaction.get_nearest_hit()
462 {
463 transform.translation = position + vec3(0.0, SPHERE_OFFSET, 0.0);
464 }
465 }
466 }
467
468 /// Changes the help text at the top of the screen when the lighting mode
469 /// changes.
470 fn adjust_help_text(
471 mut commands: Commands,
472 help_texts: Query<Entity, With<HelpText>>,
473 app_status: Res<AppStatus>,
474 mut lighting_mode_changed_reader: MessageReader<LightingModeChanged>,
475 ) {
476 if lighting_mode_changed_reader.read().next().is_none() {
477 return;
478 }
479
480 for help_text in &help_texts {
481 commands
482 .entity(help_text)
483 .insert(create_help_text(&app_status));
484 }
485 }
486
487 /// Returns appropriate text to display at the top of the screen.
488 fn create_help_text(app_status: &AppStatus) -> Text {
489 match app_status.lighting_mode {
490 LightingMode::Baked => Text::new(
491 "Scenery: Static, baked direct light, baked indirect light
492 Sphere: Static, baked direct light, baked indirect light",
493 ),
494 LightingMode::MixedDirect => Text::new(
495 "Scenery: Static, baked direct light, baked indirect light
496 Sphere: Dynamic, real-time direct light, no indirect light
497 Click in the scene to move the sphere",
498 ),
499 LightingMode::MixedIndirect => Text::new(
500 "Scenery: Static, real-time direct light, baked indirect light
501 Sphere: Dynamic, real-time direct light, no indirect light
502 Click in the scene to move the sphere",
503 ),
504 LightingMode::RealTime => Text::new(
505 "Scenery: Dynamic, real-time direct light, no indirect light
506 Sphere: Dynamic, real-time direct light, no indirect light
507 Click in the scene to move the sphere",
508 ),
509 }
510 }