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