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