bevy_sun_gizmo/
lib.rs

1#![warn(missing_docs)]
2#![doc = include_str!("../README.md")]
3
4use bevy::{input::mouse::MouseMotion, prelude::*, transform::TransformSystem};
5use std::f32::consts::{FRAC_PI_2, PI, TAU};
6
7/// A plugin that adds a controller and gizmo for controlling directional lights
8pub struct SunGizmoPlugin {
9    /// Position of the gizmo in screen space. [0.0, 0.0] is top left, [1.0, 1.0] is bottom right
10    pub position: Vec2,
11    /// Size of the gizmo on screen
12    pub size: f32,
13    /// Time in seconds the gizmo persists after being activated by the user
14    pub persist_time: f32,
15    /// The combination of keys that will activate the gizmo
16    pub key_bindings: Vec<KeyCode>,
17    /// The sensitivity of the mouse controller
18    pub sensitivity: f32,
19    /// Line width of the gizmo
20    pub line_width: f32,
21}
22
23impl Default for SunGizmoPlugin {
24    fn default() -> Self {
25        Self {
26            position: Vec2::new(0.7, 0.7),
27            size: 0.1,
28            persist_time: 5.0,
29            key_bindings: vec![KeyCode::ControlRight, KeyCode::KeyL],
30            sensitivity: 0.5,
31            line_width: 4.0,
32        }
33    }
34}
35
36impl Plugin for SunGizmoPlugin {
37    fn build(&self, app: &mut App) {
38        app.add_systems(Update, update_sun)
39            .add_systems(
40                PostUpdate,
41                draw_sun_gizmo.after(TransformSystem::TransformPropagate),
42            )
43            .add_event::<SunUpdatedEvent>()
44            .insert_resource(SunGizmoControlConfig {
45                key_bindings: self.key_bindings.clone(),
46                sensitivity: self.sensitivity,
47            })
48            .insert_gizmo_group(
49                SunGizmoGroup {
50                    pos: self.position,
51                    size: self.size,
52                    persist_time: self.persist_time,
53                },
54                GizmoConfig {
55                    line_width: self.line_width,
56                    depth_bias: -1.0,
57                    ..default()
58                },
59            );
60    }
61}
62
63#[derive(Resource)]
64struct SunGizmoControlConfig {
65    pub key_bindings: Vec<KeyCode>,
66    pub sensitivity: f32,
67}
68
69#[derive(Default, Reflect, GizmoConfigGroup)]
70struct SunGizmoGroup {
71    pub pos: Vec2,
72    pub size: f32,
73    pub persist_time: f32,
74}
75
76#[derive(Event)]
77struct SunUpdatedEvent;
78
79fn update_sun(
80    mut query: Query<&mut Transform, With<DirectionalLight>>,
81    mut mouse: EventReader<MouseMotion>,
82    mut event_writer: EventWriter<SunUpdatedEvent>,
83    input: Res<ButtonInput<KeyCode>>,
84    config: Res<SunGizmoControlConfig>,
85    time: Res<Time>,
86) {
87    let Ok(mut transform) = query.get_single_mut() else {
88        return;
89    };
90
91    if input.all_pressed(config.key_bindings.clone()) {
92        let mut delta = Vec2::ZERO;
93        for event in mouse.read() {
94            delta += event.delta * config.sensitivity;
95        }
96
97        let target: Vec3 = transform.up().into();
98        let right: Vec3 = transform.right().into();
99        let forward: Vec3 = transform.forward().into();
100        let angle_between = forward.angle_between(target);
101        transform.rotate_axis(right, angle_between.min(-delta.y * time.delta_seconds()));
102        transform.rotate_axis(Vec3::Y, delta.x * time.delta_seconds());
103
104        event_writer.send(SunUpdatedEvent);
105    }
106}
107
108fn draw_sun_gizmo(
109    directional_light: Query<&mut Transform, With<DirectionalLight>>,
110    camera: Query<(&Camera, &GlobalTransform)>,
111    mut gizmos: Gizmos<SunGizmoGroup>,
112    mut event_reader: EventReader<SunUpdatedEvent>,
113    mut persist_time: Local<f32>,
114    time: Res<Time>,
115) {
116    if !event_reader.is_empty() {
117        *persist_time = gizmos.config_ext.persist_time;
118        event_reader.clear();
119    }
120
121    if *persist_time < 0.0 {
122        return;
123    }
124
125    *persist_time -= time.delta_seconds();
126
127    let Ok(directional_light) = directional_light.get_single() else {
128        return;
129    };
130    let Ok((camera, camera_transform)) = camera.get_single() else {
131        return;
132    };
133    let Some(viewport_size) = camera.logical_viewport_size() else {
134        return;
135    };
136
137    let viewport_pos = viewport_size * gizmos.config_ext.pos;
138
139    let Some(ray) = camera.viewport_to_world(camera_transform, viewport_pos) else {
140        return;
141    };
142
143    let size = gizmos.config_ext.size;
144    let origin = ray.get_point(1.0 / size);
145    let x_color = Color::rgba(1.0, 0.4, 0.4, 1.0);
146    let y_color = Color::rgba(0.4, 1.0, 0.4, 1.0);
147    let z_color = Color::rgba(0.4, 0.4, 1.0, 1.0);
148
149    // XZ Arc
150    let y_forward_roation = Quat::IDENTITY;
151    gizmos.arc_3d(TAU, 1.0, origin, y_forward_roation, y_color);
152    gizmos.arc_3d(TAU, 0.8, origin, y_forward_roation, y_color.with_a(0.4));
153    gizmos.arc_3d(TAU, 0.6, origin, y_forward_roation, y_color.with_a(0.25));
154    gizmos.arc_3d(TAU, 0.4, origin, y_forward_roation, y_color.with_a(0.1));
155
156    // YZ Arc
157    let x_forward_rotation =
158        Quat::from_rotation_arc(Vec3::Y, Vec3::X) * Quat::from_rotation_y(FRAC_PI_2);
159    gizmos.arc_3d(PI, 1.0, origin, x_forward_rotation, x_color);
160    // XY Arc
161    let z_forward_rotation = Quat::from_rotation_arc(Vec3::Y, Vec3::Z);
162    gizmos.arc_3d(PI, 1.0, origin, z_forward_rotation, z_color);
163
164    gizmos.arrow(origin, origin + Vec3::X, x_color);
165    gizmos.arrow(origin, origin + Vec3::Y, y_color);
166    gizmos.arrow(origin, origin + Vec3::Z, z_color);
167
168    let light_dir = *directional_light.forward();
169    let start = origin - light_dir * 1.2;
170    let end = origin - light_dir * 0.2;
171    gizmos.arrow(start, end, Color::YELLOW);
172
173    let projected_start = Vec3::new(start.x, origin.y, start.z);
174    let x_axis_proj = Vec3::new(start.x, origin.y, origin.z);
175    let z_axis_proj = Vec3::new(origin.x, origin.y, start.z);
176    gizmos.line(start, projected_start, y_color);
177    gizmos.line(projected_start, x_axis_proj, z_color);
178    gizmos.line(projected_start, z_axis_proj, x_color);
179}