1use 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
13use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
14use crate::theme::basic_example_theme;
15
16#[path = "../helpers/radio.rs"]
17mod radio;
18
19#[path = "../helpers/theme.rs"]
20mod theme;
21
22const LIGHTMAP_EXPOSURE: f32 = 600.0;
24
25const SPHERE_OFFSET: f32 = 0.2;
27
28#[derive(Clone, Default, Resource)]
30struct AppStatus {
31 lighting_mode: LightingMode,
34}
35
36#[derive(Clone, Component, Copy, PartialEq, Default)]
38enum LightingMode {
39 Baked,
45
46 MixedDirect,
56
57 #[default]
67 MixedIndirect,
68
69 RealTime,
76}
77
78#[derive(Clone, Copy, Default, Message)]
82struct LightingModeChanged;
83
84#[derive(Clone, Copy, Component, Debug)]
85struct HelpText;
86
87static 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
115static SPHERE_UV_RECT: Rect = uv_rect_opengl(vec2(0.788, 0.484), Vec2::splat(0.062));
116
117const INITIAL_SPHERE_POSITION: Vec3 = vec3(0.0, 0.5233223, 0.0);
122
123fn 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
156fn 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
164fn 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
171fn 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 lighting_mode_changed_writer.write(LightingModeChanged);
188 },
189 );
190}
191
192fn 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 )]
207 });
208}
209
210fn 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
224fn 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 if lighting_mode_changed_reader.read().next().is_none() {
240 return;
241 }
242
243 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 for (lightmap_name, uv_rect) in LIGHTMAPS {
264 if &**name != lightmap_name {
265 continue;
266 }
267
268 if let Some(ref mut material) = materials.get_mut(material) {
270 material.lightmap_exposure = LIGHTMAP_EXPOSURE;
271 }
272
273 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 if &**name == "Sphere" {
291 if let Some(ref mut material) = materials.get_mut(material) {
293 material.lightmap_exposure = LIGHTMAP_EXPOSURE;
294 }
295
296 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
314const 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
329fn 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
342fn update_directional_light(
345 mut lights: Query<&mut DirectionalLight>,
346 mut lighting_mode_changed_reader: MessageReader<LightingModeChanged>,
347 app_status: Res<AppStatus>,
348) {
349 if lighting_mode_changed_reader.read().next().is_none() {
352 return;
353 }
354
355 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 light.shadow_maps_enabled = scenery_is_lit_in_real_time;
366 }
367}
368
369fn 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
385fn 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 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
413fn 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 if app_status.lighting_mode == LightingMode::Baked
427 || !mouse_button_input.pressed(MouseButton::Left)
428 {
429 return;
430 }
431
432 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 let Ok(mut transform) = transforms.get_mut(child_of.parent()) else {
449 return;
450 };
451
452 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
468fn 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
487fn 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
492Sphere: Static, baked direct light, baked indirect light",
493 ),
494 LightingMode::MixedDirect => Text::new(
495 "Scenery: Static, baked direct light, baked indirect light
496Sphere: Dynamic, real-time direct light, no indirect light
497Click in the scene to move the sphere",
498 ),
499 LightingMode::MixedIndirect => Text::new(
500 "Scenery: Static, real-time direct light, baked indirect light
501Sphere: Dynamic, real-time direct light, no indirect light
502Click in the scene to move the sphere",
503 ),
504 LightingMode::RealTime => Text::new(
505 "Scenery: Dynamic, real-time direct light, no indirect light
506Sphere: Dynamic, real-time direct light, no indirect light
507Click in the scene to move the sphere",
508 ),
509 }
510}