1use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, PI};
4use std::fmt::{self, Formatter};
5
6use bevy::ui_widgets::radio_self_update;
7use bevy::{
8 camera::primitives::CubemapLayout,
9 color::palettes::css::{SILVER, YELLOW},
10 feathers::{
11 controls::{FeathersNumberInput, FeathersRadio, NumberInputPrecision, NumberInputValue},
12 theme::UiTheme,
13 FeathersPlugins,
14 },
15 input::mouse::AccumulatedMouseMotion,
16 light::{DirectionalLightTexture, NotShadowCaster, PointLightTexture, SpotLightTexture},
17 pbr::decal,
18 prelude::*,
19 render::renderer::{RenderAdapter, RenderDevice},
20 ui::Checked,
21 ui_widgets::ValueChange,
22};
23use light_consts::lux::{AMBIENT_DAYLIGHT, CLEAR_SUNRISE};
24use number_input_f32::number_input_f32;
25use ops::{acos, cos, sin};
26use radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
27
28#[path = "../helpers/radio.rs"]
29mod radio;
30
31#[path = "../helpers/number_input_f32.rs"]
32mod number_input_f32;
33
34#[path = "../helpers/theme.rs"]
35mod theme;
36
37const CUBE_ROTATION_SPEED: f32 = 0.02;
39
40const MOVE_SPEED: f32 = 0.008;
43
44#[derive(Resource, Default)]
46struct AppStatus {
47 selection: Selection,
50}
51
52#[derive(Clone, Copy, Component, Default, PartialEq)]
54enum Selection {
55 #[default]
59 Camera,
60 SpotLight,
62 PointLight,
64 DirectionalLight,
66}
67
68impl fmt::Display for Selection {
69 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
70 match *self {
71 Selection::Camera => f.write_str("camera"),
72 Selection::SpotLight => f.write_str("spotlight"),
73 Selection::PointLight => f.write_str("point light"),
74 Selection::DirectionalLight => f.write_str("directional light"),
75 }
76 }
77}
78
79#[derive(Clone, Copy, Component, Default, PartialEq, Debug)]
81enum AppNumberInput {
82 #[default]
86 Scale,
87 Roll,
91}
92
93#[derive(Clone, Copy, Component, Default, PartialEq, Debug)]
95struct BaseScale(Vec3);
96
97#[derive(Clone, Copy, Component)]
99struct HelpText;
100
101fn main() {
103 App::new()
104 .add_plugins((
105 DefaultPlugins.set(WindowPlugin {
106 primary_window: Some(Window {
107 title: "Bevy Light Textures Example".into(),
108 ..default()
109 }),
110 ..default()
111 }),
112 FeathersPlugins,
113 ))
114 .insert_resource(UiTheme(theme::basic_example_theme(Color::WHITE)))
115 .init_resource::<AppStatus>()
116 .add_systems(Startup, setup)
117 .add_systems(Update, draw_gizmos)
118 .add_systems(Update, rotate_cube)
119 .add_systems(Update, hide_shadows)
120 .add_observer(handle_selection_change)
121 .add_observer(handle_visibility_change)
122 .add_observer(radio_self_update)
123 .add_observer(handle_value_change_number_input)
124 .add_observer(handle_drag_as_movement)
125 .add_systems(Update, update_directional_light)
126 .add_systems(Update, update_help_text)
127 .run();
128}
129
130fn setup(
132 mut commands: Commands,
133 asset_server: Res<AssetServer>,
134 app_status: Res<AppStatus>,
135 render_device: Res<RenderDevice>,
136 render_adapter: Res<RenderAdapter>,
137 mut meshes: ResMut<Assets<Mesh>>,
138 mut materials: ResMut<Assets<StandardMaterial>>,
139) {
140 if !decal::clustered::clustered_decals_are_usable(&render_device, &render_adapter) {
142 error!("Light textures aren't usable on this platform.");
143 commands.write_message(AppExit::error());
144 }
145
146 spawn_cubes(&mut commands, &mut meshes, &mut materials);
147 spawn_camera(&mut commands);
148 spawn_light(&mut commands, &asset_server);
149 spawn_buttons(&mut commands);
150 spawn_help_text(&mut commands, &app_status);
151 spawn_light_textures(&mut commands, &asset_server, &mut meshes, &mut materials);
152}
153
154#[derive(Component)]
155struct Rotate;
156
157fn spawn_cubes(
159 commands: &mut Commands,
160 meshes: &mut Assets<Mesh>,
161 materials: &mut Assets<StandardMaterial>,
162) {
163 let mut transform = Transform::IDENTITY;
165 transform.rotate_y(FRAC_PI_3);
166
167 commands.spawn((
168 Mesh3d(meshes.add(Cuboid::new(3.0, 3.0, 3.0))),
169 MeshMaterial3d(materials.add(StandardMaterial {
170 base_color: SILVER.into(),
171 ..default()
172 })),
173 transform,
174 Rotate,
175 ));
176
177 commands.spawn((
178 Mesh3d(meshes.add(Cuboid::new(-13.0, -13.0, -13.0))),
179 MeshMaterial3d(materials.add(StandardMaterial {
180 base_color: SILVER.into(),
181 ..default()
182 })),
183 transform,
184 ));
185}
186
187fn spawn_light(commands: &mut Commands, asset_server: &AssetServer) {
189 commands.spawn((
190 Visibility::Hidden,
191 Transform::from_xyz(8.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y),
192 BaseScale(Vec3::ONE),
193 Selection::DirectionalLight,
194 children![(
195 DirectionalLight {
196 illuminance: AMBIENT_DAYLIGHT,
197 ..default()
198 },
199 DirectionalLightTexture {
200 image: asset_server.load("lightmaps/caustic_directional_texture.png"),
201 tiled: true,
202 },
203 Visibility::Visible,
204 )],
205 ));
206}
207
208fn spawn_camera(commands: &mut Commands) {
210 commands
211 .spawn(Camera3d::default())
212 .insert(Transform::from_xyz(0.0, 2.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y))
213 .insert(Selection::Camera);
215}
216
217fn spawn_light_textures(
218 commands: &mut Commands,
219 asset_server: &AssetServer,
220 meshes: &mut Assets<Mesh>,
221 materials: &mut Assets<StandardMaterial>,
222) {
223 commands.spawn((
224 SpotLight {
225 color: Color::srgb(1.0, 1.0, 0.8),
226 intensity: 10e6,
227 outer_angle: 0.25,
228 inner_angle: 0.25,
229 shadow_maps_enabled: true,
230 ..default()
231 },
232 Transform::from_translation(Vec3::new(6.0, 1.0, 2.0)).looking_at(Vec3::ZERO, Vec3::Y),
233 BaseScale(Vec3::ONE),
234 SpotLightTexture {
235 image: asset_server.load("lightmaps/torch_spotlight_texture.png"),
236 },
237 Visibility::Inherited,
238 Selection::SpotLight,
239 ));
240
241 commands.spawn((
242 Visibility::Hidden,
243 Transform::from_translation(Vec3::new(0.0, 1.8, 0.01)).with_scale(Vec3::splat(0.25)),
244 BaseScale(Vec3::splat(0.25)),
245 Selection::PointLight,
246 children![
247 WorldAssetRoot(
248 asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/Faces/faces.glb")),
249 ),
250 (
251 Mesh3d(meshes.add(Sphere::new(1.0))),
252 MeshMaterial3d(materials.add(StandardMaterial {
253 emissive: Color::srgb(0.0, 0.0, 300.0).to_linear(),
254 ..default()
255 })),
256 ),
257 (
258 PointLight {
259 color: Color::srgb(0.0, 0.0, 1.0),
260 intensity: 1e6,
261 shadow_maps_enabled: true,
262 ..default()
263 },
264 PointLightTexture {
265 image: asset_server.load("lightmaps/faces_pointlight_texture_blurred.png"),
266 cubemap_layout: CubemapLayout::CrossVertical,
267 },
268 )
269 ],
270 ));
271}
272
273fn spawn_buttons(commands: &mut Commands) {
275 commands.spawn_scene(bsn! {
276 @main_ui_node_scene()
277 Children [
278 @feathers_option_buttons(
279 "Drag to Move",
280 &[
281 (Selection::Camera, "Camera"),
282 (Selection::SpotLight, "Spotlight"),
283 (Selection::PointLight, "Point Light"),
284 (Selection::DirectionalLight, "Directional Light"),
285 ],
286 0,
287 )
288 --
289 Visibility::Hidden
291 @feathers_option_buttons(
292 "Visibility",
293 &[
294 (Visibility::Inherited, "Show"),
295 (Visibility::Hidden, "Hide"),
296 ],
297 0,
298 )
299 --
300 Visibility::Hidden
302 @number_input_f32("Scale Multiplier", Some(AppNumberInput::Scale), 1.0, NumberInputPrecision(2), 0.01..=5.)
303 --
304 Visibility::Hidden
305 @number_input_f32("Roll (-π to π)", Some(AppNumberInput::Roll), 0.0, NumberInputPrecision(2), -PI + f32::EPSILON ..=PI.next_down())
307 ]
308 });
309}
310
311fn handle_value_change_number_input(
314 value_change: On<ValueChange<f32>>,
315 mut commands: Commands,
316 number_input_q: Query<&AppNumberInput, With<FeathersNumberInput>>,
317 app_status: ResMut<AppStatus>,
318 mut selections: Query<(
319 &mut Transform,
320 Option<&mut SpotLight>,
321 &BaseScale,
322 &Selection,
323 )>,
324) {
325 if app_status.selection == Selection::Camera {
326 return;
327 }
328 if let Ok(app_number_input) = number_input_q.get(value_change.source) {
329 for (mut transform, spotlight_option, base_scale, selection) in &mut selections {
330 if app_status.selection != *selection {
331 continue;
332 }
333 match app_number_input {
334 AppNumberInput::Scale => {
335 transform.scale = base_scale.0 * value_change.value;
336
337 if let Some(mut spotlight) = spotlight_option {
338 spotlight.outer_angle = (0.25 * value_change.value).clamp(0.01, FRAC_PI_4);
340 spotlight.inner_angle = spotlight.outer_angle;
341 }
342 }
343 AppNumberInput::Roll => {
344 let (yaw, pitch, mut _roll) = transform.rotation.to_euler(EulerRot::YXZ);
345 transform.rotation =
347 Quat::from_euler(EulerRot::YXZ, yaw, pitch, value_change.value);
348 }
349 }
350 }
351 commands
352 .entity(value_change.source)
353 .insert(NumberInputValue::F32(value_change.value));
354 }
355}
356
357fn handle_selection_change(
361 event: On<ValueChange<Entity>>,
362 new_value_query: Query<&RadioButtonOptionValue<Selection>>,
363 mut app_status: ResMut<AppStatus>,
364 mut commands: Commands,
365 selections: Query<(&Transform, &BaseScale, &Visibility, &Selection)>,
366 visibility_radio: Query<
367 (Entity, &ChildOf, &RadioButtonOptionValue<Visibility>),
368 With<FeathersRadio>,
369 >,
370 number_inputs: Query<(Entity, &ChildOf, &AppNumberInput)>,
371) {
372 let Ok(RadioButtonOptionValue(selection)) = new_value_query.get(event.value) else {
373 return;
374 };
375 app_status.selection = *selection;
376
377 if app_status.selection == Selection::Camera
379 && let Some((_, child_of, _)) = visibility_radio.iter().next()
380 {
381 commands
382 .entity(child_of.parent())
383 .insert(Visibility::Hidden);
384 } else if let Some((_, child_of, _)) = visibility_radio.iter().next() {
385 commands
386 .entity(child_of.parent())
387 .insert(Visibility::Inherited);
388
389 for (_transform, _base_scale, selected_visibility, _selection) in selections
391 .iter()
392 .filter(|&(_, _, _, selection)| *selection == app_status.selection)
393 {
394 for (entity, _, visibility_option_value) in visibility_radio.iter() {
395 if visibility_option_value.0 == *selected_visibility {
396 commands.entity(entity).insert(Checked);
397 } else {
398 commands.entity(entity).remove::<Checked>();
399 }
400 }
401 }
402 }
403
404 for (input_entity, child_of, app_number_input) in number_inputs.iter() {
407 match app_status.selection {
408 Selection::Camera => {
409 commands
410 .entity(child_of.parent())
411 .insert(Visibility::Hidden);
412 }
413 _ => {
414 commands
415 .entity(child_of.parent())
416 .insert(Visibility::Inherited);
417
418 for (transform, base_scale, _, _) in selections
420 .iter()
421 .filter(|&(_, _, _, selection)| *selection == app_status.selection)
422 {
423 if AppNumberInput::Scale == *app_number_input {
424 let scale_multiplier = transform.scale.x / base_scale.0.x;
426 commands
427 .entity(input_entity)
428 .insert(NumberInputValue::F32(scale_multiplier));
429 } else {
430 let roll = transform.rotation.to_euler(EulerRot::YXZ).2;
431 commands
432 .entity(input_entity)
433 .insert(NumberInputValue::F32(roll));
434 }
435 }
436 }
437 };
438 }
439}
440
441fn handle_visibility_change(
445 event: On<ValueChange<Entity>>,
446 new_value_query: Query<&RadioButtonOptionValue<Visibility>>,
447 app_status: Res<AppStatus>,
448 mut visibility_q: Query<(&mut Visibility, &Selection)>,
449) {
450 let Ok(RadioButtonOptionValue(new_visibility)) = new_value_query.get(event.value) else {
451 return;
452 };
453
454 for (mut visibility, selection) in visibility_q.iter_mut() {
455 if *selection == app_status.selection {
456 *visibility = *new_visibility;
457 }
458 }
459}
460
461fn spawn_help_text(commands: &mut Commands, app_status: &AppStatus) {
463 commands.spawn((
464 Text::new(create_help_string(app_status)),
465 Node {
466 position_type: PositionType::Absolute,
467 top: px(12),
468 left: px(12),
469 ..default()
470 },
471 HelpText,
472 ));
473}
474
475fn draw_gizmos(mut gizmos: Gizmos, spotlight: Query<(&GlobalTransform, &SpotLight, &Visibility)>) {
477 if let Ok((global_transform, spotlight, visibility)) = spotlight.single()
478 && visibility != Visibility::Hidden
479 {
480 gizmos.primitive_3d(
481 &Cone::new(7.0 * spotlight.outer_angle, 7.0),
482 Isometry3d {
483 rotation: global_transform.rotation() * Quat::from_rotation_x(FRAC_PI_2),
484 translation: global_transform.translation_vec3a() * 0.5,
485 },
486 YELLOW,
487 );
488 }
489}
490
491fn rotate_cube(mut meshes: Query<&mut Transform, With<Rotate>>) {
493 for mut transform in &mut meshes {
494 transform.rotate_y(CUBE_ROTATION_SPEED);
495 }
496}
497
498fn hide_shadows(
500 mut commands: Commands,
501 meshes: Query<Entity, (With<Mesh3d>, Without<NotShadowCaster>, Without<Rotate>)>,
502) {
503 for ent in &meshes {
504 commands.entity(ent).insert(NotShadowCaster);
505 }
506}
507
508fn handle_drag_as_movement(
510 event: On<PointerDrag>,
511 parent_q: Query<&ChildOf>,
512 number_input_q: Query<(), With<FeathersNumberInput>>,
513 mut selections: Query<(&mut Transform, &Selection)>,
514 mouse_motion: Res<AccumulatedMouseMotion>,
515 app_status: Res<AppStatus>,
516) {
517 if parent_q
520 .iter_ancestors(event.entity)
521 .any(|parent| number_input_q.contains(parent))
522 {
523 return;
524 }
525
526 for (mut transform, selection) in &mut selections {
527 if app_status.selection != *selection {
528 continue;
529 }
530
531 if *selection == Selection::PointLight {
533 transform.translation +=
534 (mouse_motion.delta * Vec2::new(1.0, -1.0) * MOVE_SPEED).extend(0.0);
535 return;
536 }
537
538 let position = transform.translation;
539
540 let radius = position.length();
542 let mut theta = acos(position.y / radius);
543 let mut phi = position.z.signum() * acos(position.x * position.xz().length_recip());
544
545 let (phi_factor, theta_factor) = match *selection {
547 Selection::Camera => (1.0, -1.0),
548 _ => (-1.0, 1.0),
549 };
550
551 phi += phi_factor * mouse_motion.delta.x * MOVE_SPEED;
553 theta = f32::clamp(
554 theta + theta_factor * mouse_motion.delta.y * MOVE_SPEED,
555 0.001,
556 PI - 0.001,
557 );
558
559 transform.translation =
561 radius * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
562
563 let roll = transform.rotation.to_euler(EulerRot::YXZ).2;
565 transform.look_at(Vec3::ZERO, Vec3::Y);
566 let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
567 transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
568 }
569}
570
571fn create_help_string(app_status: &AppStatus) -> String {
573 if app_status.selection == Selection::Camera {
574 format!("Click and drag to move {}.", app_status.selection)
575 } else {
576 format!(
577 "Click and drag to move/scale/rotate {}.\n\
578 To scale/rotate, start the drag within the corresponding number input.\n\
579 To move, start the drag anywhere else in the example.",
580 app_status.selection
581 )
582 }
583}
584
585fn update_help_text(mut help_text: Query<&mut Text, With<HelpText>>, app_status: Res<AppStatus>) {
588 for mut text in &mut help_text {
589 text.0 = create_help_string(&app_status);
590 }
591}
592
593fn update_directional_light(
594 mut commands: Commands,
595 asset_server: Res<AssetServer>,
596 selections: Query<(&Selection, &Visibility)>,
597 mut light: Query<(
598 Entity,
599 &mut DirectionalLight,
600 Option<&DirectionalLightTexture>,
601 )>,
602) {
603 let directional_visible = selections
604 .iter()
605 .filter(|(selection, _)| **selection == Selection::DirectionalLight)
606 .any(|(_, visibility)| visibility != Visibility::Hidden);
607 let any_texture_light_visible = selections
608 .iter()
609 .filter(|(selection, _)| {
610 **selection == Selection::PointLight || **selection == Selection::SpotLight
611 })
612 .any(|(_, visibility)| visibility != Visibility::Hidden);
613
614 let (entity, mut light, maybe_texture) = light
615 .single_mut()
616 .expect("there should be a single directional light");
617
618 if directional_visible {
619 light.illuminance = AMBIENT_DAYLIGHT;
620 if maybe_texture.is_none() {
621 commands.entity(entity).insert(DirectionalLightTexture {
622 image: asset_server.load("lightmaps/caustic_directional_texture.png"),
623 tiled: true,
624 });
625 }
626 } else if any_texture_light_visible {
627 light.illuminance = CLEAR_SUNRISE;
628 if maybe_texture.is_some() {
629 commands.entity(entity).remove::<DirectionalLightTexture>();
630 }
631 } else {
632 light.illuminance = AMBIENT_DAYLIGHT;
633 if maybe_texture.is_some() {
634 commands.entity(entity).remove::<DirectionalLightTexture>();
635 }
636 }
637}