1use crate::render::telemetry::CameraUniform;
4
5#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct CameraState {
8 pub yaw: f32,
9 pub pitch: f32,
10 pub zoom: f32,
11}
12
13impl Default for CameraState {
14 fn default() -> Self {
15 Self {
16 yaw: 0.0,
17 pitch: 0.25,
18 zoom: 3.5,
19 }
20 }
21}
22
23impl CameraState {
24 #[inline]
25 pub fn clamped(mut self) -> Self {
26 const PITCH_LIMIT: f32 = 1.45;
27 self.pitch = self.pitch.clamp(-PITCH_LIMIT, PITCH_LIMIT);
28 self.zoom = self.zoom.clamp(0.35, 48.0);
29 self
30 }
31
32 pub fn to_uniform(&self, aspect: f32, tensor_mode: bool) -> CameraUniform {
34 let state = self.clamped();
35 let eye = orbit_eye_position(state.yaw, state.pitch, state.zoom);
36 let mut padding = [0.0_f32; 12];
37 padding[1] = eye[0];
38 padding[2] = eye[1];
39 padding[3] = eye[2];
40 CameraUniform {
41 view_projection: orbit_view_projection(state.yaw, state.pitch, state.zoom, aspect),
42 yaw: state.yaw,
43 pitch: state.pitch,
44 zoom: state.zoom,
45 tensor_mode: if tensor_mode { 1 } else { 0 },
46 _padding: padding,
47 }
48 }
49}
50
51#[inline]
53pub fn orbit_eye_position(yaw: f32, pitch: f32, zoom: f32) -> [f32; 3] {
54 let pitch = pitch.clamp(-1.45, 1.45);
55 let dist = zoom.clamp(0.35, 48.0);
56 let cy = yaw.cos();
57 let sy = yaw.sin();
58 let cp = pitch.cos();
59 let sp = pitch.sin();
60 [dist * cp * sy, dist * sp, dist * cp * cy]
61}
62
63pub fn orbit_view_projection(yaw: f32, pitch: f32, zoom: f32, aspect: f32) -> [[f32; 4]; 4] {
65 let yaw = yaw;
66 let pitch = pitch.clamp(-1.45, 1.45);
67 let dist = zoom.clamp(0.35, 48.0);
68 let aspect = aspect.max(0.05);
69
70 let cy = yaw.cos();
71 let sy = yaw.sin();
72 let cp = pitch.cos();
73 let sp = pitch.sin();
74
75 let eye_x = dist * cp * sy;
76 let eye_y = dist * sp;
77 let eye_z = dist * cp * cy;
78
79 let view = look_at_rh([eye_x, eye_y, eye_z], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
80 let proj = perspective_rh_gl(45.0_f32.to_radians(), aspect, 0.05, 200.0);
81 mat4_mul(proj, view)
82}
83
84#[inline]
85fn look_at_rh(eye: [f32; 3], center: [f32; 3], up: [f32; 3]) -> [[f32; 4]; 4] {
86 let f = normalize(sub(center, eye));
87 let s = normalize(cross(f, up));
88 let u = cross(s, f);
89
90 [
91 [s[0], u[0], -f[0], 0.0],
92 [s[1], u[1], -f[1], 0.0],
93 [s[2], u[2], -f[2], 0.0],
94 [-dot(s, eye), -dot(u, eye), dot(f, eye), 1.0],
95 ]
96}
97
98#[inline]
99fn perspective_rh_gl(fov_y: f32, aspect: f32, near: f32, far: f32) -> [[f32; 4]; 4] {
100 let f = 1.0 / (fov_y * 0.5).tan();
101 let nf = 1.0 / (near - far);
102 [
103 [f / aspect, 0.0, 0.0, 0.0],
104 [0.0, f, 0.0, 0.0],
105 [0.0, 0.0, (far + near) * nf, -1.0],
106 [0.0, 0.0, 2.0 * far * near * nf, 0.0],
107 ]
108}
109
110#[inline]
111fn mat4_mul(a: [[f32; 4]; 4], b: [[f32; 4]; 4]) -> [[f32; 4]; 4] {
112 let mut out = [[0.0; 4]; 4];
113 for c in 0..4 {
114 for r in 0..4 {
115 out[c][r] =
116 a[0][r] * b[c][0] + a[1][r] * b[c][1] + a[2][r] * b[c][2] + a[3][r] * b[c][3];
117 }
118 }
119 out
120}
121
122#[inline]
123fn sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
124 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
125}
126
127#[inline]
128fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
129 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
130}
131
132#[inline]
133fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
134 [
135 a[1] * b[2] - a[2] * b[1],
136 a[2] * b[0] - a[0] * b[2],
137 a[0] * b[1] - a[1] * b[0],
138 ]
139}
140
141#[inline]
142fn normalize(v: [f32; 3]) -> [f32; 3] {
143 let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
144 if len < 1e-6 {
145 return [0.0, 0.0, 1.0];
146 }
147 [v[0] / len, v[1] / len, v[2] / len]
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn view_projection_finite_at_defaults() {
156 let m = orbit_view_projection(0.0, 0.25, 3.5, 16.0 / 9.0);
157 assert!(m[0][0].is_finite());
158 assert!(m[3][2].is_finite());
159 }
160
161 #[test]
162 fn camera_uniform_is_128_bytes() {
163 let u = CameraState::default().to_uniform(1.0, true);
164 assert_eq!(std::mem::size_of::<CameraUniform>(), 128);
165 let bytes = bytemuck::bytes_of(&u);
166 assert_eq!(bytes.len(), 128);
167 }
168
169 #[test]
170 fn camera_uniform_carries_eye_position() {
171 let state = CameraState {
172 yaw: 0.5,
173 pitch: 0.25,
174 zoom: 4.0,
175 };
176 let u = state.to_uniform(16.0 / 9.0, true);
177 let eye = orbit_eye_position(state.yaw, state.pitch, state.zoom);
178 assert!((u._padding[1] - eye[0]).abs() < 1e-5);
179 assert!((u._padding[2] - eye[1]).abs() < 1e-5);
180 assert!((u._padding[3] - eye[2]).abs() < 1e-5);
181 }
182}