Skip to main content

proof_engine/relativistic/
terrell.rs

1//! Terrell rotation: apparent rotation of fast-moving objects due to finite light speed.
2
3use glam::{Vec2, Vec3, Vec4};
4use super::lorentz::lorentz_factor;
5
6/// Terrell rotation angle: the apparent rotation angle of an object moving at speed v.
7/// For an object at angle `object_angle` from the direction of motion,
8/// the apparent rotation is approximately arcsin(v/c).
9pub 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    // The Terrell rotation angle depends on the viewing geometry.
15    // For a sphere, the apparent rotation is arcsin(beta).
16    // For general objects at angle theta, the rotation is:
17    // alpha = arcsin(beta * sin(object_angle))
18    let arg = beta * object_angle.sin();
19    arg.clamp(-1.0, 1.0).asin()
20}
21
22/// Compute the apparent position of an object accounting for light travel time.
23/// The observer sees the object at its retarded position.
24pub fn apparent_position(
25    true_pos: Vec3,
26    velocity: Vec3,
27    observer: Vec3,
28    c: f64,
29    time: f64,
30) -> Vec3 {
31    // Current true position at time t
32    let current_pos = true_pos + velocity * time as f32;
33
34    // Find retarded time: the time t_r such that |pos(t_r) - observer| = c * (time - t_r)
35    // For constant velocity: pos(t_r) = true_pos + velocity * t_r
36    // |true_pos + velocity * t_r - observer| = c * (time - t_r)
37    //
38    // Solve iteratively
39    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    // Return the position at retarded time
51    true_pos + velocity * t_r as f32
52}
53
54/// Compute the retarded time: the time at which light must have left the object
55/// to arrive at the observer at the current time.
56/// t_retarded = t - |object_pos - observer_pos| / c
57pub 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 // time offset (negative, meaning light was emitted this much earlier)
60}
61
62/// Terrell renderer: renders fast-moving objects with apparent rotation.
63#[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    /// Compute apparent vertices of an object given its velocity.
75    /// Each vertex is displaced to its retarded position.
76    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    /// Compute the combined Terrell rotation + Lorentz contraction appearance.
89    /// The Terrell effect means a sphere always looks circular (not contracted),
90    /// but other shapes appear rotated.
91    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        // Cross product gives the rotation axis
107        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        // Apply apparent rotation around the rotation axis
112        vertices.iter().map(|vert| {
113            let rel = *vert - center;
114            // Rodrigues' rotation formula
115            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    /// Full rendering pipeline: retarded positions + Terrell rotation.
123    pub fn full_render(
124        &self,
125        vertices: &[Vec3],
126        center: Vec3,
127        velocity: Vec3,
128        time: f64,
129    ) -> Vec<Vec3> {
130        // First compute retarded positions
131        let retarded = self.apparent_vertices(vertices, center, velocity, time);
132        // The retarded positions already encode the Terrell effect
133        retarded
134    }
135}
136
137/// Render a moving cube: compute apparent vertex positions considering light travel time.
138/// `velocity` is the cube's velocity, `observer` is the observer position.
139/// `cube_vertices` are the 8 corners of the cube in the cube's rest frame.
140pub 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
152/// For multiple objects, compute where they appear based on retarded time.
153/// Each object is (position, velocity). Returns apparent positions.
154pub 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        // Retarded time t: light that left the object's position t ago,
161        // pos - vel * t, reaches the observer now:
162        //   |r - v t|^2 = c^2 t^2, r = pos - observer
163        //   (v.v - c^2) t^2 - 2 (r.v) t + r.r = 0.
164        // (This used the current distance / c, which is only right for an
165        // object at rest, and is what the old comment called iterative.)
166        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
184/// Generate unit cube vertices centered at origin.
185pub 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
199/// Compute the time delay for light from each vertex to reach the observer.
200pub 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
206/// Check if Terrell rotation makes a sphere still appear circular.
207/// Returns the max deviation from circular appearance.
208pub 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    // Generate sphere surface points
220    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        // At theta=0 (head-on), the apparent rotation should be zero
261        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        // Distance = c, so delay = 1 second
273        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        // Stationary object: apparent position = true position
286        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        // In metres and m/s a 10 m light delay moves a 1 m/s object by
296        // 3e-8 m, which f32 positions cannot show. Use c = 1 (natural
297        // units) and v = 0.5c so the delay is visible.
298        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        // Receding along the line of sight at 0.5c, the retarded time is
301        // d / (c + v) = d / 1.5, so the shift is v d / 1.5 = d / 3.
302        assert!((apparent[0].x - 10.0 * 2.0 / 3.0).abs() < 1e-4, "{}", apparent[0].x);
303        // Both should be shifted backward along velocity by light delay
304        assert!(apparent[0].x < 10.0);
305        assert!(apparent[1].x < 20.0);
306        // Farther object has more delay
307        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}