viewport_lib/camera/controllers/
third_person.rs1use glam::{Vec2, Vec3};
16
17use super::look::{build_orientation, orientation_to_aim, yaw_pitch_from_orientation};
18use crate::Camera;
19use crate::interaction::input::action_frame::ActionFrame;
20
21pub struct ThirdPersonCameraController {
28 pub yaw: f32,
30 pub pitch: f32,
33 pub yaw_sensitivity: f32,
35 pub pitch_sensitivity: f32,
37 pub pitch_clamp: (f32, f32),
41 pub distance: f32,
44 pub height: f32,
47 pub shoulder_offset: Vec2,
50}
51
52impl ThirdPersonCameraController {
53 pub const DEFAULT_DISTANCE: f32 = 6.0;
55 pub const DEFAULT_HEIGHT: f32 = 1.2;
57
58 pub fn new(yaw_sensitivity: f32, pitch_sensitivity: f32) -> Self {
62 Self {
63 yaw: 0.0,
64 pitch: 0.0,
65 yaw_sensitivity,
66 pitch_sensitivity,
67 pitch_clamp: (-std::f32::consts::FRAC_PI_4, std::f32::consts::FRAC_PI_4),
68 distance: Self::DEFAULT_DISTANCE,
69 height: Self::DEFAULT_HEIGHT,
70 shoulder_offset: Vec2::ZERO,
71 }
72 }
73
74 pub fn with_distance(mut self, distance: f32) -> Self {
76 self.distance = distance;
77 self
78 }
79
80 pub fn with_height(mut self, height: f32) -> Self {
82 self.height = height;
83 self
84 }
85
86 pub fn with_shoulder_offset(mut self, offset: Vec2) -> Self {
88 self.shoulder_offset = offset;
89 self
90 }
91
92 pub fn with_pitch_clamp(mut self, min: f32, max: f32) -> Self {
94 self.pitch_clamp = if min <= max { (min, max) } else { (max, min) };
95 self
96 }
97
98 pub fn apply(&mut self, camera: &mut Camera, frame: &ActionFrame, target: Vec3) {
103 let look = frame.navigation.orbit;
104 self.yaw += look.x * self.yaw_sensitivity;
105 self.pitch -= look.y * self.pitch_sensitivity;
106 self.pitch = self.pitch.clamp(self.pitch_clamp.0, self.pitch_clamp.1);
107
108 let orientation = build_orientation(self.yaw, self.pitch);
109
110 let mut pivot = target + Vec3::Z * self.height;
111 if self.shoulder_offset.x != 0.0 {
112 pivot += self.right_dir() * self.shoulder_offset.x;
113 }
114 if self.shoulder_offset.y != 0.0 {
115 pivot += Vec3::Z * self.shoulder_offset.y;
116 }
117
118 camera.center = pivot;
119 camera.distance = self.distance;
120 camera.orientation = orientation;
121 }
122
123 pub fn sync_from_camera(&mut self, camera: &Camera) {
127 let (yaw, pitch) = yaw_pitch_from_orientation(camera.orientation);
128 self.yaw = yaw;
129 self.pitch = pitch.clamp(self.pitch_clamp.0, self.pitch_clamp.1);
130 }
131
132 pub fn set_look(&mut self, yaw: f32, pitch: f32) {
134 self.yaw = yaw;
135 self.pitch = pitch.clamp(self.pitch_clamp.0, self.pitch_clamp.1);
136 }
137
138 pub fn forward_dir(&self) -> Vec3 {
140 let (s, c) = self.yaw.sin_cos();
141 Vec3::new(s, c, 0.0)
142 }
143
144 pub fn right_dir(&self) -> Vec3 {
146 self.forward_dir().cross(Vec3::Z).normalize_or_zero()
147 }
148
149 pub fn aim_dir(&self) -> Vec3 {
151 orientation_to_aim(build_orientation(self.yaw, self.pitch))
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 fn cam() -> ThirdPersonCameraController {
160 ThirdPersonCameraController::new(0.0035, 0.0030)
161 }
162
163 fn look(x: f32, y: f32) -> ActionFrame {
164 let mut f = ActionFrame::default();
165 f.navigation.orbit = Vec2::new(x, y);
166 f
167 }
168
169 fn approx_eq(a: Vec3, b: Vec3) -> bool {
170 (a - b).length() < 1e-4
171 }
172
173 #[test]
174 fn default_forward_is_plus_y() {
175 assert!(approx_eq(cam().forward_dir(), Vec3::Y));
176 }
177
178 #[test]
179 fn right_look_increases_yaw() {
180 let mut c = cam();
181 let mut camera = Camera::default();
182 c.apply(&mut camera, &look(10.0, 0.0), Vec3::ZERO);
183 assert!(c.yaw > 0.0);
184 }
185
186 #[test]
187 fn pitch_clamp_is_respected() {
188 let mut c = cam().with_pitch_clamp(-0.1, 0.5);
189 let mut camera = Camera::default();
190 c.apply(&mut camera, &look(0.0, 100.0), Vec3::ZERO);
191 assert!((c.pitch - -0.1).abs() < 1e-5, "pitch={}", c.pitch);
192 }
193
194 #[test]
195 fn pivot_is_above_target_and_boom_pulls_back() {
196 let mut c = cam().with_distance(6.0).with_height(1.2);
197 let mut camera = Camera::default();
198 c.apply(&mut camera, &look(0.0, 0.0), Vec3::ZERO);
199 let eye = camera.eye_position();
201 assert!(approx_eq(eye, Vec3::new(0.0, -6.0, 1.2)), "eye={eye:?}");
202 }
203
204 #[test]
205 fn shoulder_offset_shifts_pivot_right() {
206 let mut c = cam()
207 .with_distance(6.0)
208 .with_height(1.2)
209 .with_shoulder_offset(Vec2::new(0.5, 0.0));
210 let mut camera = Camera::default();
211 c.apply(&mut camera, &look(0.0, 0.0), Vec3::ZERO);
212 assert!((camera.center.x - 0.5).abs() < 1e-5);
213 assert!((camera.center.z - 1.2).abs() < 1e-5);
214 }
215
216 #[test]
217 fn sync_from_camera_round_trips() {
218 let mut c = cam();
219 let mut camera = Camera::default();
220 c.set_look(0.6, 0.2);
221 c.apply(&mut camera, &look(0.0, 0.0), Vec3::ZERO);
222 let mut c2 = cam();
223 c2.sync_from_camera(&camera);
224 assert!((c2.yaw - 0.6).abs() < 1e-4, "yaw={}", c2.yaw);
225 assert!((c2.pitch - 0.2).abs() < 1e-4, "pitch={}", c2.pitch);
226 }
227}