use brepkit_math::vec::{Point3, Vec3};
#[derive(Debug, Clone, Copy)]
pub struct Camera {
pub eye: Point3,
pub target: Point3,
pub up: Vec3,
pub fov_y: f64,
pub aspect: f64,
pub near: f64,
pub far: f64,
}
impl Camera {
#[must_use]
pub fn view_direction(&self) -> Vec3 {
(self.target - self.eye)
.normalize()
.unwrap_or_else(|_| Vec3::new(0.0, 0.0, -1.0))
}
}
pub fn view_proj_rtc(cam: &Camera, center: Point3) -> [f32; 16] {
let view = look_at_rh_rtc(cam.eye, cam.target, cam.up, center);
let proj = perspective_rh_zo(cam.fov_y, cam.aspect, cam.near, cam.far);
proj.mul(&view).to_cols_array()
}
#[derive(Debug, Clone, Copy)]
struct Mat4f {
cols: [[f32; 4]; 4],
}
impl Mat4f {
fn to_cols_array(self) -> [f32; 16] {
let c = &self.cols;
[
c[0][0], c[0][1], c[0][2], c[0][3], c[1][0], c[1][1], c[1][2], c[1][3], c[2][0],
c[2][1], c[2][2], c[2][3], c[3][0], c[3][1], c[3][2], c[3][3],
]
}
}
#[derive(Debug, Clone, Copy)]
struct Mat4d {
cols: [[f64; 4]; 4],
}
impl Mat4d {
fn mul(self, rhs: &Self) -> Mat4f {
let mut out = [[0.0_f64; 4]; 4];
for col in 0..4 {
for row in 0..4 {
let mut sum = 0.0;
for k in 0..4 {
sum += self.cols[k][row] * rhs.cols[col][k];
}
out[col][row] = sum;
}
}
#[allow(clippy::cast_possible_truncation)]
Mat4f {
cols: std::array::from_fn(|c| std::array::from_fn(|r| out[c][r] as f32)),
}
}
}
fn fallback_up(f: Vec3) -> Vec3 {
let ax = f.x().abs();
let ay = f.y().abs();
let az = f.z().abs();
if ax <= ay && ax <= az {
Vec3::new(1.0, 0.0, 0.0)
} else if ay <= az {
Vec3::new(0.0, 1.0, 0.0)
} else {
Vec3::new(0.0, 0.0, 1.0)
}
}
fn look_at_rh_rtc(eye: Point3, target: Point3, up: Vec3, center: Point3) -> Mat4d {
let f = (target - eye)
.normalize()
.unwrap_or(Vec3::new(0.0, 0.0, -1.0));
let s = f.cross(up).normalize().unwrap_or_else(|_| {
f.cross(fallback_up(f))
.normalize()
.unwrap_or(Vec3::new(1.0, 0.0, 0.0))
});
let u = s.cross(f);
let eye_rel = eye - center;
let tx = -s.dot(eye_rel);
let ty = -u.dot(eye_rel);
let tz = f.dot(eye_rel);
Mat4d {
cols: [
[s.x(), u.x(), -f.x(), 0.0],
[s.y(), u.y(), -f.y(), 0.0],
[s.z(), u.z(), -f.z(), 0.0],
[tx, ty, tz, 1.0],
],
}
}
fn perspective_rh_zo(fov_y: f64, aspect: f64, near: f64, far: f64) -> Mat4d {
let fov_y = fov_y.clamp(1.0e-4, std::f64::consts::PI - 1.0e-4);
let aspect = if aspect.is_finite() && aspect > 0.0 {
aspect
} else {
1.0
};
let near = if near.is_finite() && near > 0.0 {
near
} else {
1.0e-3
};
let far = if far.is_finite() && far > near {
far
} else {
near * 1000.0
};
let f = 1.0 / (fov_y * 0.5).tan();
let nf = 1.0 / (near - far);
Mat4d {
cols: [
[f / aspect, 0.0, 0.0, 0.0],
[0.0, f, 0.0, 0.0],
[0.0, 0.0, far * nf, -1.0],
[0.0, 0.0, far * near * nf, 0.0],
],
}
}
#[cfg(test)]
mod tests {
use super::*;
fn all_finite(m: &[f32; 16]) -> bool {
m.iter().all(|v| v.is_finite())
}
#[test]
fn degenerate_up_parallel_to_view_stays_finite() {
let cam = Camera {
eye: Point3::new(0.0, 0.0, 10.0),
target: Point3::new(0.0, 0.0, 0.0),
up: Vec3::new(0.0, 0.0, 1.0), fov_y: 45.0_f64.to_radians(),
aspect: 1.0,
near: 0.1,
far: 100.0,
};
let m = view_proj_rtc(&cam, Point3::new(0.0, 0.0, 0.0));
assert!(all_finite(&m), "view-proj must be finite for parallel up");
}
#[test]
fn fallback_up_is_not_parallel_to_view() {
for f in [
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
] {
let up = fallback_up(f);
assert!(
f.cross(up).length() > 0.5,
"fallback up {up:?} too aligned with view dir {f:?}"
);
}
}
#[test]
fn projection_guards_degenerate_inputs() {
let cam = Camera {
eye: Point3::new(5.0, 5.0, 5.0),
target: Point3::new(0.0, 0.0, 0.0),
up: Vec3::new(0.0, 0.0, 1.0),
fov_y: 0.0,
aspect: 0.0,
near: 0.0,
far: 0.0,
};
let m = view_proj_rtc(&cam, Point3::new(0.0, 0.0, 0.0));
assert!(
all_finite(&m),
"projection must be finite for degenerate inputs"
);
}
}