#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Projection {
Perspective,
Orthographic,
}
#[derive(Clone)]
pub struct Camera {
pub projection: Projection,
pub center: glam::Vec3,
pub distance: f32,
pub orientation: glam::Quat,
pub fov_y: f32,
pub aspect: f32,
pub znear: f32,
pub zfar: f32,
}
impl Default for Camera {
fn default() -> Self {
Self {
projection: Projection::Perspective,
center: glam::Vec3::ZERO,
distance: 5.0,
orientation: glam::Quat::from_rotation_y(0.3) * glam::Quat::from_rotation_x(-0.3),
fov_y: std::f32::consts::FRAC_PI_4,
aspect: 1.5,
znear: 0.01,
zfar: 1000.0,
}
}
}
impl Camera {
fn eye_offset(&self) -> glam::Vec3 {
self.orientation * (glam::Vec3::Z * self.distance)
}
pub fn eye_position(&self) -> glam::Vec3 {
self.center + self.eye_offset()
}
pub fn view_matrix(&self) -> glam::Mat4 {
let eye = self.eye_position();
let up = self.orientation * glam::Vec3::Y;
glam::Mat4::look_at_rh(eye, self.center, up)
}
pub fn proj_matrix(&self) -> glam::Mat4 {
let effective_zfar = self.zfar.max(self.distance * 3.0);
match self.projection {
Projection::Perspective => {
glam::Mat4::perspective_rh(self.fov_y, self.aspect, self.znear, effective_zfar)
}
Projection::Orthographic => {
let half_h = self.distance * (self.fov_y / 2.0).tan();
let half_w = half_h * self.aspect;
glam::Mat4::orthographic_rh(
-half_w,
half_w,
-half_h,
half_h,
self.znear,
effective_zfar,
)
}
}
}
pub fn view_proj_matrix(&self) -> glam::Mat4 {
self.proj_matrix() * self.view_matrix()
}
pub fn right(&self) -> glam::Vec3 {
self.orientation * glam::Vec3::X
}
pub fn up(&self) -> glam::Vec3 {
self.orientation * glam::Vec3::Y
}
pub fn frustum(&self) -> crate::camera::frustum::Frustum {
crate::camera::frustum::Frustum::from_view_proj(&self.view_proj_matrix())
}
pub fn fit_sphere(&self, center: glam::Vec3, radius: f32) -> (glam::Vec3, f32) {
let distance = match self.projection {
Projection::Perspective => radius / (self.fov_y / 2.0).tan() * 1.2,
Projection::Orthographic => radius * 1.2,
};
(center, distance)
}
pub fn fit_aabb(&self, aabb: &crate::scene::aabb::Aabb) -> (glam::Vec3, f32) {
let center = aabb.center();
let radius = aabb.half_extents().length();
self.fit_sphere(center, radius)
}
pub fn center_on_domain(&mut self, nx: f32, ny: f32, nz: f32) {
self.center = glam::Vec3::new(nx / 2.0, ny / 2.0, nz / 2.0);
let diagonal = (nx * nx + ny * ny + nz * nz).sqrt();
self.distance = (diagonal / 2.0) / (self.fov_y / 2.0).tan() * 1.2;
self.znear = (diagonal * 0.0001).max(0.01);
self.zfar = (diagonal * 10.0).max(1000.0);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_eye_position() {
let cam = Camera::default();
let expected = cam.center + cam.orientation * (glam::Vec3::Z * cam.distance);
let eye = cam.eye_position();
assert!(
(eye - expected).length() < 1e-5,
"eye={eye:?} expected={expected:?}"
);
}
#[test]
fn test_view_matrix_looks_at_center() {
let cam = Camera::default();
let view = cam.view_matrix();
let center_view = view.transform_point3(cam.center);
assert!(
center_view.x.abs() < 1e-4,
"center_view.x={}",
center_view.x
);
assert!(
center_view.y.abs() < 1e-4,
"center_view.y={}",
center_view.y
);
assert!(
center_view.z < 0.0,
"center should be in front of camera, z={}",
center_view.z
);
}
#[test]
fn test_view_proj_roundtrip() {
let cam = Camera::default();
let vp = cam.view_proj_matrix();
let vp_inv = vp.inverse();
let world_pt = vp_inv.project_point3(glam::Vec3::new(0.0, 0.0, 0.5));
let eye = cam.eye_position();
let to_center = (cam.center - eye).normalize();
let to_pt = (world_pt - eye).normalize();
let dot = to_center.dot(to_pt);
assert!(
dot > 0.99,
"dot={dot}, point should be along camera-to-center ray"
);
}
#[test]
fn test_center_on_domain() {
let mut cam = Camera::default();
cam.center_on_domain(10.0, 10.0, 10.0);
assert!((cam.center - glam::Vec3::splat(5.0)).length() < 1e-5);
assert!(cam.distance > 0.0);
}
#[test]
fn test_fit_sphere_perspective() {
let cam = Camera::default(); let (center, dist) = cam.fit_sphere(glam::Vec3::ZERO, 5.0);
assert!((center - glam::Vec3::ZERO).length() < 1e-5);
let expected = 5.0 / (cam.fov_y / 2.0).tan() * 1.2;
assert!(
(dist - expected).abs() < 1e-4,
"dist={dist}, expected={expected}"
);
}
#[test]
fn test_fit_sphere_orthographic() {
let mut cam = Camera::default();
cam.projection = Projection::Orthographic;
let (_, dist) = cam.fit_sphere(glam::Vec3::ZERO, 5.0);
let expected = 5.0 * 1.2;
assert!(
(dist - expected).abs() < 1e-4,
"dist={dist}, expected={expected}"
);
}
#[test]
fn test_fit_aabb_unit_cube() {
let cam = Camera::default();
let aabb = crate::scene::aabb::Aabb {
min: glam::Vec3::splat(-0.5),
max: glam::Vec3::splat(0.5),
};
let (center, dist) = cam.fit_aabb(&aabb);
assert!(center.length() < 1e-5, "center should be origin");
assert!(dist > 0.0, "distance should be positive");
let radius = aabb.half_extents().length();
let expected = radius / (cam.fov_y / 2.0).tan() * 1.2;
assert!(
(dist - expected).abs() < 1e-4,
"dist={dist}, expected={expected}"
);
}
#[test]
fn test_fit_aabb_preserves_padding() {
let cam = Camera::default();
let aabb = crate::scene::aabb::Aabb {
min: glam::Vec3::splat(-2.0),
max: glam::Vec3::splat(2.0),
};
let (_, dist) = cam.fit_aabb(&aabb);
let radius = aabb.half_extents().length();
let no_pad = radius / (cam.fov_y / 2.0).tan();
assert!(
dist > no_pad,
"padded distance ({dist}) should exceed unpadded ({no_pad})"
);
}
#[test]
fn test_right_up_orthogonal() {
let cam = Camera::default();
let dot = cam.right().dot(cam.up());
assert!(
dot.abs() < 1e-5,
"right and up should be orthogonal, dot={dot}"
);
}
}