use crate::catalog;
use crate::geometry::{Primitive, SceneObject};
use crate::material::{Material, MaterialParams};
use crate::ray::{HitRecord, Ray};
use crate::source::Source;
use crate::MaterialId;
use nalgebra::{Point3, Unit, Vector3};
#[derive(Debug, Clone)]
pub struct Scene {
pub sources: Vec<Source>,
pub objects: Vec<SceneObject>,
materials: Vec<Material>,
material_params: Vec<MaterialParams>,
}
impl Scene {
pub fn new() -> Self {
Self {
sources: Vec::new(),
objects: Vec::new(),
materials: Vec::new(),
material_params: Vec::new(),
}
}
pub fn add_source(&mut self, source: Source) {
self.sources.push(source);
}
pub fn add_material(&mut self, params: MaterialParams) -> MaterialId {
let id = self.materials.len();
self.materials.push(params.to_material());
self.material_params.push(params);
id
}
pub fn add_object(&mut self, primitive: Primitive, material: MaterialId, label: &str) -> usize {
let idx = self.objects.len();
self.objects.push(SceneObject {
primitive,
material,
label: label.to_string(),
});
idx
}
pub fn material(&self, id: MaterialId) -> &Material {
&self.materials[id]
}
pub fn material_params(&self, id: MaterialId) -> &MaterialParams {
&self.material_params[id]
}
pub fn total_source_flux(&self) -> f64 {
self.sources.iter().map(|s| s.flux_lm()).sum()
}
pub fn intersect(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
let mut closest: Option<HitRecord> = None;
let mut closest_t = t_max;
for obj in &self.objects {
if let Some(hit) = obj.primitive.intersect(ray, t_min, closest_t, obj.material) {
closest_t = hit.t;
closest = Some(hit);
}
}
closest
}
}
impl Default for Scene {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
pub enum ReflectorSide {
Left,
Right,
Back,
Surround,
}
#[derive(Debug, Clone)]
pub struct CoverPlacement {
pub distance_mm: f64,
pub width_mm: f64,
pub height_mm: f64,
}
#[derive(Debug, Clone)]
pub struct ReflectorPlacement {
pub distance_mm: f64,
pub length_mm: f64,
pub side: ReflectorSide,
}
pub struct SceneBuilder {
scene: Scene,
source_position: Point3<f64>,
source_direction: Unit<Vector3<f64>>,
}
impl SceneBuilder {
pub fn new() -> Self {
Self {
scene: Scene::new(),
source_position: Point3::origin(),
source_direction: Unit::new_unchecked(Vector3::new(0.0, 0.0, -1.0)),
}
}
pub fn source(mut self, source: Source) -> Self {
self.scene.add_source(source);
self
}
pub fn reflector(mut self, material: MaterialParams, placement: ReflectorPlacement) -> Self {
let mat_name = material.name.clone();
let mat_id = self.scene.add_material(material);
let d = placement.distance_mm / 1000.0; let l = placement.length_mm / 1000.0;
match placement.side {
ReflectorSide::Surround => {
let prim = Primitive::Cylinder {
center: self.source_position + self.source_direction.as_ref() * (l / 2.0),
axis: self.source_direction,
radius: d,
half_height: l / 2.0,
capped: false,
};
self.scene
.add_object(prim, mat_id, &format!("{mat_name} housing"));
}
ReflectorSide::Back => {
let normal = Unit::new_unchecked(-self.source_direction.into_inner());
let u_axis = perpendicular_axis(&self.source_direction);
let center = self.source_position - self.source_direction.as_ref() * d;
let prim = Primitive::Sheet {
center,
normal,
u_axis,
half_width: l / 2.0,
half_height: l / 2.0,
thickness: 0.001,
};
self.scene
.add_object(prim, mat_id, &format!("{mat_name} back"));
}
ReflectorSide::Left => {
let u_axis = perpendicular_axis(&self.source_direction);
let normal = Unit::new_normalize(u_axis.into_inner());
let center = self.source_position - u_axis.as_ref() * d
+ self.source_direction.as_ref() * (l / 2.0);
let prim = Primitive::Sheet {
center,
normal,
u_axis: self.source_direction,
half_width: l / 2.0,
half_height: l / 2.0,
thickness: 0.001,
};
self.scene
.add_object(prim, mat_id, &format!("{mat_name} left"));
}
ReflectorSide::Right => {
let u_axis = perpendicular_axis(&self.source_direction);
let normal = Unit::new_normalize(-u_axis.into_inner());
let center = self.source_position
+ u_axis.as_ref() * d
+ self.source_direction.as_ref() * (l / 2.0);
let prim = Primitive::Sheet {
center,
normal,
u_axis: self.source_direction,
half_width: l / 2.0,
half_height: l / 2.0,
thickness: 0.001,
};
self.scene
.add_object(prim, mat_id, &format!("{mat_name} right"));
}
}
self
}
pub fn cover(mut self, material: MaterialParams, placement: CoverPlacement) -> Self {
let mat_name = material.name.clone();
let mat_id = self.scene.add_material(material);
let d = placement.distance_mm / 1000.0;
let w = placement.width_mm / 1000.0;
let h = placement.height_mm / 1000.0;
let center = self.source_position + self.source_direction.as_ref() * d;
let normal = Unit::new_unchecked(-self.source_direction.into_inner());
let u_axis = perpendicular_axis(&self.source_direction);
let thickness = self.scene.material_params[mat_id].thickness_mm / 1000.0;
let prim = Primitive::Sheet {
center,
normal,
u_axis,
half_width: w / 2.0,
half_height: h / 2.0,
thickness,
};
self.scene
.add_object(prim, mat_id, &format!("{mat_name} cover"));
self
}
pub fn build(self) -> Scene {
self.scene
}
}
impl Default for SceneBuilder {
fn default() -> Self {
Self::new()
}
}
fn perpendicular_axis(axis: &Unit<Vector3<f64>>) -> Unit<Vector3<f64>> {
let a = if axis.x.abs() > 0.9 {
Vector3::y_axis()
} else {
Vector3::x_axis()
};
Unit::new_normalize(axis.cross(a.as_ref()))
}
pub fn bare_lambertian(flux_lm: f64) -> Scene {
let mut scene = Scene::new();
scene.add_source(Source::Lambertian {
position: Point3::origin(),
normal: Unit::new_unchecked(Vector3::new(0.0, 0.0, -1.0)),
flux_lm,
});
scene
}
pub fn bare_isotropic(flux_lm: f64) -> Scene {
let mut scene = Scene::new();
scene.add_source(Source::Isotropic {
position: Point3::origin(),
flux_lm,
});
scene
}
pub fn led_with_housing(flux_lm: f64, beam_angle_deg: f64) -> Scene {
SceneBuilder::new()
.source(Source::Led {
position: Point3::origin(),
direction: Unit::new_unchecked(Vector3::new(0.0, 0.0, -1.0)),
half_angle_deg: beam_angle_deg / 2.0,
flux_lm,
})
.reflector(
catalog::white_paint(),
ReflectorPlacement {
distance_mm: 25.0,
length_mm: 50.0,
side: ReflectorSide::Surround,
},
)
.build()
}
pub fn led_housing_with_cover(
flux_lm: f64,
beam_angle_deg: f64,
cover_material: MaterialParams,
cover_distance_mm: f64,
) -> Scene {
SceneBuilder::new()
.source(Source::Led {
position: Point3::origin(),
direction: Unit::new_unchecked(Vector3::new(0.0, 0.0, -1.0)),
half_angle_deg: beam_angle_deg / 2.0,
flux_lm,
})
.reflector(
catalog::white_paint(),
ReflectorPlacement {
distance_mm: 25.0,
length_mm: cover_distance_mm + 10.0,
side: ReflectorSide::Surround,
},
)
.cover(
cover_material,
CoverPlacement {
distance_mm: cover_distance_mm,
width_mm: 60.0,
height_mm: 60.0,
},
)
.build()
}
pub fn roundtrip_validation(ldt: &eulumdat::Eulumdat) -> Scene {
let flux = ldt.total_luminous_flux();
let mut scene = Scene::new();
scene.add_source(Source::from_lvk(
Point3::origin(),
nalgebra::Rotation3::identity(),
ldt.clone(),
flux,
));
scene
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scene_builder_creates_objects() {
let scene = SceneBuilder::new()
.source(Source::Led {
position: Point3::origin(),
direction: Unit::new_unchecked(Vector3::new(0.0, 0.0, -1.0)),
half_angle_deg: 60.0,
flux_lm: 1000.0,
})
.reflector(
catalog::white_paint(),
ReflectorPlacement {
distance_mm: 25.0,
length_mm: 50.0,
side: ReflectorSide::Surround,
},
)
.cover(
catalog::opal_pmma_3mm(),
CoverPlacement {
distance_mm: 40.0,
width_mm: 60.0,
height_mm: 60.0,
},
)
.build();
assert_eq!(scene.sources.len(), 1);
assert_eq!(scene.objects.len(), 2); assert!((scene.total_source_flux() - 1000.0).abs() < 0.01);
}
#[test]
fn bare_lambertian_has_no_objects() {
let scene = bare_lambertian(1000.0);
assert_eq!(scene.sources.len(), 1);
assert!(scene.objects.is_empty());
}
}