1use glam::{Vec2, Vec3, Vec4};
4use super::lorentz::lorentz_factor;
5
6pub fn terrell_rotation_angle(v: f64, c: f64, object_angle: f64) -> f64 {
10 let beta = v / c;
11 if beta.abs() >= 1.0 {
12 return std::f64::consts::FRAC_PI_2;
13 }
14 let arg = beta * object_angle.sin();
19 arg.clamp(-1.0, 1.0).asin()
20}
21
22pub fn apparent_position(
25 true_pos: Vec3,
26 velocity: Vec3,
27 observer: Vec3,
28 c: f64,
29 time: f64,
30) -> Vec3 {
31 let current_pos = true_pos + velocity * time as f32;
33
34 let mut t_r = time;
40 for _ in 0..20 {
41 let pos_at_tr = true_pos + velocity * t_r as f32;
42 let dist = (pos_at_tr - observer).length() as f64;
43 let t_r_new = time - dist / c;
44 if (t_r_new - t_r).abs() < 1e-12 {
45 break;
46 }
47 t_r = t_r_new;
48 }
49
50 true_pos + velocity * t_r as f32
52}
53
54pub fn retarded_time(object_pos: Vec3, observer_pos: Vec3, c: f64) -> f64 {
58 let dist = (object_pos - observer_pos).length() as f64;
59 -dist / c }
61
62#[derive(Debug, Clone)]
64pub struct TerellRenderer {
65 pub c: f64,
66 pub observer_pos: Vec3,
67}
68
69impl TerellRenderer {
70 pub fn new(c: f64, observer_pos: Vec3) -> Self {
71 Self { c, observer_pos }
72 }
73
74 pub fn apparent_vertices(
77 &self,
78 vertices: &[Vec3],
79 center: Vec3,
80 velocity: Vec3,
81 time: f64,
82 ) -> Vec<Vec3> {
83 vertices.iter().map(|v| {
84 apparent_position(*v, velocity, self.observer_pos, self.c, time)
85 }).collect()
86 }
87
88 pub fn render_object(
92 &self,
93 vertices: &[Vec3],
94 center: Vec3,
95 velocity: Vec3,
96 time: f64,
97 ) -> Vec<Vec3> {
98 let v = velocity.length() as f64;
99 if v < 1e-10 {
100 return vertices.to_vec();
101 }
102
103 let vel_dir = velocity.normalize();
104 let to_observer = (self.observer_pos - center).normalize_or_zero();
105
106 let rotation_axis = vel_dir.cross(to_observer).normalize_or_zero();
108 let sin_angle = (v / self.c) as f32;
109 let cos_angle = (1.0 - sin_angle * sin_angle).max(0.0).sqrt();
110
111 vertices.iter().map(|vert| {
113 let rel = *vert - center;
114 let rotated = rel * cos_angle
116 + rotation_axis.cross(rel) * sin_angle
117 + rotation_axis * rotation_axis.dot(rel) * (1.0 - cos_angle);
118 center + rotated
119 }).collect()
120 }
121
122 pub fn full_render(
124 &self,
125 vertices: &[Vec3],
126 center: Vec3,
127 velocity: Vec3,
128 time: f64,
129 ) -> Vec<Vec3> {
130 let retarded = self.apparent_vertices(vertices, center, velocity, time);
132 retarded
134 }
135}
136
137pub fn render_moving_cube(
141 velocity: Vec3,
142 observer: Vec3,
143 cube_vertices: &[Vec3],
144 c: f64,
145 time: f64,
146) -> Vec<Vec3> {
147 cube_vertices.iter().map(|v| {
148 apparent_position(*v, velocity, observer, c, time)
149 }).collect()
150}
151
152pub fn finite_light_speed_positions(
155 objects: &[(Vec3, Vec3)],
156 observer: Vec3,
157 c: f64,
158) -> Vec<Vec3> {
159 objects.iter().map(|(pos, vel)| {
160 let r = (*pos - observer).as_dvec3();
167 let v = vel.as_dvec3();
168 let a = v.dot(v) - c * c;
169 let b = -2.0 * r.dot(v);
170 let cc = r.dot(r);
171 let t = if a.abs() < 1e-30 {
172 if b.abs() < 1e-30 { 0.0 } else { (-cc / b).max(0.0) }
173 } else {
174 let disc = (b * b - 4.0 * a * cc).max(0.0).sqrt();
175 let t1 = (-b - disc) / (2.0 * a);
176 let t2 = (-b + disc) / (2.0 * a);
177 [t1, t2].into_iter().filter(|t| *t >= 0.0).fold(f64::INFINITY, f64::min)
178 };
179 let t = if t.is_finite() { t } else { r.length() / c };
180 (r - v * t).as_vec3() + observer
181 }).collect()
182}
183
184pub fn unit_cube_vertices(center: Vec3, half_size: f32) -> Vec<Vec3> {
186 let h = half_size;
187 vec![
188 center + Vec3::new(-h, -h, -h),
189 center + Vec3::new( h, -h, -h),
190 center + Vec3::new( h, h, -h),
191 center + Vec3::new(-h, h, -h),
192 center + Vec3::new(-h, -h, h),
193 center + Vec3::new( h, -h, h),
194 center + Vec3::new( h, h, h),
195 center + Vec3::new(-h, h, h),
196 ]
197}
198
199pub fn light_travel_delays(vertices: &[Vec3], observer: Vec3, c: f64) -> Vec<f64> {
201 vertices.iter().map(|v| {
202 (*v - observer).length() as f64 / c
203 }).collect()
204}
205
206pub fn sphere_appearance_deviation(
209 v: f64,
210 c: f64,
211 n_points: usize,
212 observer_distance: f64,
213) -> f64 {
214 let observer = Vec3::new(observer_distance as f32, 0.0, 0.0);
215 let center = Vec3::ZERO;
216 let velocity = Vec3::new(0.0, v as f32, 0.0);
217 let radius = 1.0_f32;
218
219 let mut apparent_radii = Vec::new();
221 for i in 0..n_points {
222 let theta = (i as f64 / n_points as f64) * std::f64::consts::TAU;
223 let point = center + Vec3::new(0.0, theta.cos() as f32, theta.sin() as f32) * radius;
224
225 let app = apparent_position(point, velocity, observer, c, 0.0);
226 let app_center = apparent_position(center, velocity, observer, c, 0.0);
227 let r = (app - app_center).length();
228 apparent_radii.push(r);
229 }
230
231 if apparent_radii.is_empty() {
232 return 0.0;
233 }
234 let mean = apparent_radii.iter().sum::<f32>() / apparent_radii.len() as f32;
235 let max_dev = apparent_radii.iter().map(|r| (r - mean).abs()).fold(0.0_f32, f32::max);
236 (max_dev / mean) as f64
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 const C: f64 = 299_792_458.0;
244
245 #[test]
246 fn test_terrell_rotation_angle_at_rest() {
247 let angle = terrell_rotation_angle(0.0, C, std::f64::consts::FRAC_PI_2);
248 assert!(angle.abs() < 1e-10, "No rotation at rest: {}", angle);
249 }
250
251 #[test]
252 fn test_terrell_rotation_angle_increases_with_v() {
253 let a1 = terrell_rotation_angle(0.5 * C, C, std::f64::consts::FRAC_PI_2);
254 let a2 = terrell_rotation_angle(0.9 * C, C, std::f64::consts::FRAC_PI_2);
255 assert!(a2 > a1, "Rotation should increase with v: {} vs {}", a1, a2);
256 }
257
258 #[test]
259 fn test_terrell_rotation_angle_at_zero_viewing_angle() {
260 let angle = terrell_rotation_angle(0.9 * C, C, 0.0);
262 assert!(angle.abs() < 1e-10, "No rotation at head-on: {}", angle);
263 }
264
265 #[test]
266 fn test_retarded_time() {
267 let t = retarded_time(
268 Vec3::new(C as f32, 0.0, 0.0),
269 Vec3::ZERO,
270 C,
271 );
272 assert!((t - (-1.0)).abs() < 1e-6, "Retarded time offset: {}", t);
274 }
275
276 #[test]
277 fn test_apparent_position_stationary() {
278 let app = apparent_position(
279 Vec3::new(10.0, 0.0, 0.0),
280 Vec3::ZERO,
281 Vec3::ZERO,
282 C,
283 0.0,
284 );
285 assert!((app.x - 10.0).abs() < 0.01);
287 }
288
289 #[test]
290 fn test_finite_light_speed_positions() {
291 let objects = vec![
292 (Vec3::new(10.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)),
293 (Vec3::new(20.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)),
294 ];
295 let objects: Vec<(Vec3, Vec3)> = objects.iter().map(|(p, _)| (*p, Vec3::new(0.5, 0.0, 0.0))).collect();
299 let apparent = finite_light_speed_positions(&objects, Vec3::ZERO, 1.0);
300 assert!((apparent[0].x - 10.0 * 2.0 / 3.0).abs() < 1e-4, "{}", apparent[0].x);
303 assert!(apparent[0].x < 10.0);
305 assert!(apparent[1].x < 20.0);
306 let shift_0 = 10.0 - apparent[0].x;
308 let shift_1 = 20.0 - apparent[1].x;
309 assert!(shift_1 > shift_0);
310 }
311
312 #[test]
313 fn test_render_moving_cube() {
314 let verts = unit_cube_vertices(Vec3::ZERO, 1.0);
315 assert_eq!(verts.len(), 8);
316 let velocity = Vec3::new(0.5 * C as f32, 0.0, 0.0);
317 let observer = Vec3::new(100.0, 0.0, 0.0);
318 let apparent = render_moving_cube(velocity, observer, &verts, C, 0.0);
319 assert_eq!(apparent.len(), 8);
320 }
321
322 #[test]
323 fn test_light_travel_delays() {
324 let verts = vec![
325 Vec3::new(10.0, 0.0, 0.0),
326 Vec3::new(20.0, 0.0, 0.0),
327 ];
328 let delays = light_travel_delays(&verts, Vec3::ZERO, C);
329 assert!(delays[1] > delays[0]);
330 assert!((delays[0] - 10.0 / C).abs() < 1e-10);
331 }
332}