use crate::ray::Ray;
use eulumdat::Eulumdat;
use nalgebra::{Point3, Rotation3, Unit, Vector3};
use rand::Rng;
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub enum Source {
Isotropic { position: Point3<f64>, flux_lm: f64 },
Lambertian {
position: Point3<f64>,
normal: Unit<Vector3<f64>>,
flux_lm: f64,
},
Led {
position: Point3<f64>,
direction: Unit<Vector3<f64>>,
half_angle_deg: f64,
flux_lm: f64,
},
LineSource {
start: Point3<f64>,
end: Point3<f64>,
normal: Unit<Vector3<f64>>,
half_angle_deg: f64,
flux_lm: f64,
},
AreaSource {
center: Point3<f64>,
normal: Unit<Vector3<f64>>,
u_axis: Unit<Vector3<f64>>,
half_width: f64,
half_height: f64,
flux_lm: f64,
},
FromLvk {
position: Point3<f64>,
orientation: Rotation3<f64>,
eulumdat: Box<Eulumdat>,
flux_lm: f64,
cdf: LvkCdf,
},
}
impl Source {
pub fn from_lvk(
position: Point3<f64>,
orientation: Rotation3<f64>,
eulumdat: Eulumdat,
flux_lm: f64,
) -> Self {
let cdf = LvkCdf::build(&eulumdat);
Source::FromLvk {
position,
orientation,
eulumdat: Box::new(eulumdat),
flux_lm,
cdf,
}
}
pub fn flux_lm(&self) -> f64 {
match self {
Source::Isotropic { flux_lm, .. } => *flux_lm,
Source::Lambertian { flux_lm, .. } => *flux_lm,
Source::Led { flux_lm, .. } => *flux_lm,
Source::LineSource { flux_lm, .. } => *flux_lm,
Source::AreaSource { flux_lm, .. } => *flux_lm,
Source::FromLvk { flux_lm, .. } => *flux_lm,
}
}
pub fn sample<R: Rng>(&self, rng: &mut R) -> Ray {
match self {
Source::Isotropic { position, .. } => {
let dir = random_sphere(rng);
Ray::new(*position, dir)
}
Source::Lambertian {
position, normal, ..
} => {
let dir = random_cosine_hemisphere(normal, rng);
Ray::new(*position, dir)
}
Source::Led {
position,
direction,
half_angle_deg,
..
} => {
let dir = random_cone(direction, *half_angle_deg, rng);
Ray::new(*position, dir)
}
Source::LineSource {
start,
end,
normal,
half_angle_deg,
..
} => {
let t: f64 = rng.random();
let origin = start + t * (end - start);
let dir = random_cone(normal, *half_angle_deg, rng);
Ray::new(origin, dir)
}
Source::AreaSource {
center,
normal,
u_axis,
half_width,
half_height,
..
} => {
let u: f64 = (rng.random::<f64>() * 2.0 - 1.0) * half_width;
let v_axis = Unit::new_normalize(normal.cross(u_axis.as_ref()));
let v: f64 = (rng.random::<f64>() * 2.0 - 1.0) * half_height;
let origin = center + u * u_axis.as_ref() + v * v_axis.as_ref();
let dir = random_cosine_hemisphere(normal, rng);
Ray::new(origin, dir)
}
Source::FromLvk {
position,
orientation,
cdf,
..
} => {
let (c_angle, g_angle) = cdf.sample(rng);
let g_rad = g_angle.to_radians();
let c_rad = c_angle.to_radians();
let dir_local = Vector3::new(
g_rad.sin() * c_rad.cos(),
g_rad.sin() * c_rad.sin(),
-g_rad.cos(),
);
let dir_world = orientation * dir_local;
Ray::new(*position, Unit::new_normalize(dir_world))
}
}
}
}
fn random_sphere<R: Rng>(rng: &mut R) -> Unit<Vector3<f64>> {
let z: f64 = 2.0 * rng.random::<f64>() - 1.0;
let r = (1.0 - z * z).max(0.0).sqrt();
let phi = 2.0 * PI * rng.random::<f64>();
Unit::new_normalize(Vector3::new(r * phi.cos(), r * phi.sin(), z))
}
fn random_cosine_hemisphere<R: Rng>(
normal: &Unit<Vector3<f64>>,
rng: &mut R,
) -> Unit<Vector3<f64>> {
let u1: f64 = rng.random();
let u2: f64 = rng.random();
let r = u1.sqrt();
let theta = 2.0 * PI * u2;
let x = r * theta.cos();
let y = r * theta.sin();
let z = (1.0 - u1).sqrt();
let (tangent, bitangent) = build_onb(normal);
let dir = x * tangent.as_ref() + y * bitangent.as_ref() + z * normal.as_ref();
Unit::new_normalize(dir)
}
fn random_cone<R: Rng>(
axis: &Unit<Vector3<f64>>,
half_angle_deg: f64,
rng: &mut R,
) -> Unit<Vector3<f64>> {
let cos_max = (half_angle_deg.to_radians()).cos();
let u: f64 = rng.random();
let cos_theta = 1.0 - u * (1.0 - cos_max);
let sin_theta = (1.0 - cos_theta * cos_theta).max(0.0).sqrt();
let phi = 2.0 * PI * rng.random::<f64>();
let (tangent, bitangent) = build_onb(axis);
let dir = sin_theta * phi.cos() * tangent.as_ref()
+ sin_theta * phi.sin() * bitangent.as_ref()
+ cos_theta * axis.as_ref();
Unit::new_normalize(dir)
}
fn build_onb(n: &Unit<Vector3<f64>>) -> (Unit<Vector3<f64>>, Unit<Vector3<f64>>) {
let a = if n.x.abs() > 0.9 {
Vector3::y_axis()
} else {
Vector3::x_axis()
};
let t = Unit::new_normalize(n.cross(a.as_ref()));
let b = Unit::new_normalize(n.cross(t.as_ref()));
(t, b)
}
#[derive(Debug, Clone)]
pub struct LvkCdf {
pub g_steps: usize,
pub c_steps: usize,
pub g_max: f64,
pub marginal_g: Vec<f64>,
pub conditional_c: Vec<Vec<f64>>,
}
impl LvkCdf {
#[allow(clippy::needless_range_loop)]
pub fn build(ldt: &Eulumdat) -> Self {
let g_max = ldt.g_angles.last().copied().unwrap_or(180.0);
let g_step: f64 = 1.0; let c_step: f64 = 5.0; let g_steps = (g_max / g_step).ceil() as usize + 1;
let c_steps = (360.0 / c_step).ceil() as usize;
let mut pdf = vec![vec![0.0f64; c_steps]; g_steps];
for gi in 0..g_steps {
let g = (gi as f64 * g_step).min(g_max);
let sin_g = g.to_radians().sin();
for ci in 0..c_steps {
let c = ci as f64 * c_step;
pdf[gi][ci] = ldt.sample(c, g) * sin_g;
}
}
let marginal_unnorm: Vec<f64> = pdf.iter().map(|row| row.iter().sum::<f64>()).collect();
let mut marginal_g = vec![0.0; g_steps];
let mut cum = 0.0;
for gi in 0..g_steps {
cum += marginal_unnorm[gi];
marginal_g[gi] = cum;
}
if cum > 0.0 {
for v in &mut marginal_g {
*v /= cum;
}
}
let mut conditional_c = vec![vec![0.0; c_steps]; g_steps];
for gi in 0..g_steps {
let row_sum: f64 = pdf[gi].iter().sum();
let mut ccum = 0.0;
for ci in 0..c_steps {
ccum += pdf[gi][ci];
conditional_c[gi][ci] = if row_sum > 0.0 {
ccum / row_sum
} else {
(ci + 1) as f64 / c_steps as f64
};
}
}
Self {
g_steps,
c_steps,
g_max,
marginal_g,
conditional_c,
}
}
fn sample<R: Rng>(&self, rng: &mut R) -> (f64, f64) {
let u_g: f64 = rng.random();
let gi = match self
.marginal_g
.binary_search_by(|v| v.partial_cmp(&u_g).unwrap())
{
Ok(i) => i,
Err(i) => i.min(self.g_steps - 1),
};
let g = (gi as f64 / (self.g_steps - 1).max(1) as f64) * self.g_max;
let u_c: f64 = rng.random();
let ci = match self.conditional_c[gi].binary_search_by(|v| v.partial_cmp(&u_c).unwrap()) {
Ok(i) => i,
Err(i) => i.min(self.c_steps - 1),
};
let c = ci as f64 * (360.0 / self.c_steps as f64);
(c, g)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
use rand_xoshiro::Xoshiro256PlusPlus;
#[test]
fn isotropic_samples_all_directions() {
let source = Source::Isotropic {
position: Point3::origin(),
flux_lm: 1000.0,
};
let mut rng = Xoshiro256PlusPlus::seed_from_u64(42);
let mut sum = Vector3::zeros();
let n = 10000;
for _ in 0..n {
let ray = source.sample(&mut rng);
sum += ray.direction.as_ref();
}
let mean = sum / n as f64;
assert!(mean.norm() < 0.05, "Mean direction should be near zero");
}
#[test]
fn lambertian_samples_hemisphere() {
let source = Source::Lambertian {
position: Point3::origin(),
normal: Vector3::z_axis(),
flux_lm: 1000.0,
};
let mut rng = Xoshiro256PlusPlus::seed_from_u64(42);
for _ in 0..1000 {
let ray = source.sample(&mut rng);
assert!(
ray.direction.z >= -1e-6,
"Lambertian should only emit into positive hemisphere"
);
}
}
#[test]
fn led_samples_within_cone() {
let source = Source::Led {
position: Point3::origin(),
direction: Vector3::z_axis(),
half_angle_deg: 30.0,
flux_lm: 1000.0,
};
let mut rng = Xoshiro256PlusPlus::seed_from_u64(42);
let cos_limit = 30.0f64.to_radians().cos();
for _ in 0..1000 {
let ray = source.sample(&mut rng);
let cos_angle = ray.direction.dot(Vector3::z_axis().as_ref());
assert!(
cos_angle >= cos_limit - 1e-6,
"LED should sample within cone"
);
}
}
}