use astrodyn_quantities::aliases::Position;
use astrodyn_quantities::ext::Vec3Ext;
use astrodyn_quantities::frame::RootInertial;
use glam::DVec3;
use std::f64::consts::PI;
const EPSILON: f64 = 1.0e-12;
#[derive(Debug, Clone, Default)]
pub struct LightingBody {
pub radius: f64,
pub position: Position<RootInertial>,
pub distance: f64,
pub half_angle: f64,
}
#[derive(Debug, Clone, Default)]
pub struct LightingParams {
pub obs_angle: f64,
pub phase: f64,
pub occlusion: f64,
pub visible: f64,
pub lighting: f64,
}
#[derive(Debug, Clone, Default)]
pub struct EarthLightingState {
pub sun_body: LightingBody,
pub earth_body: LightingBody,
pub moon_body: LightingBody,
pub sun_earth: LightingParams,
pub moon_earth: LightingParams,
pub earth_albedo: LightingParams,
}
pub fn circle_intersect(r_bottom: f64, r_top: f64, d_centers: f64) -> (bool, f64) {
if d_centers > r_bottom + r_top {
return (false, 0.0);
}
if r_bottom > r_top {
if d_centers < (r_bottom - r_top) + EPSILON {
return (true, PI * r_top * r_top);
}
} else if d_centers < (r_top - r_bottom) + EPSILON {
return (true, PI * r_bottom * r_bottom);
}
let d_c2 = d_centers * d_centers;
let r_t2 = r_top * r_top;
let r_b2 = r_bottom * r_bottom;
let diff_r2 = r_b2 - r_t2;
let cos_bottom_ang = ((d_c2 + diff_r2) / (2.0 * d_centers * r_bottom)).clamp(-1.0, 1.0);
let bottom_ang = cos_bottom_ang.acos();
let cos_top_ang = ((d_c2 - diff_r2) / (2.0 * d_centers * r_top)).clamp(-1.0, 1.0);
let top_ang = cos_top_ang.acos();
let top_area = r_t2 * (top_ang - top_ang.sin() * top_ang.cos());
let bottom_area = r_b2 * (bottom_ang - bottom_ang.sin() * bottom_ang.cos());
(true, top_area + bottom_area)
}
pub fn calc_lighting_params(
source_half_angle: f64,
occluder_half_angle: f64,
obs_angle: f64,
phase: f64,
) -> LightingParams {
let (_intersects, eclipse_area) =
circle_intersect(source_half_angle, occluder_half_angle, obs_angle);
let source_solid_angle = source_half_angle * source_half_angle * PI;
let occlusion = if source_solid_angle > 0.0 {
(eclipse_area / source_solid_angle).clamp(0.0, 1.0)
} else {
0.0
};
let visible = 1.0 - occlusion;
let lighting = phase * visible;
LightingParams {
obs_angle,
phase,
occlusion,
visible,
lighting,
}
}
pub fn compute_earth_lighting(
pos_veh: DVec3,
pos_sun: DVec3,
pos_moon: DVec3,
sun_radius: f64,
earth_radius: f64,
moon_radius: f64,
) -> EarthLightingState {
let sun_rel = pos_sun - pos_veh;
let moon_rel = pos_moon - pos_veh;
let earth_rel = -pos_veh;
let sun_dist = sun_rel.length();
let moon_dist = moon_rel.length();
let earth_dist = earth_rel.length();
let sun_half = (sun_radius / sun_dist).clamp(0.0, 1.0).asin();
let moon_half = (moon_radius / moon_dist).clamp(0.0, 1.0).asin();
let earth_half = (earth_radius / earth_dist).clamp(0.0, 1.0).asin();
let sun_earth_obs = observation_angle(sun_rel, sun_dist, earth_rel, earth_dist);
let moon_earth_obs = observation_angle(moon_rel, moon_dist, earth_rel, earth_dist);
let sun_earth = calc_lighting_params(sun_half, earth_half, sun_earth_obs, 1.0);
let moon_earth = calc_lighting_params(moon_half, earth_half, moon_earth_obs, 1.0);
let earth_albedo_lighting = (sun_earth_obs / PI).abs() * sun_earth.lighting;
EarthLightingState {
sun_body: LightingBody {
radius: sun_radius,
position: sun_rel.m_at::<RootInertial>(),
distance: sun_dist,
half_angle: sun_half,
},
earth_body: LightingBody {
radius: earth_radius,
position: earth_rel.m_at::<RootInertial>(),
distance: earth_dist,
half_angle: earth_half,
},
moon_body: LightingBody {
radius: moon_radius,
position: moon_rel.m_at::<RootInertial>(),
distance: moon_dist,
half_angle: moon_half,
},
sun_earth,
moon_earth,
earth_albedo: LightingParams {
obs_angle: 0.0,
phase: 0.0,
occlusion: 0.0,
visible: 0.0,
lighting: earth_albedo_lighting,
},
}
}
#[allow(clippy::too_many_arguments)]
pub fn compute_earth_lighting_typed(
pos_veh: Position<RootInertial>,
pos_sun: Position<RootInertial>,
pos_moon: Position<RootInertial>,
sun_radius: f64,
earth_radius: f64,
moon_radius: f64,
) -> EarthLightingState {
compute_earth_lighting(
pos_veh.raw_si(),
pos_sun.raw_si(),
pos_moon.raw_si(),
sun_radius,
earth_radius,
moon_radius,
)
}
fn observation_angle(dir_a: DVec3, dist_a: f64, dir_b: DVec3, dist_b: f64) -> f64 {
let denom = dist_a * dist_b;
if denom <= 0.0 {
return 0.0;
}
let cos_obs = dir_a.dot(dir_b) / denom;
let sin_obs = dir_a.cross(dir_b).length() / denom;
sin_obs.atan2(cos_obs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn circle_no_overlap() {
let (intersects, area) = circle_intersect(1.0, 1.0, 3.0);
assert!(!intersects);
assert_eq!(area, 0.0);
}
#[test]
fn circle_containment_top_inside_bottom() {
let (intersects, area) = circle_intersect(2.0, 1.0, 0.0);
assert!(intersects);
assert!(
(area - PI).abs() < 1e-12,
"area should be π·1² = π, got {area}"
);
}
#[test]
fn circle_containment_bottom_inside_top() {
let (intersects, area) = circle_intersect(1.0, 2.0, 0.0);
assert!(intersects);
assert!(
(area - PI).abs() < 1e-12,
"area should be π·1² = π, got {area}"
);
}
#[test]
fn circle_partial_overlap_symmetric() {
let r = 1.0;
let d = 1.0; let (intersects, area) = circle_intersect(r, r, d);
assert!(intersects);
let expected = 2.0
* r
* r
* ((d / (2.0 * r)).acos() - (d / (2.0 * r)) * (1.0 - (d / (2.0 * r)).powi(2)).sqrt());
assert!(
(area - expected).abs() < 1e-10,
"area {area} vs expected {expected}"
);
}
#[test]
fn full_sunlight() {
let pos_veh = DVec3::new(1e8, 0.0, 0.0); let pos_sun = DVec3::new(1.5e11, 0.0, 0.0); let pos_moon = DVec3::new(0.0, 3.84e8, 0.0);
let state = compute_earth_lighting(
pos_veh, pos_sun, pos_moon, 6.96e8, 6.371e6, 1.737e6, );
assert!(
state.sun_earth.visible > 0.99,
"Sun should be fully visible, got {}",
state.sun_earth.visible
);
assert!(
state.sun_earth.occlusion < 0.01,
"No eclipse expected, got occlusion {}",
state.sun_earth.occlusion
);
}
#[test]
fn full_eclipse() {
let pos_sun = DVec3::new(1.5e11, 0.0, 0.0);
let pos_veh = DVec3::new(-7e6, 0.0, 0.0); let pos_moon = DVec3::new(0.0, 3.84e8, 0.0);
let state = compute_earth_lighting(
pos_veh, pos_sun, pos_moon, 6.96e8, 6.371e6, 1.737e6, );
assert!(
state.sun_earth.occlusion > 0.99,
"Full eclipse expected, got occlusion {}",
state.sun_earth.occlusion
);
assert!(
state.sun_earth.visible < 0.01,
"Sun should be fully occluded, got visible {}",
state.sun_earth.visible
);
}
#[test]
fn observation_angle_perpendicular() {
let a = DVec3::new(1.0, 0.0, 0.0);
let b = DVec3::new(0.0, 1.0, 0.0);
let angle = observation_angle(a, 1.0, b, 1.0);
assert!(
(angle - std::f64::consts::FRAC_PI_2).abs() < 1e-12,
"90° expected, got {angle}"
);
}
#[test]
fn observation_angle_same_direction() {
let a = DVec3::new(1.0, 0.0, 0.0);
let angle = observation_angle(a, 1.0, a, 1.0);
assert!(angle.abs() < 1e-12, "0° expected, got {angle}");
}
#[test]
fn lighting_body_position_typed_round_trip() {
let pos_veh = DVec3::new(1.0e6, 2.0e6, 3.0e5);
let pos_sun = DVec3::new(1.5e11, -4.0e10, 1.0e9);
let pos_moon = DVec3::new(0.0, 3.84e8, 1.0e7);
let state = compute_earth_lighting(pos_veh, pos_sun, pos_moon, 6.96e8, 6.371e6, 1.737e6);
assert_eq!(state.sun_body.position.raw_si(), pos_sun - pos_veh);
assert_eq!(state.moon_body.position.raw_si(), pos_moon - pos_veh);
assert_eq!(state.earth_body.position.raw_si(), -pos_veh);
let typed = compute_earth_lighting_typed(
pos_veh.m_at::<RootInertial>(),
pos_sun.m_at::<RootInertial>(),
pos_moon.m_at::<RootInertial>(),
6.96e8,
6.371e6,
1.737e6,
);
assert_eq!(
typed.sun_body.position.raw_si(),
state.sun_body.position.raw_si()
);
assert_eq!(
typed.moon_body.position.raw_si(),
state.moon_body.position.raw_si()
);
assert_eq!(
typed.earth_body.position.raw_si(),
state.earth_body.position.raw_si()
);
}
}