use glam::{Mat3, Mat4, Vec3};
#[derive(Debug, Clone, Copy)]
pub struct CameraIntrinsics {
pub fov_vertical_degrees: f32,
pub aspect_ratio: f32,
}
impl CameraIntrinsics {
#[must_use]
pub fn new(fov_vertical_degrees: f32, aspect_ratio: f32) -> Self {
Self {
fov_vertical_degrees,
aspect_ratio,
}
}
#[must_use]
pub fn from_horizontal_fov(fov_horizontal_degrees: f32, aspect_ratio: f32) -> Self {
let h_rad = fov_horizontal_degrees.to_radians();
let v_rad = 2.0 * ((h_rad / 2.0).tan() / aspect_ratio).atan();
Self {
fov_vertical_degrees: v_rad.to_degrees(),
aspect_ratio,
}
}
#[must_use]
pub fn default_intrinsics() -> Self {
Self {
fov_vertical_degrees: 60.0,
aspect_ratio: 16.0 / 9.0,
}
}
}
impl Default for CameraIntrinsics {
fn default() -> Self {
Self::default_intrinsics()
}
}
#[derive(Debug, Clone, Copy)]
pub struct CameraExtrinsics {
pub position: Vec3,
pub look_dir: Vec3,
pub up_dir: Vec3,
}
impl CameraExtrinsics {
#[must_use]
pub fn new(position: Vec3, look_dir: Vec3, up_dir: Vec3) -> Self {
Self {
position,
look_dir: look_dir.normalize(),
up_dir: up_dir.normalize(),
}
}
#[must_use]
pub fn default_extrinsics() -> Self {
Self {
position: Vec3::ZERO,
look_dir: -Vec3::Z,
up_dir: Vec3::Y,
}
}
#[must_use]
pub fn look_at(position: Vec3, target: Vec3, up: Vec3) -> Self {
let look_dir = (target - position).normalize();
Self::new(position, look_dir, up)
}
#[must_use]
pub fn right_dir(&self) -> Vec3 {
self.look_dir.cross(self.up_dir).normalize()
}
#[must_use]
pub fn camera_frame(&self) -> (Vec3, Vec3, Vec3) {
let right = self.right_dir();
let up = right.cross(self.look_dir).normalize();
(self.look_dir, up, right)
}
#[must_use]
pub fn view_matrix(&self) -> Mat4 {
let (look, up, right) = self.camera_frame();
let rotation = Mat3::from_cols(right, up, -look);
let translation = -rotation * self.position;
Mat4::from_cols(
rotation.x_axis.extend(0.0),
rotation.y_axis.extend(0.0),
rotation.z_axis.extend(0.0),
translation.extend(1.0),
)
}
}
impl Default for CameraExtrinsics {
fn default() -> Self {
Self::default_extrinsics()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CameraParameters {
pub intrinsics: CameraIntrinsics,
pub extrinsics: CameraExtrinsics,
}
impl CameraParameters {
#[must_use]
pub fn new(intrinsics: CameraIntrinsics, extrinsics: CameraExtrinsics) -> Self {
Self {
intrinsics,
extrinsics,
}
}
#[must_use]
pub fn from_vectors(
position: Vec3,
look_dir: Vec3,
up_dir: Vec3,
fov_vertical_degrees: f32,
aspect_ratio: f32,
) -> Self {
Self {
intrinsics: CameraIntrinsics::new(fov_vertical_degrees, aspect_ratio),
extrinsics: CameraExtrinsics::new(position, look_dir, up_dir),
}
}
#[must_use]
pub fn look_at(
position: Vec3,
target: Vec3,
up: Vec3,
fov_vertical_degrees: f32,
aspect_ratio: f32,
) -> Self {
Self {
intrinsics: CameraIntrinsics::new(fov_vertical_degrees, aspect_ratio),
extrinsics: CameraExtrinsics::look_at(position, target, up),
}
}
#[must_use]
pub fn position(&self) -> Vec3 {
self.extrinsics.position
}
#[must_use]
pub fn look_dir(&self) -> Vec3 {
self.extrinsics.look_dir
}
#[must_use]
pub fn up_dir(&self) -> Vec3 {
self.extrinsics.up_dir
}
#[must_use]
pub fn right_dir(&self) -> Vec3 {
self.extrinsics.right_dir()
}
#[must_use]
pub fn camera_frame(&self) -> (Vec3, Vec3, Vec3) {
self.extrinsics.camera_frame()
}
#[must_use]
pub fn fov_vertical_degrees(&self) -> f32 {
self.intrinsics.fov_vertical_degrees
}
#[must_use]
pub fn aspect_ratio(&self) -> f32 {
self.intrinsics.aspect_ratio
}
#[must_use]
pub fn view_matrix(&self) -> Mat4 {
self.extrinsics.view_matrix()
}
#[must_use]
pub fn projection_matrix(&self, near: f32, far: f32) -> Mat4 {
Mat4::perspective_rh(
self.intrinsics.fov_vertical_degrees.to_radians(),
self.intrinsics.aspect_ratio,
near,
far,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_camera_frame() {
let extrinsics = CameraExtrinsics::new(
Vec3::new(0.0, 0.0, 5.0),
Vec3::new(0.0, 0.0, -1.0),
Vec3::new(0.0, 1.0, 0.0),
);
let (look, up, right) = extrinsics.camera_frame();
assert!((look - Vec3::new(0.0, 0.0, -1.0)).length() < 1e-6);
assert!((up - Vec3::new(0.0, 1.0, 0.0)).length() < 1e-6);
assert!((right - Vec3::new(1.0, 0.0, 0.0)).length() < 1e-6);
}
#[test]
fn test_look_at() {
let params =
CameraParameters::look_at(Vec3::new(0.0, 0.0, 5.0), Vec3::ZERO, Vec3::Y, 60.0, 1.5);
assert!((params.look_dir() - Vec3::new(0.0, 0.0, -1.0)).length() < 1e-6);
assert_eq!(params.fov_vertical_degrees(), 60.0);
assert_eq!(params.aspect_ratio(), 1.5);
}
}