Skip to main content

proof_engine/relativistic/
searchlight.rs

1//! Relativistic searchlight/beaming effect.
2
3use glam::{Vec3, Vec4};
4use super::lorentz::lorentz_factor;
5
6/// Relativistic beaming: intensity transformation for a source moving at speed v.
7/// I_obs = I_rest / (gamma^3 * (1 - beta*cos(angle))^3)
8/// where angle is measured in the observer frame between velocity and line of sight.
9pub fn relativistic_beaming(intensity_rest: f64, v: f64, c: f64, angle: f64) -> f64 {
10    let beta = v / c;
11    let gamma = lorentz_factor(v, c);
12    let d = gamma * (1.0 - beta * angle.cos());
13    if d.abs() < 1e-15 {
14        return intensity_rest * 1e15;
15    }
16    intensity_rest / (d * d * d)
17}
18
19/// Relativistic aberration: convert angle in source rest frame to observer frame.
20/// cos(theta_obs) = (cos(theta_rest) + beta) / (1 + beta * cos(theta_rest))
21pub fn aberration_angle(theta_rest: f64, v: f64, c: f64) -> f64 {
22    let beta = v / c;
23    let cos_rest = theta_rest.cos();
24    let cos_obs = (cos_rest + beta) / (1.0 + beta * cos_rest);
25    cos_obs.clamp(-1.0, 1.0).acos()
26}
27
28/// Headlight factor: forward intensity boost for a source moving at speed v.
29/// This is the beaming factor at angle = 0 (directly forward).
30/// D = 1 / (gamma * (1 - beta)), so I_obs = I_rest * D^3.
31pub fn headlight_factor(v: f64, c: f64) -> f64 {
32    let beta = v / c;
33    let gamma = lorentz_factor(v, c);
34    let d = 1.0 / (gamma * (1.0 - beta));
35    d * d * d
36}
37
38/// Renderer that modifies entity brightness based on velocity direction relative to observer.
39#[derive(Debug, Clone)]
40pub struct SearchlightRenderer {
41    pub c: f64,
42    pub observer_pos: Vec3,
43    pub max_boost: f32,
44}
45
46impl SearchlightRenderer {
47    pub fn new(c: f64, observer_pos: Vec3) -> Self {
48        Self {
49            c,
50            observer_pos,
51            max_boost: 100.0,
52        }
53    }
54
55    /// Compute the apparent brightness of an entity given its position, velocity, and base luminosity.
56    pub fn entity_brightness(
57        &self,
58        entity_pos: Vec3,
59        entity_velocity: Vec3,
60        base_luminosity: f32,
61    ) -> f32 {
62        let v = entity_velocity.length() as f64;
63        if v < 1e-10 {
64            return base_luminosity;
65        }
66
67        let to_observer = (self.observer_pos - entity_pos).normalize_or_zero();
68        let vel_dir = entity_velocity.normalize();
69        let cos_angle = vel_dir.dot(to_observer) as f64;
70        let angle = cos_angle.clamp(-1.0, 1.0).acos();
71
72        let beamed = relativistic_beaming(base_luminosity as f64, v, self.c, angle);
73        (beamed as f32).min(self.max_boost * base_luminosity)
74    }
75
76    /// Compute brightness for multiple entities.
77    pub fn batch_brightness(
78        &self,
79        entities: &[(Vec3, Vec3, f32)], // (pos, velocity, base_luminosity)
80    ) -> Vec<f32> {
81        entities.iter().map(|(pos, vel, lum)| {
82            self.entity_brightness(*pos, *vel, *lum)
83        }).collect()
84    }
85
86    /// Apply beaming to a color by scaling its RGB components.
87    pub fn beamed_color(
88        &self,
89        entity_pos: Vec3,
90        entity_velocity: Vec3,
91        base_color: Vec4,
92    ) -> Vec4 {
93        let factor = self.entity_brightness(entity_pos, entity_velocity, 1.0);
94        Vec4::new(
95            (base_color.x * factor).min(1.0),
96            (base_color.y * factor).min(1.0),
97            (base_color.z * factor).min(1.0),
98            base_color.w,
99        )
100    }
101
102    /// Compute the half-angle of the beaming cone (angle where intensity drops to half of forward max).
103    pub fn beaming_half_angle(&self, v: f64) -> f64 {
104        let beta = v / self.c;
105        let gamma = lorentz_factor(v, self.c);
106        // Approximate: theta_half ~ 1/gamma
107        (1.0 / gamma).asin()
108    }
109}
110
111/// Apparent brightness including distance and beaming.
112/// L_obs = luminosity * D^3 / (4*pi*r^2) where D is the Doppler factor.
113pub fn apparent_brightness(luminosity: f64, v: f64, c: f64, angle: f64) -> f64 {
114    let beta = v / c;
115    let gamma = lorentz_factor(v, c);
116    let d = 1.0 / (gamma * (1.0 - beta * angle.cos()));
117    luminosity * d * d * d
118}
119
120/// Solid angle transformation under Lorentz boost.
121/// d_omega_obs = d_omega_rest / (gamma^2 * (1 - beta*cos(theta))^2)
122pub fn solid_angle_transform(d_omega_rest: f64, v: f64, c: f64, theta: f64) -> f64 {
123    let beta = v / c;
124    let gamma = lorentz_factor(v, c);
125    let denom = gamma * (1.0 - beta * theta.cos());
126    d_omega_rest / (denom * denom)
127}
128
129/// Backward dimming factor (angle = pi).
130pub fn backward_dimming(v: f64, c: f64) -> f64 {
131    let beta = v / c;
132    let gamma = lorentz_factor(v, c);
133    let d = 1.0 / (gamma * (1.0 + beta));
134    d * d * d
135}
136
137/// Compute the Doppler factor D for a given angle.
138/// D = 1 / (gamma * (1 - beta * cos(theta)))
139pub fn doppler_boost_factor(v: f64, c: f64, theta: f64) -> f64 {
140    let beta = v / c;
141    let gamma = lorentz_factor(v, c);
142    1.0 / (gamma * (1.0 - beta * theta.cos()))
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use std::f64::consts::PI;
149
150    const C: f64 = 299_792_458.0;
151
152    #[test]
153    fn test_forward_brightness_boost() {
154        let fwd = relativistic_beaming(1.0, 0.9 * C, C, 0.0);
155        assert!(fwd > 1.0, "Forward should be boosted: {}", fwd);
156        assert!(fwd > 100.0, "At 0.9c forward boost should be large: {}", fwd);
157    }
158
159    #[test]
160    fn test_backward_dimming() {
161        let bwd = relativistic_beaming(1.0, 0.9 * C, C, PI);
162        assert!(bwd < 1.0, "Backward should be dimmed: {}", bwd);
163    }
164
165    #[test]
166    fn test_headlight_factor_increases_with_v() {
167        let h1 = headlight_factor(0.5 * C, C);
168        let h2 = headlight_factor(0.9 * C, C);
169        assert!(h2 > h1, "Higher v should give more forward boost: {} vs {}", h1, h2);
170    }
171
172    #[test]
173    fn test_headlight_at_rest() {
174        let h = headlight_factor(0.0, C);
175        assert!((h - 1.0).abs() < 1e-6, "At rest, headlight factor = 1: {}", h);
176    }
177
178    #[test]
179    fn test_aberration_forward() {
180        // Isotropic emission at pi/2 in rest frame gets beamed forward
181        let theta_obs = aberration_angle(std::f64::consts::FRAC_PI_2, 0.9 * C, C);
182        assert!(theta_obs < std::f64::consts::FRAC_PI_2, "Should be aberrated forward: {}", theta_obs);
183    }
184
185    #[test]
186    fn test_aberration_zero_velocity() {
187        let theta_obs = aberration_angle(1.0, 0.0, C);
188        assert!((theta_obs - 1.0).abs() < 1e-10);
189    }
190
191    #[test]
192    fn test_total_luminosity_conservation() {
193        // Numerically integrate beamed intensity over solid angle.
194        // Total power should be conserved (scales as gamma^2 in directed case,
195        // but integrating D^3 * sin(theta) d(theta) should give a consistent result).
196        let v = 0.5 * C;
197        let n = 10000;
198        let mut sum_beamed = 0.0;
199        let mut sum_rest = 0.0;
200        let dtheta = PI / n as f64;
201        for i in 0..n {
202            let theta = (i as f64 + 0.5) * dtheta;
203            let d_omega = 2.0 * PI * theta.sin() * dtheta;
204            sum_beamed += relativistic_beaming(1.0, v, C, theta) * d_omega;
205            sum_rest += 1.0 * d_omega;
206        }
207        // The total should be boosted by gamma^2 for a moving source
208        // Actually total radiated power transforms as P_obs = P_rest (Lorentz invariant for total)
209        // but beaming redistributes it. For isotropic rest emission:
210        // integral of D^3 over solid angle = 4*pi * gamma^2
211        let gamma = lorentz_factor(v, C);
212        let expected_ratio = gamma * gamma;
213        let actual_ratio = sum_beamed / sum_rest;
214        assert!(
215            (actual_ratio - expected_ratio).abs() / expected_ratio < 0.05,
216            "Luminosity ratio: {} expected: {}",
217            actual_ratio, expected_ratio
218        );
219    }
220
221    #[test]
222    fn test_solid_angle_transform() {
223        let d_omega = solid_angle_transform(1.0, 0.0, C, 0.0);
224        assert!((d_omega - 1.0).abs() < 1e-10);
225    }
226
227    #[test]
228    fn test_searchlight_renderer() {
229        let renderer = SearchlightRenderer::new(C, Vec3::new(0.0, 0.0, 0.0));
230        // Object moving toward observer should be bright
231        let bright = renderer.entity_brightness(
232            Vec3::new(10.0, 0.0, 0.0),
233            Vec3::new(-0.9 * C as f32, 0.0, 0.0),
234            1.0,
235        );
236        // Object moving away should be dim
237        let dim = renderer.entity_brightness(
238            Vec3::new(10.0, 0.0, 0.0),
239            Vec3::new(0.9 * C as f32, 0.0, 0.0),
240            1.0,
241        );
242        assert!(bright > dim, "Forward should be brighter: {} vs {}", bright, dim);
243    }
244}