1use bevy::{
4 gltf::GltfMeshName,
5 pbr::Lightmap,
6 picking::{backend::HitData, pointer::PointerInteraction},
7 prelude::*,
8 scene::SceneInstanceReady,
9};
10
11use crate::widgets::{RadioButton, RadioButtonText, WidgetClickEvent, WidgetClickSender};
12
13#[path = "../helpers/widgets.rs"]
14mod widgets;
15
16const LIGHTMAP_EXPOSURE: f32 = 600.0;
18
19const SPHERE_OFFSET: f32 = 0.2;
21
22#[derive(Clone, Default, Resource)]
24struct AppStatus {
25 lighting_mode: LightingMode,
28}
29
30#[derive(Clone, Copy, PartialEq, Default)]
32enum LightingMode {
33 Baked,
39
40 MixedDirect,
50
51 #[default]
61 MixedIndirect,
62
63 RealTime,
70}
71
72#[derive(Clone, Copy, Default, Message)]
76struct LightingModeChanged;
77
78#[derive(Clone, Copy, Component, Debug)]
79struct HelpText;
80
81static 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
109static SPHERE_UV_RECT: Rect = uv_rect_opengl(vec2(0.788, 0.484), Vec2::splat(0.062));
110
111const INITIAL_SPHERE_POSITION: Vec3 = vec3(0.0, 0.5233223, 0.0);
116
117fn 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_message::<WidgetClickEvent<LightingMode>>()
134 .add_message::<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
148fn 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
156fn 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
163fn 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_changed_writer: MessageWriter<LightingModeChanged>| {
177 lighting_mode_changed_writer.write(LightingModeChanged);
180 },
181 );
182}
183
184fn 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
200fn 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
214fn 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_changed_reader: MessageReader<LightingModeChanged>,
225 app_status: Res<AppStatus>,
226) {
227 if lighting_mode_changed_reader.read().next().is_none() {
230 return;
231 }
232
233 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 for (lightmap_name, uv_rect) in LIGHTMAPS {
254 if &**name != lightmap_name {
255 continue;
256 }
257
258 if let Some(ref mut material) = materials.get_mut(material) {
260 material.lightmap_exposure = LIGHTMAP_EXPOSURE;
261 }
262
263 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 if &**name == "Sphere" {
281 if let Some(ref mut material) = materials.get_mut(material) {
283 material.lightmap_exposure = LIGHTMAP_EXPOSURE;
284 }
285
286 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
304const 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
319fn 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
332fn update_directional_light(
335 mut lights: Query<&mut DirectionalLight>,
336 mut lighting_mode_changed_reader: MessageReader<LightingModeChanged>,
337 app_status: Res<AppStatus>,
338) {
339 if lighting_mode_changed_reader.read().next().is_none() {
342 return;
343 }
344
345 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 light.shadows_enabled = scenery_is_lit_in_real_time;
356 }
357}
358
359fn 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
386fn handle_lighting_mode_change(
389 mut widget_click_event_reader: MessageReader<WidgetClickEvent<LightingMode>>,
390 mut lighting_mode_changed_writer: MessageWriter<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_changed_writer.write(LightingModeChanged);
396 }
397}
398
399fn reset_sphere_position(
406 mut objects: Query<(&Name, &mut Transform)>,
407 mut lighting_mode_changed_reader: MessageReader<LightingModeChanged>,
408 app_status: Res<AppStatus>,
409) {
410 if lighting_mode_changed_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
427fn 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 if app_status.lighting_mode == LightingMode::Baked
441 || !mouse_button_input.pressed(MouseButton::Left)
442 {
443 return;
444 }
445
446 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 let Ok(mut transform) = transforms.get_mut(child_of.parent()) else {
463 return;
464 };
465
466 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
482fn adjust_help_text(
485 mut commands: Commands,
486 help_texts: Query<Entity, With<HelpText>>,
487 app_status: Res<AppStatus>,
488 mut lighting_mode_changed_reader: MessageReader<LightingModeChanged>,
489) {
490 if lighting_mode_changed_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
501fn 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
506Sphere: Static, baked direct light, baked indirect light",
507 ),
508 LightingMode::MixedDirect => Text::new(
509 "Scenery: Static, baked direct light, baked indirect light
510Sphere: Dynamic, real-time direct light, no indirect light
511Click in the scene to move the sphere",
512 ),
513 LightingMode::MixedIndirect => Text::new(
514 "Scenery: Static, real-time direct light, baked indirect light
515Sphere: Dynamic, real-time direct light, no indirect light
516Click in the scene to move the sphere",
517 ),
518 LightingMode::RealTime => Text::new(
519 "Scenery: Dynamic, real-time direct light, no indirect light
520Sphere: Dynamic, real-time direct light, no indirect light
521Click in the scene to move the sphere",
522 ),
523 }
524}