light_textures/
light_textures.rs

1//! Demonstrates light textures, which modulate light sources.
2
3use 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
26/// The speed at which the cube rotates, in radians per frame.
27const CUBE_ROTATION_SPEED: f32 = 0.02;
28
29/// The speed at which the selection can be moved, in spherical coordinate
30/// radians per mouse unit.
31const MOVE_SPEED: f32 = 0.008;
32/// The speed at which the selection can be scaled, in reciprocal mouse units.
33const SCALE_SPEED: f32 = 0.05;
34/// The speed at which the selection can be scaled, in radians per mouse unit.
35const ROLL_SPEED: f32 = 0.01;
36
37/// Various settings for the demo.
38#[derive(Resource, Default)]
39struct AppStatus {
40    /// The object that will be moved, scaled, or rotated when the mouse is
41    /// dragged.
42    selection: Selection,
43    /// What happens when the mouse is dragged: one of a move, rotate, or scale
44    /// operation.
45    drag_mode: DragMode,
46}
47
48/// The object that will be moved, scaled, or rotated when the mouse is dragged.
49#[derive(Clone, Copy, Component, Default, PartialEq)]
50enum Selection {
51    /// The camera.
52    ///
53    /// The camera can only be moved, not scaled or rotated.
54    #[default]
55    Camera,
56    /// The spotlight, which uses a torch-like light texture
57    SpotLight,
58    /// The point light, which uses a light texture cubemap constructed from the faces mesh
59    PointLight,
60    /// The directional light, which uses a caustic-like texture
61    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/// What happens when the mouse is dragged: one of a move, rotate, or scale
76/// operation.
77#[derive(Clone, Copy, Component, Default, PartialEq, Debug)]
78enum DragMode {
79    /// The mouse moves the current selection.
80    #[default]
81    Move,
82    /// The mouse scales the current selection.
83    ///
84    /// This only applies to decals, not cameras.
85    Scale,
86    /// The mouse rotates the current selection around its local Z axis.
87    ///
88    /// This only applies to decals, not cameras.
89    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/// A marker component for the help text in the top left corner of the window.
103#[derive(Clone, Copy, Component)]
104struct HelpText;
105
106/// Entry point.
107fn 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
142/// Creates the scene.
143fn 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    // Error out if clustered decals (and so light textures) aren't supported on the current platform.
153    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
169/// Spawns the cube onto which the decals are projected.
170fn spawn_cubes(
171    commands: &mut Commands,
172    meshes: &mut Assets<Mesh>,
173    materials: &mut Assets<StandardMaterial>,
174) {
175    // Rotate the cube a bit just to make it more interesting.
176    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
199/// Spawns the directional light.
200fn 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
219/// Spawns the camera.
220fn 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        // Tag the camera with `Selection::Camera`.
225        .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
282/// Spawns the buttons at the bottom of the screen.
283fn spawn_buttons(commands: &mut Commands) {
284    // Spawn the radio buttons that allow the user to select an object to
285    // control.
286    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    // Spawn the drag buttons that allow the user to control the scale and roll
300    // of the selected object.
301    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
324/// Spawns a button that the user can drag to change a parameter.
325fn 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
342/// Spawns the help text at the top of the screen.
343fn 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
356/// Draws the outlines that show the bounds of the spotlight.
357fn 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
372/// Rotates the cube a bit every frame.
373fn 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
379/// Hide shadows on all meshes except the main cube
380fn 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
389/// Updates the state of the radio buttons when the user clicks on one.
390fn 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
436/// Changes the selection when the user clicks a radio button.
437fn 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
460/// Process a drag event that moves the selected object.
461fn 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    // Only process drags when movement is selected.
468    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        // use simple movement for the point light
478        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        // Convert to spherical coordinates.
487        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        // Camera movement is the inverse of object movement.
492        let (phi_factor, theta_factor) = match *selection {
493            Selection::Camera => (1.0, -1.0),
494            _ => (-1.0, 1.0),
495        };
496
497        // Adjust the spherical coordinates. Clamp the inclination to (0, π).
498        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        // Convert spherical coordinates back to Cartesian coordinates.
506        transform.translation =
507            radius * vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
508
509        // Look at the center, but preserve the previous roll angle.
510        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
517/// Processes a drag event that scales the selected target.
518fn 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    // Only process drags when the scaling operation is selected.
526    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
547/// Processes a drag event that rotates the selected target along its local Z
548/// axis.
549fn 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    // Only process drags when the rolling operation is selected.
556    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
571/// Creates the help string at the top left of the screen.
572fn 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
579/// Changes the drag mode when the user hovers over the "Scale" and "Roll"
580/// buttons in the lower right.
581///
582/// If the user is hovering over no such button, this system changes the drag
583/// mode back to its default value of [`DragMode::Move`].
584fn 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        // Set the cursor to provide the user with a nice visual hint.
603        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
618/// Updates the help text in the top left of the screen to reflect the current
619/// selection and drag mode.
620fn 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
626/// Updates the visibility of the drag mode buttons so that they aren't visible
627/// if the camera is selected.
628fn 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}