use crate::aabb::{Aabb, BoundingSphere};
use crate::projection::Projection;
use crate::{Mat4, Vec3, Vec4};
use serde::{Deserialize, Serialize};
#[cfg(test)]
#[path = "camera_tests.rs"]
mod tests;
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
pub struct Camera {
pub eye: Vec3,
pub target: Vec3,
pub up: Vec3,
pub projection: Projection,
}
impl Camera {
#[must_use]
pub fn look_at(eye: Vec3, target: Vec3, up: Vec3) -> Mat4 {
glam::camera::rh::view::look_at_mat4(eye.into(), target.into(), up.into()).into()
}
#[must_use]
pub fn framing_aabb(bound: &Aabb, aspect: f32) -> Self {
if bound.is_empty() {
return Self::framing(
&BoundingSphere {
center: Vec3::ZERO,
radius: 1.0,
},
aspect,
);
}
let fov_y = std::f32::consts::FRAC_PI_4;
let half = bound.half_extents() * 1.12;
let vertical_tangent = (fov_y * 0.5).tan();
let horizontal_tangent = vertical_tangent * aspect.max(1e-3);
let extents = [half.x, half.y, half.z];
let mut depth_axis = 2usize;
for axis in [1usize, 0usize] {
if extents[axis] < extents[depth_axis] {
depth_axis = axis;
}
}
let first = (depth_axis + 1) % 3;
let second = (depth_axis + 2) % 3;
let (horizontal_axis, vertical_axis) = if extents[first] >= extents[second] {
(first, second)
} else {
(second, first)
};
let projected = (extents[vertical_axis] / vertical_tangent)
.max(extents[horizontal_axis] / horizontal_tangent)
.max(1.0);
let center = bound.center();
let unit = |axis: usize| match axis {
0 => Vec3::X,
1 => Vec3::Y,
_ => Vec3::Z,
};
let eye = center + unit(depth_axis) * (extents[depth_axis] + projected);
let up = unit(vertical_axis);
let sphere = bound.bounding_sphere();
let mut projection = Projection::Perspective {
fov_y,
aspect,
near: 0.1,
far: eye.distance(center) + sphere.radius * 2.0,
};
projection.fit_near_far(eye, &sphere);
Self {
eye,
target: center,
up,
projection,
}
}
#[must_use]
pub fn framing(bound: &BoundingSphere, aspect: f32) -> Self {
let fov_y = std::f32::consts::FRAC_PI_4;
let radius = bound.radius.max(1.0);
let half_min_fov = if aspect < 1.0 {
(fov_y * 0.5).tan() * aspect
} else {
(fov_y * 0.5).tan()
};
let distance = radius / half_min_fov.clamp(1e-3, 1.0) * 1.2;
let eye = bound.center + Vec3::new(0.0, 0.0, distance);
let mut projection = Projection::Perspective {
fov_y,
aspect,
near: 0.1,
far: distance + radius * 2.0,
};
projection.fit_near_far(eye, bound);
Self {
eye,
target: bound.center,
up: Vec3::Y,
projection,
}
}
#[must_use]
pub fn view(&self) -> Mat4 {
glam::camera::rh::view::look_at_mat4(self.eye.into(), self.target.into(), self.up.into())
.into()
}
#[must_use]
pub fn view_proj(&self) -> Mat4 {
self.projection.matrix() * self.view()
}
#[must_use]
pub fn frustum_planes(&self) -> [Vec4; 6] {
let m = self.view_proj();
let row = |i: usize| m.row(i);
let (r0, r1, r2, r3) = (row(0), row(1), row(2), row(3));
let normalize = |p: Vec4| {
let len = p.truncate().length();
if len > 0.0 { p / len } else { p }
};
[
normalize(r3 + r0), normalize(r3 - r0), normalize(r3 + r1), normalize(r3 - r1), normalize(r3 - r2), normalize(r2), ]
}
#[must_use]
pub fn focus_distance(&self) -> f32 {
self.eye.distance(self.target)
}
}