1use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, PI};
4use std::fmt::{self, Formatter};
5
6use bevy::{
7 camera::primitives::CubemapLayout,
8 color::palettes::css::{SILVER, YELLOW},
9 input::mouse::AccumulatedMouseMotion,
10 light::{DirectionalLightTexture, NotShadowCaster, PointLightTexture, SpotLightTexture},
11 pbr::decal,
12 prelude::*,
13 render::renderer::{RenderAdapter, RenderDevice},
14 window::{CursorIcon, SystemCursorIcon},
15};
16use light_consts::lux::{AMBIENT_DAYLIGHT, CLEAR_SUNRISE};
17use ops::{acos, cos, sin};
18use widgets::{
19 WidgetClickEvent, WidgetClickSender, BUTTON_BORDER, BUTTON_BORDER_COLOR,
20 BUTTON_BORDER_RADIUS_SIZE, BUTTON_PADDING,
21};
22
23#[path = "../helpers/widgets.rs"]
24mod widgets;
25
26const CUBE_ROTATION_SPEED: f32 = 0.02;
28
29const MOVE_SPEED: f32 = 0.008;
32const SCALE_SPEED: f32 = 0.05;
34const ROLL_SPEED: f32 = 0.01;
36
37#[derive(Resource, Default)]
39struct AppStatus {
40 selection: Selection,
43 drag_mode: DragMode,
46}
47
48#[derive(Clone, Copy, Component, Default, PartialEq)]
50enum Selection {
51 #[default]
55 Camera,
56 SpotLight,
58 PointLight,
60 DirectionalLight,
62}
63
64impl fmt::Display for Selection {
65 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
66 match *self {
67 Selection::Camera => f.write_str("camera"),
68 Selection::SpotLight => f.write_str("spotlight"),
69 Selection::PointLight => f.write_str("point light"),
70 Selection::DirectionalLight => f.write_str("directional light"),
71 }
72 }
73}
74
75#[derive(Clone, Copy, Component, Default, PartialEq, Debug)]
78enum DragMode {
79 #[default]
81 Move,
82 Scale,
86 Roll,
90}
91
92impl fmt::Display for DragMode {
93 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
94 match *self {
95 DragMode::Move => f.write_str("move"),
96 DragMode::Scale => f.write_str("scale"),
97 DragMode::Roll => f.write_str("roll"),
98 }
99 }
100}
101
102#[derive(Clone, Copy, Component)]
104struct HelpText;
105
106fn main() {
108 App::new()
109 .add_plugins(DefaultPlugins.set(WindowPlugin {
110 primary_window: Some(Window {
111 title: "Bevy Light Textures Example".into(),
112 ..default()
113 }),
114 ..default()
115 }))
116 .init_resource::<AppStatus>()
117 .add_message::<WidgetClickEvent<Selection>>()
118 .add_message::<WidgetClickEvent<Visibility>>()
119 .add_systems(Startup, setup)
120 .add_systems(Update, draw_gizmos)
121 .add_systems(Update, rotate_cube)
122 .add_systems(Update, hide_shadows)
123 .add_systems(Update, widgets::handle_ui_interactions::<Selection>)
124 .add_systems(Update, widgets::handle_ui_interactions::<Visibility>)
125 .add_systems(
126 Update,
127 (handle_selection_change, update_radio_buttons)
128 .after(widgets::handle_ui_interactions::<Selection>)
129 .after(widgets::handle_ui_interactions::<Visibility>),
130 )
131 .add_systems(Update, toggle_visibility)
132 .add_systems(Update, update_directional_light)
133 .add_systems(Update, process_move_input)
134 .add_systems(Update, process_scale_input)
135 .add_systems(Update, process_roll_input)
136 .add_systems(Update, switch_drag_mode)
137 .add_systems(Update, update_help_text)
138 .add_systems(Update, update_button_visibility)
139 .run();
140}
141
142fn setup(
144 mut commands: Commands,
145 asset_server: Res<AssetServer>,
146 app_status: Res<AppStatus>,
147 render_device: Res<RenderDevice>,
148 render_adapter: Res<RenderAdapter>,
149 mut meshes: ResMut<Assets<Mesh>>,
150 mut materials: ResMut<Assets<StandardMaterial>>,
151) {
152 if !decal::clustered::clustered_decals_are_usable(&render_device, &render_adapter) {
154 error!("Light textures aren't usable on this platform.");
155 commands.write_message(AppExit::error());
156 }
157
158 spawn_cubes(&mut commands, &mut meshes, &mut materials);
159 spawn_camera(&mut commands);
160 spawn_light(&mut commands, &asset_server);
161 spawn_buttons(&mut commands);
162 spawn_help_text(&mut commands, &app_status);
163 spawn_light_textures(&mut commands, &asset_server, &mut meshes, &mut materials);
164}
165
166#[derive(Component)]
167struct Rotate;
168
169fn spawn_cubes(
171 commands: &mut Commands,
172 meshes: &mut Assets<Mesh>,
173 materials: &mut Assets<StandardMaterial>,
174) {
175 let mut transform = Transform::IDENTITY;
177 transform.rotate_y(FRAC_PI_3);
178
179 commands.spawn((
180 Mesh3d(meshes.add(Cuboid::new(3.0, 3.0, 3.0))),
181 MeshMaterial3d(materials.add(StandardMaterial {
182 base_color: SILVER.into(),
183 ..default()
184 })),
185 transform,
186 Rotate,
187 ));
188
189 commands.spawn((
190 Mesh3d(meshes.add(Cuboid::new(-13.0, -13.0, -13.0))),
191 MeshMaterial3d(materials.add(StandardMaterial {
192 base_color: SILVER.into(),
193 ..default()
194 })),
195 transform,
196 ));
197}
198
199fn spawn_light(commands: &mut Commands, asset_server: &AssetServer) {
201 commands.spawn((
202 Visibility::Hidden,
203 Transform::from_xyz(8.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y),
204 Selection::DirectionalLight,
205 children![(
206 DirectionalLight {
207 illuminance: AMBIENT_DAYLIGHT,
208 ..default()
209 },
210 DirectionalLightTexture {
211 image: asset_server.load("lightmaps/caustic_directional_texture.png"),
212 tiled: true,
213 },
214 Visibility::Visible,
215 )],
216 ));
217}
218
219fn spawn_camera(commands: &mut Commands) {
221 commands
222 .spawn(Camera3d::default())
223 .insert(Transform::from_xyz(0.0, 2.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y))
224 .insert(Selection::Camera);
226}
227
228fn spawn_light_textures(
229 commands: &mut Commands,
230 asset_server: &AssetServer,
231 meshes: &mut Assets<Mesh>,
232 materials: &mut Assets<StandardMaterial>,
233) {
234 commands.spawn((
235 SpotLight {
236 color: Color::srgb(1.0, 1.0, 0.8),
237 intensity: 10e6,
238 outer_angle: 0.25,
239 inner_angle: 0.25,
240 shadows_enabled: true,
241 ..default()
242 },
243 Transform::from_translation(Vec3::new(6.0, 1.0, 2.0)).looking_at(Vec3::ZERO, Vec3::Y),
244 SpotLightTexture {
245 image: asset_server.load("lightmaps/torch_spotlight_texture.png"),
246 },
247 Visibility::Inherited,
248 Selection::SpotLight,
249 ));
250
251 commands.spawn((
252 Visibility::Hidden,
253 Transform::from_translation(Vec3::new(0.0, 1.8, 0.01)).with_scale(Vec3::splat(0.1)),
254 Selection::PointLight,
255 children![
256 SceneRoot(
257 asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/Faces/faces.glb")),
258 ),
259 (
260 Mesh3d(meshes.add(Sphere::new(1.0))),
261 MeshMaterial3d(materials.add(StandardMaterial {
262 emissive: Color::srgb(0.0, 0.0, 300.0).to_linear(),
263 ..default()
264 })),
265 ),
266 (
267 PointLight {
268 color: Color::srgb(0.0, 0.0, 1.0),
269 intensity: 1e6,
270 shadows_enabled: true,
271 ..default()
272 },
273 PointLightTexture {
274 image: asset_server.load("lightmaps/faces_pointlight_texture_blurred.png"),
275 cubemap_layout: CubemapLayout::CrossVertical,
276 },
277 )
278 ],
279 ));
280}
281
282fn spawn_buttons(commands: &mut Commands) {
284 commands.spawn((
287 widgets::main_ui_node(),
288 children![widgets::option_buttons(
289 "Drag to Move",
290 &[
291 (Selection::Camera, "Camera"),
292 (Selection::SpotLight, "Spotlight"),
293 (Selection::PointLight, "Point Light"),
294 (Selection::DirectionalLight, "Directional Light"),
295 ],
296 )],
297 ));
298
299 commands.spawn((
302 Node {
303 flex_direction: FlexDirection::Row,
304 position_type: PositionType::Absolute,
305 right: px(10),
306 bottom: px(10),
307 column_gap: px(6),
308 ..default()
309 },
310 children![
311 widgets::option_buttons(
312 "",
313 &[
314 (Visibility::Inherited, "Show"),
315 (Visibility::Hidden, "Hide"),
316 ],
317 ),
318 (drag_button("Scale"), DragMode::Scale),
319 (drag_button("Roll"), DragMode::Roll),
320 ],
321 ));
322}
323
324fn drag_button(label: &str) -> impl Bundle {
326 (
327 Node {
328 border: BUTTON_BORDER,
329 justify_content: JustifyContent::Center,
330 align_items: AlignItems::Center,
331 padding: BUTTON_PADDING,
332 ..default()
333 },
334 Button,
335 BackgroundColor(Color::BLACK),
336 BorderRadius::all(BUTTON_BORDER_RADIUS_SIZE),
337 BUTTON_BORDER_COLOR,
338 children![widgets::ui_text(label, Color::WHITE),],
339 )
340}
341
342fn spawn_help_text(commands: &mut Commands, app_status: &AppStatus) {
344 commands.spawn((
345 Text::new(create_help_string(app_status)),
346 Node {
347 position_type: PositionType::Absolute,
348 top: px(12),
349 left: px(12),
350 ..default()
351 },
352 HelpText,
353 ));
354}
355
356fn draw_gizmos(mut gizmos: Gizmos, spotlight: Query<(&GlobalTransform, &SpotLight, &Visibility)>) {
358 if let Ok((global_transform, spotlight, visibility)) = spotlight.single()
359 && visibility != Visibility::Hidden
360 {
361 gizmos.primitive_3d(
362 &Cone::new(7.0 * spotlight.outer_angle, 7.0),
363 Isometry3d {
364 rotation: global_transform.rotation() * Quat::from_rotation_x(FRAC_PI_2),
365 translation: global_transform.translation_vec3a() * 0.5,
366 },
367 YELLOW,
368 );
369 }
370}
371
372fn rotate_cube(mut meshes: Query<&mut Transform, With<Rotate>>) {
374 for mut transform in &mut meshes {
375 transform.rotate_y(CUBE_ROTATION_SPEED);
376 }
377}
378
379fn hide_shadows(
381 mut commands: Commands,
382 meshes: Query<Entity, (With<Mesh3d>, Without<NotShadowCaster>, Without<Rotate>)>,
383) {
384 for ent in &meshes {
385 commands.entity(ent).insert(NotShadowCaster);
386 }
387}
388
389fn update_radio_buttons(
391 mut widgets: Query<(
392 Entity,
393 Option<&mut BackgroundColor>,
394 Has<Text>,
395 &WidgetClickSender<Selection>,
396 )>,
397 app_status: Res<AppStatus>,
398 mut writer: TextUiWriter,
399 visible: Query<(&Visibility, &Selection)>,
400 mut visibility_widgets: Query<
401 (
402 Entity,
403 Option<&mut BackgroundColor>,
404 Has<Text>,
405 &WidgetClickSender<Visibility>,
406 ),
407 Without<WidgetClickSender<Selection>>,
408 >,
409) {
410 for (entity, maybe_bg_color, has_text, sender) in &mut widgets {
411 let selected = app_status.selection == **sender;
412 if let Some(mut bg_color) = maybe_bg_color {
413 widgets::update_ui_radio_button(&mut bg_color, selected);
414 }
415 if has_text {
416 widgets::update_ui_radio_button_text(entity, &mut writer, selected);
417 }
418 }
419
420 let visibility = visible
421 .iter()
422 .filter(|(_, selection)| **selection == app_status.selection)
423 .map(|(visibility, _)| *visibility)
424 .next()
425 .unwrap_or_default();
426 for (entity, maybe_bg_color, has_text, sender) in &mut visibility_widgets {
427 if let Some(mut bg_color) = maybe_bg_color {
428 widgets::update_ui_radio_button(&mut bg_color, **sender == visibility);
429 }
430 if has_text {
431 widgets::update_ui_radio_button_text(entity, &mut writer, **sender == visibility);
432 }
433 }
434}
435
436fn handle_selection_change(
438 mut events: MessageReader<WidgetClickEvent<Selection>>,
439 mut app_status: ResMut<AppStatus>,
440) {
441 for event in events.read() {
442 app_status.selection = **event;
443 }
444}
445
446fn toggle_visibility(
447 mut events: MessageReader<WidgetClickEvent<Visibility>>,
448 app_status: Res<AppStatus>,
449 mut visibility: Query<(&mut Visibility, &Selection)>,
450) {
451 if let Some(vis) = events.read().last() {
452 for (mut visibility, selection) in visibility.iter_mut() {
453 if selection == &app_status.selection {
454 *visibility = **vis;
455 }
456 }
457 }
458}
459
460fn process_move_input(
462 mut selections: Query<(&mut Transform, &Selection)>,
463 mouse_buttons: Res<ButtonInput<MouseButton>>,
464 mouse_motion: Res<AccumulatedMouseMotion>,
465 app_status: Res<AppStatus>,
466) {
467 if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Move {
469 return;
470 }
471
472 for (mut transform, selection) in &mut selections {
473 if app_status.selection != *selection {
474 continue;
475 }
476
477 if *selection == Selection::PointLight {
479 transform.translation +=
480 (mouse_motion.delta * Vec2::new(1.0, -1.0) * MOVE_SPEED).extend(0.0);
481 return;
482 }
483
484 let position = transform.translation;
485
486 let radius = position.length();
488 let mut theta = acos(position.y / radius);
489 let mut phi = position.z.signum() * acos(position.x * position.xz().length_recip());
490
491 let (phi_factor, theta_factor) = match *selection {
493 Selection::Camera => (1.0, -1.0),
494 _ => (-1.0, 1.0),
495 };
496
497 phi += phi_factor * mouse_motion.delta.x * MOVE_SPEED;
499 theta = f32::clamp(
500 theta + theta_factor * mouse_motion.delta.y * MOVE_SPEED,
501 0.001,
502 PI - 0.001,
503 );
504
505 transform.translation =
507 radius * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
508
509 let roll = transform.rotation.to_euler(EulerRot::YXZ).2;
511 transform.look_at(Vec3::ZERO, Vec3::Y);
512 let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
513 transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
514 }
515}
516
517fn process_scale_input(
519 mut scale_selections: Query<(&mut Transform, &Selection)>,
520 mut spotlight_selections: Query<(&mut SpotLight, &Selection)>,
521 mouse_buttons: Res<ButtonInput<MouseButton>>,
522 mouse_motion: Res<AccumulatedMouseMotion>,
523 app_status: Res<AppStatus>,
524) {
525 if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Scale {
527 return;
528 }
529
530 for (mut transform, selection) in &mut scale_selections {
531 if app_status.selection == *selection {
532 transform.scale = (transform.scale * (1.0 + mouse_motion.delta.x * SCALE_SPEED))
533 .clamp(Vec3::splat(0.01), Vec3::splat(5.0));
534 }
535 }
536
537 for (mut spotlight, selection) in &mut spotlight_selections {
538 if app_status.selection == *selection {
539 spotlight.outer_angle = (spotlight.outer_angle
540 * (1.0 + mouse_motion.delta.x * SCALE_SPEED))
541 .clamp(0.01, FRAC_PI_4);
542 spotlight.inner_angle = spotlight.outer_angle;
543 }
544 }
545}
546
547fn process_roll_input(
550 mut selections: Query<(&mut Transform, &Selection)>,
551 mouse_buttons: Res<ButtonInput<MouseButton>>,
552 mouse_motion: Res<AccumulatedMouseMotion>,
553 app_status: Res<AppStatus>,
554) {
555 if !mouse_buttons.pressed(MouseButton::Left) || app_status.drag_mode != DragMode::Roll {
557 return;
558 }
559
560 for (mut transform, selection) in &mut selections {
561 if app_status.selection != *selection {
562 continue;
563 }
564
565 let (yaw, pitch, mut roll) = transform.rotation.to_euler(EulerRot::YXZ);
566 roll += mouse_motion.delta.x * ROLL_SPEED;
567 transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
568 }
569}
570
571fn create_help_string(app_status: &AppStatus) -> String {
573 format!(
574 "Click and drag to {} {}",
575 app_status.drag_mode, app_status.selection
576 )
577}
578
579fn switch_drag_mode(
585 mut commands: Commands,
586 mut interactions: Query<(&Interaction, &DragMode)>,
587 mut windows: Query<Entity, With<Window>>,
588 mouse_buttons: Res<ButtonInput<MouseButton>>,
589 mut app_status: ResMut<AppStatus>,
590) {
591 if mouse_buttons.pressed(MouseButton::Left) {
592 return;
593 }
594
595 for (interaction, drag_mode) in &mut interactions {
596 if *interaction != Interaction::Hovered {
597 continue;
598 }
599
600 app_status.drag_mode = *drag_mode;
601
602 for window in &mut windows {
604 commands
605 .entity(window)
606 .insert(CursorIcon::from(SystemCursorIcon::EwResize));
607 }
608 return;
609 }
610
611 app_status.drag_mode = DragMode::Move;
612
613 for window in &mut windows {
614 commands.entity(window).remove::<CursorIcon>();
615 }
616}
617
618fn update_help_text(mut help_text: Query<&mut Text, With<HelpText>>, app_status: Res<AppStatus>) {
621 for mut text in &mut help_text {
622 text.0 = create_help_string(&app_status);
623 }
624}
625
626fn update_button_visibility(
629 mut nodes: Query<&mut Visibility, Or<(With<DragMode>, With<WidgetClickSender<Visibility>>)>>,
630 app_status: Res<AppStatus>,
631) {
632 for mut visibility in &mut nodes {
633 *visibility = match app_status.selection {
634 Selection::Camera => Visibility::Hidden,
635 _ => Visibility::Visible,
636 };
637 }
638}
639
640fn update_directional_light(
641 mut commands: Commands,
642 asset_server: Res<AssetServer>,
643 selections: Query<(&Selection, &Visibility)>,
644 mut light: Query<(
645 Entity,
646 &mut DirectionalLight,
647 Option<&DirectionalLightTexture>,
648 )>,
649) {
650 let directional_visible = selections
651 .iter()
652 .filter(|(selection, _)| **selection == Selection::DirectionalLight)
653 .any(|(_, visibility)| visibility != Visibility::Hidden);
654 let any_texture_light_visible = selections
655 .iter()
656 .filter(|(selection, _)| {
657 **selection == Selection::PointLight || **selection == Selection::SpotLight
658 })
659 .any(|(_, visibility)| visibility != Visibility::Hidden);
660
661 let (entity, mut light, maybe_texture) = light
662 .single_mut()
663 .expect("there should be a single directional light");
664
665 if directional_visible {
666 light.illuminance = AMBIENT_DAYLIGHT;
667 if maybe_texture.is_none() {
668 commands.entity(entity).insert(DirectionalLightTexture {
669 image: asset_server.load("lightmaps/caustic_directional_texture.png"),
670 tiled: true,
671 });
672 }
673 } else if any_texture_light_visible {
674 light.illuminance = CLEAR_SUNRISE;
675 if maybe_texture.is_some() {
676 commands.entity(entity).remove::<DirectionalLightTexture>();
677 }
678 } else {
679 light.illuminance = AMBIENT_DAYLIGHT;
680 if maybe_texture.is_some() {
681 commands.entity(entity).remove::<DirectionalLightTexture>();
682 }
683 }
684}