use nalgebra::{Point3, RealField, Rotation3, Unit, Vector3};
use crate::{
error::{GeometryError, Result},
rt::Ray,
scene::Projection,
traits::FallibleNumeric,
};
#[derive(Debug, Clone)]
pub struct Camera<T: RealField + Copy> {
position: Point3<T>,
look_at: Point3<T>,
projection: Projection<T>,
resolution: [usize; 2],
}
impl<T: RealField + Copy> Camera<T> {
pub fn new(position: Point3<T>, look_at: Point3<T>, projection: Projection<T>, resolution: [usize; 2]) -> Result<Self> {
if resolution[0] == 0 || resolution[1] == 0 {
return Err(GeometryError::InvalidResolution {
width: resolution[1],
height: resolution[0],
}
.into());
}
Ok(Self {
position,
look_at,
projection,
resolution,
})
}
pub const fn resolution(&self) -> &[usize; 2] {
&self.resolution
}
pub fn generate_ray(&self, pixel_index: [usize; 2]) -> Result<Ray<T>> {
if pixel_index[0] >= self.resolution[0] || pixel_index[1] >= self.resolution[1] {
return Err(GeometryError::PixelOutOfBounds {
row: pixel_index[0],
col: pixel_index[1],
res_height: self.resolution[0],
res_width: self.resolution[1],
}
.into());
}
match self.projection {
Projection::Perspective(fov) => self.generate_perspective_ray(pixel_index, fov),
Projection::Orthographic(width) => self.generate_ortho_ray(pixel_index, width),
}
}
fn generate_perspective_ray(&self, pixel_index: [usize; 2], fov: T) -> Result<Ray<T>> {
let height = T::try_from_usize(self.resolution[0])?;
let width = T::try_from_usize(self.resolution[1])?;
let half = T::try_from_f32(0.5)?;
let d_row = (T::try_from_usize(pixel_index[0])? / height) - half;
let d_col = (T::try_from_usize(pixel_index[1])? / width) - half;
let aspect_ratio = width / height;
let half_fov = fov * half;
let d_theta = -d_col * half_fov;
let d_phi = -d_row * (half_fov / aspect_ratio);
let forward = Unit::new_normalize(self.look_at - self.position);
let right = Unit::new_normalize(forward.cross(&Vector3::z()));
let up = Unit::new_normalize(right.cross(&forward));
let vertical_rotation = Rotation3::from_axis_angle(&right, d_phi);
let lateral_rotation = Rotation3::from_axis_angle(&up, d_theta);
let direction = lateral_rotation * vertical_rotation * forward;
Ok(Ray::new(self.position, direction))
}
fn generate_ortho_ray(&self, pixel_index: [usize; 2], width: T) -> Result<Ray<T>> {
let height_px = T::try_from_usize(self.resolution[0])?;
let width_px = T::try_from_usize(self.resolution[1])?;
let half = T::try_from_f32(0.5)?;
let u = (T::try_from_usize(pixel_index[1])? / width_px) - half;
let v = (T::try_from_usize(pixel_index[0])? / height_px) - half;
let aspect_ratio = width_px / height_px;
let view_width = width;
let view_height = -view_width / aspect_ratio;
let forward = Unit::new_normalize(self.look_at - self.position);
let right = Unit::new_normalize(forward.cross(&Vector3::z()));
let up = Unit::new_normalize(right.cross(&forward));
let horizontal_offset = right.as_ref() * (u * view_width);
let vertical_offset = up.as_ref() * (v * view_height);
let ray_origin = self.position + horizontal_offset + vertical_offset;
Ok(Ray::new(ray_origin, forward))
}
}