use super::mat4::{
Mat4, Vec3, mat4_look_at_dir, mat4_orthographic, mat4_perspective, mat4_rotate, mat4_translate,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CameraDirection {
Up,
Down,
Left,
Right,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CameraMove {
Up,
Down,
Left,
Right,
Forward,
Backward,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CameraFace {
Side,
Front,
Back,
Top,
Bottom,
Right,
Left,
}
impl CameraFace {
fn direction_up(self) -> (Vec3, Vec3) {
match self {
CameraFace::Side => (Vec3::new(-1.0, -1.0, -1.0), Vec3::new(0.0, 1.0, 0.0)),
CameraFace::Front => (Vec3::new(0.0, 0.0, -1.0), Vec3::new(0.0, 1.0, 0.0)),
CameraFace::Back => (Vec3::new(0.0, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0)),
CameraFace::Top => (Vec3::new(0.0, -1.0, 0.0), Vec3::new(0.0, 0.0, -1.0)),
CameraFace::Bottom => (Vec3::new(0.0, 1.0, 0.0), Vec3::new(0.0, 0.0, 1.0)),
CameraFace::Right => (Vec3::new(-1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0)),
CameraFace::Left => (Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0)),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct CameraExtrinsic {
position: Vec3,
direction: Vec3,
up: Vec3,
side: Vec3,
}
impl Default for CameraExtrinsic {
fn default() -> Self {
CameraExtrinsic::new(
Vec3::ZERO,
Vec3::new(0.0, 0.0, -1.0),
Vec3::new(0.0, 1.0, 0.0),
)
}
}
impl CameraExtrinsic {
pub fn new(position: Vec3, direction: Vec3, up: Vec3) -> Self {
let mut e = CameraExtrinsic {
position,
direction: Vec3::new(0.0, 0.0, -1.0),
up: Vec3::new(0.0, 1.0, 0.0),
side: Vec3::new(1.0, 0.0, 0.0),
};
e.set_orientation(Some(direction), Some(up));
e
}
pub fn set_orientation(&mut self, direction: Option<Vec3>, up: Option<Vec3>) -> bool {
let direction = match direction {
Some(d) => d.normalized(),
None => self.direction,
};
let up = up.unwrap_or(self.up);
let side = direction.cross(up);
let sidenormal = side.length();
if sidenormal == 0.0 {
return false;
}
let side = side * (1.0 / sidenormal);
let up = side.cross(direction).normalized();
self.side = side;
self.up = up;
self.direction = direction;
true
}
pub fn position(&self) -> Vec3 {
self.position
}
pub fn set_position(&mut self, position: Vec3) {
self.position = position;
}
pub fn direction(&self) -> Vec3 {
self.direction
}
pub fn set_direction(&mut self, direction: Vec3) -> bool {
self.set_orientation(Some(direction), None)
}
pub fn up(&self) -> Vec3 {
self.up
}
pub fn set_up(&mut self, up: Vec3) -> bool {
self.set_orientation(None, Some(up))
}
pub fn side(&self) -> Vec3 {
self.side
}
pub fn matrix(&self) -> Mat4 {
mat4_look_at_dir(self.position, self.direction, self.up)
}
pub fn move_to(&mut self, direction: CameraMove, step: f32) {
let vector = match direction {
CameraMove::Up => self.up,
CameraMove::Down => -self.up,
CameraMove::Right => self.side,
CameraMove::Left => -self.side,
CameraMove::Forward => self.direction,
CameraMove::Backward => -self.direction,
};
self.position += vector * step;
}
pub fn rotate(&mut self, direction: CameraDirection, angle: f32) -> bool {
let axis = match direction {
CameraDirection::Up => self.side,
CameraDirection::Down => -self.side,
CameraDirection::Left => self.up,
CameraDirection::Right => -self.up,
};
let m = mat4_rotate(angle.to_radians(), axis.x, axis.y, axis.z);
let newdir = m.transform_dir(self.direction);
match direction {
CameraDirection::Up | CameraDirection::Down => {
let newup = m.transform_dir(self.up);
self.set_orientation(Some(newdir), Some(newup))
}
CameraDirection::Left | CameraDirection::Right => self.set_direction(newdir),
}
}
pub fn orbit(&mut self, direction: CameraDirection, center: Vec3, angle: f32) -> bool {
let axis = match direction {
CameraDirection::Down => self.side,
CameraDirection::Up => -self.side,
CameraDirection::Right => self.up,
CameraDirection::Left => -self.up,
};
let rotmatrix = mat4_rotate(angle.to_radians(), axis.x, axis.y, axis.z);
let newdir = rotmatrix.transform_dir(self.direction);
if !self.set_direction(newdir) {
return false;
}
let matrix = mat4_translate(center.x, center.y, center.z)
* rotmatrix
* mat4_translate(-center.x, -center.y, -center.z);
self.position = matrix.transform_point(self.position, false);
true
}
pub fn reset(&mut self, face: CameraFace) {
let distance = self.position.length();
let (direction, up) = face.direction_up();
self.set_orientation(Some(direction), Some(up));
self.position = (-self.direction) * distance;
}
}
fn signum0(v: f32) -> f32 {
if v > 0.0 {
1.0
} else if v < 0.0 {
-1.0
} else {
0.0
}
}
#[derive(Clone, Copy, Debug)]
pub struct Orthographic {
left: f32,
right: f32,
bottom: f32,
top: f32,
near: f32,
far: f32,
size: (f32, f32),
keepaspect: bool,
}
impl Orthographic {
pub fn new(clip: [f32; 4], near: f32, far: f32, size: (f32, f32), keepaspect: bool) -> Self {
let [left, right, bottom, top] = clip;
let mut o = Orthographic {
left,
right,
bottom,
top,
near,
far,
size,
keepaspect,
};
o.update(left, right, bottom, top);
o
}
fn update(&mut self, mut left: f32, mut right: f32, mut bottom: f32, mut top: f32) {
if self.keepaspect {
let (width, height) = self.size;
let aspect = width / height;
let orthoaspect = (left - right).abs() / (bottom - top).abs();
if orthoaspect >= aspect {
let newheight = signum0(top - bottom) * (left - right).abs() / aspect;
bottom = 0.5 * (bottom + top) - 0.5 * newheight;
top = bottom + newheight;
} else {
let newwidth = signum0(right - left) * (bottom - top).abs() * aspect;
left = 0.5 * (left + right) - 0.5 * newwidth;
right = left + newwidth;
}
}
self.left = left;
self.right = right;
self.bottom = bottom;
self.top = top;
}
pub fn set_clipping(&mut self, left: f32, right: f32, bottom: f32, top: f32) {
self.update(left, right, bottom, top);
}
pub fn set_size(&mut self, size: (f32, f32)) {
if size != self.size {
self.size = size;
self.update(self.left, self.right, self.bottom, self.top);
}
}
pub fn matrix(&self) -> Mat4 {
mat4_orthographic(
self.left,
self.right,
self.bottom,
self.top,
self.near,
self.far,
)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Perspective {
fovy: f32,
near: f32,
far: f32,
size: (f32, f32),
}
impl Perspective {
pub fn new(fovy: f32, near: f32, far: f32, size: (f32, f32)) -> Self {
Perspective {
fovy,
near,
far,
size,
}
}
pub fn set_size(&mut self, size: (f32, f32)) {
self.size = size;
}
pub fn matrix(&self) -> Mat4 {
let (w, h) = self.size;
mat4_perspective(self.fovy, w, h, self.near, self.far)
}
}
#[derive(Clone, Copy, Debug)]
pub enum Projection {
Perspective(Perspective),
Orthographic(Orthographic),
}
impl Projection {
pub fn matrix(&self) -> Mat4 {
match self {
Projection::Perspective(p) => p.matrix(),
Projection::Orthographic(o) => o.matrix(),
}
}
fn set_size(&mut self, size: (f32, f32)) {
match self {
Projection::Perspective(p) => p.set_size(size),
Projection::Orthographic(o) => o.set_size(size),
}
}
fn set_depth_extent(&mut self, near: f32, far: f32) {
match self {
Projection::Perspective(p) => {
p.near = near;
p.far = far;
}
Projection::Orthographic(o) => {
o.near = near;
o.far = far;
}
}
}
fn size(&self) -> (f32, f32) {
match self {
Projection::Perspective(p) => p.size,
Projection::Orthographic(o) => o.size,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Camera {
pub intrinsic: Projection,
pub extrinsic: CameraExtrinsic,
}
impl Camera {
pub fn new(
fovy: f32,
near: f32,
far: f32,
size: (f32, f32),
position: Vec3,
direction: Vec3,
up: Vec3,
) -> Self {
Camera {
intrinsic: Projection::Perspective(Perspective::new(fovy, near, far, size)),
extrinsic: CameraExtrinsic::new(position, direction, up),
}
}
pub fn matrix(&self) -> Mat4 {
self.intrinsic.matrix() * self.extrinsic.matrix()
}
pub fn set_size(&mut self, size: (f32, f32)) {
self.intrinsic.set_size(size);
}
pub fn reset_camera(&mut self, bounds: (Vec3, Vec3)) {
let (min, max) = bounds;
let center = (min + max) * 0.5;
let mut radius = ((max - min) * 0.5).length();
if radius == 0.0 {
radius = 1.0;
}
match &mut self.intrinsic {
Projection::Perspective(p) => {
let mut minfov = p.fovy.to_radians();
let (width, height) = p.size;
if width < height {
minfov *= width / height;
}
let offset = radius / (0.5 * minfov).sin();
self.extrinsic.position = center - self.extrinsic.direction * offset;
self.intrinsic
.set_depth_extent(offset - radius, offset + radius);
}
Projection::Orthographic(o) => {
o.set_clipping(
center.x - radius,
center.x + radius,
center.y - radius,
center.y + radius,
);
self.extrinsic.position = Vec3::ZERO;
self.intrinsic
.set_depth_extent(center.z - radius, center.z + radius);
}
}
}
pub fn size(&self) -> (f32, f32) {
self.intrinsic.size()
}
pub fn pan(&mut self, from_ndc: Vec3, to_ndc: Vec3) {
if let Some(inv) = self.matrix().inverse() {
let scene = inv.transform_point(to_ndc, true);
let last = inv.transform_point(from_ndc, true);
let translation = scene - last;
self.extrinsic.position -= translation;
}
}
pub fn zoom_at(&mut self, ndc: (f32, f32), ndc_z: f32, zoom_in: bool) {
let step = 0.2 * if zoom_in { -1.0 } else { 1.0 };
match self.intrinsic {
Projection::Perspective(_) => {
let position = Vec3::new(ndc.0, ndc.1, ndc_z);
if let Some(inv) = self.matrix().inverse() {
let positionscene = inv.transform_point(position, true);
let camtopos = self.extrinsic.position - positionscene;
self.extrinsic.position += camtopos * step;
}
}
Projection::Orthographic(mut o) => {
let dx = (ndc.0 + 1.0) / 2.0;
let stepwidth = step * (o.right - o.left);
let left = o.left - dx * stepwidth;
let right = o.right + (1.0 - dx) * stepwidth;
let dy = (ndc.1 + 1.0) / 2.0;
let stepheight = step * (o.top - o.bottom);
let bottom = o.bottom - dy * stepheight;
let top = o.top + (1.0 - dy) * stepheight;
o.set_clipping(left, right, bottom, top);
self.intrinsic = Projection::Orthographic(o);
}
}
}
pub fn adjust_depth_extent(&mut self, bounds: (Vec3, Vec3)) {
let (min, max) = bounds;
let corners = [
Vec3::new(min.x, min.y, min.z),
Vec3::new(max.x, min.y, min.z),
Vec3::new(min.x, max.y, min.z),
Vec3::new(max.x, max.y, min.z),
Vec3::new(min.x, min.y, max.z),
Vec3::new(max.x, min.y, max.z),
Vec3::new(min.x, max.y, max.z),
Vec3::new(max.x, max.y, max.z),
];
let ext = self.extrinsic.matrix();
let mut zmin = f32::INFINITY;
let mut zmax = f32::NEG_INFINITY;
for c in corners {
let z = ext.transform_point(c, false).z;
zmin = zmin.min(z);
zmax = zmax.max(z);
}
match self.intrinsic {
Projection::Perspective(_) => {
let near_dist = -zmax;
let far_dist = -zmin;
let zextent = (far_dist - near_dist).abs().max(0.0001);
let near = (zextent / 1000.0).max(0.95 * near_dist);
let far = (near + 0.1).max(1.05 * far_dist);
self.intrinsic.set_depth_extent(near, far);
}
Projection::Orthographic(_) => {
let border = zmin.abs().max(zmax.abs());
self.intrinsic.set_depth_extent(-border, border);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) {
assert!((a - b).abs() < 1e-4, "{a} != {b}");
}
fn approx_vec(a: Vec3, b: Vec3) {
approx(a.x, b.x);
approx(a.y, b.y);
approx(a.z, b.z);
}
#[test]
fn extrinsic_builds_orthonormal_basis() {
let e = CameraExtrinsic::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),
);
approx_vec(e.side(), Vec3::new(1.0, 0.0, 0.0));
approx(e.direction().length(), 1.0);
approx(e.up().length(), 1.0);
approx(e.direction().dot(e.up()), 0.0);
approx(e.direction().dot(e.side()), 0.0);
approx(e.up().dot(e.side()), 0.0);
}
#[test]
fn set_orientation_rejects_parallel() {
let mut e = CameraExtrinsic::default();
let before = e.direction();
assert!(!e.set_orientation(
Some(Vec3::new(0.0, 1.0, 0.0)),
Some(Vec3::new(0.0, 1.0, 0.0))
));
approx_vec(e.direction(), before);
}
#[test]
fn orbit_full_turn_returns_to_start() {
let mut e = CameraExtrinsic::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 start = e.position();
for _ in 0..36 {
assert!(e.orbit(CameraDirection::Left, Vec3::ZERO, 10.0));
}
approx_vec(e.position(), start);
}
#[test]
fn orbit_left_90_moves_camera_to_side() {
let mut e = CameraExtrinsic::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),
);
assert!(e.orbit(CameraDirection::Right, Vec3::ZERO, 90.0));
approx(e.position().length(), 5.0);
approx(e.direction().dot(e.position().normalized()), -1.0);
}
#[test]
fn reset_top_looks_down() {
let mut e = CameraExtrinsic::new(
Vec3::new(0.0, 0.0, 10.0),
Vec3::new(0.0, 0.0, -1.0),
Vec3::new(0.0, 1.0, 0.0),
);
e.reset(CameraFace::Top);
approx_vec(e.direction(), Vec3::new(0.0, -1.0, 0.0));
approx_vec(e.position(), Vec3::new(0.0, 10.0, 0.0));
}
#[test]
fn reset_camera_perspective_frames_unit_box() {
let mut cam = Camera::new(
30.0,
0.1,
10.0,
(100.0, 100.0),
Vec3::ZERO,
Vec3::new(0.0, 0.0, -1.0),
Vec3::new(0.0, 1.0, 0.0),
);
let bounds = (Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
cam.reset_camera(bounds);
assert!(cam.extrinsic.position().z > 0.0);
let mvp = cam.matrix();
let center = mvp.transform_point(Vec3::ZERO, true);
approx(center.x, 0.0);
approx(center.y, 0.0);
for &x in &[-1.0f32, 1.0] {
for &y in &[-1.0f32, 1.0] {
for &z in &[-1.0f32, 1.0] {
let ndc = mvp.transform_point(Vec3::new(x, y, z), true);
assert!(ndc.x.abs() <= 1.001, "x ndc {} out of range", ndc.x);
assert!(ndc.y.abs() <= 1.001, "y ndc {} out of range", ndc.y);
assert!(ndc.z.abs() <= 1.001, "z ndc {} out of range", ndc.z);
}
}
}
}
#[test]
fn reset_camera_collapsed_bounds_uses_unit_radius() {
let mut cam = Camera::new(
30.0,
0.1,
10.0,
(100.0, 100.0),
Vec3::ZERO,
Vec3::new(0.0, 0.0, -1.0),
Vec3::new(0.0, 1.0, 0.0),
);
cam.reset_camera((Vec3::ZERO, Vec3::ZERO));
assert!(cam.extrinsic.position().z.is_finite());
assert!(cam.extrinsic.position().z > 0.0);
}
#[test]
fn orthographic_keepaspect_enlarges_to_match_viewport() {
let o = Orthographic::new([-1.0, 1.0, -1.0, 1.0], -1.0, 1.0, (200.0, 100.0), true);
let m = o.matrix();
approx(m.transform_point(Vec3::new(2.0, 0.0, 0.0), false).x, 1.0);
approx(m.transform_point(Vec3::new(0.0, 1.0, 0.0), false).y, 1.0);
}
#[test]
fn reset_camera_orthographic_branch() {
let mut cam = Camera {
intrinsic: Projection::Orthographic(Orthographic::new(
[-1.0, 1.0, -1.0, 1.0],
-1.0,
1.0,
(100.0, 100.0),
true,
)),
extrinsic: CameraExtrinsic::default(),
};
cam.reset_camera((Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0)));
approx_vec(cam.extrinsic.position(), Vec3::ZERO);
let ndc = cam.matrix().transform_point(Vec3::ZERO, false);
approx(ndc.x, 0.0);
approx(ndc.y, 0.0);
}
fn perspective_test_camera() -> Camera {
Camera::new(
30.0,
0.1,
100.0,
(300.0, 300.0),
Vec3::new(0.0, 0.0, 5.0),
Vec3::new(0.0, 0.0, -1.0),
Vec3::new(0.0, 1.0, 0.0),
)
}
#[test]
fn zoom_at_perspective_moves_toward_and_away_from_centre() {
let mut cam = perspective_test_camera();
let plane_z = cam.matrix().transform_point(Vec3::ZERO, true).z;
let start = cam.extrinsic.position().length();
cam.zoom_at((0.0, 0.0), plane_z, true);
let after_in = cam.extrinsic.position().length();
assert!(
after_in < start,
"zoom in should reduce distance: {after_in} !< {start}"
);
cam.zoom_at((0.0, 0.0), plane_z, false);
let after_out = cam.extrinsic.position().length();
assert!(after_out > after_in, "zoom out should increase distance");
}
#[test]
fn zoom_at_orthographic_shrinks_clip_on_zoom_in() {
let mut cam = Camera {
intrinsic: Projection::Orthographic(Orthographic::new(
[-2.0, 2.0, -2.0, 2.0],
-10.0,
10.0,
(100.0, 100.0),
false,
)),
extrinsic: CameraExtrinsic::default(),
};
let width = |c: &Camera| match c.intrinsic {
Projection::Orthographic(o) => o.matrix(),
_ => unreachable!(),
};
let before = width(&cam).rows[0][0];
cam.zoom_at((0.0, 0.0), 0.0, true);
let after = width(&cam).rows[0][0];
assert!(
after > before,
"ortho zoom-in should narrow clip: {after} !> {before}"
);
}
#[test]
fn zoom_at_picked_depth_keeps_the_picked_pixel_invariant() {
let mut cam = perspective_test_camera();
let picked = Vec3::new(0.6, -0.4, 1.0); let ndc0 = cam.matrix().transform_point(picked, true);
cam.zoom_at((ndc0.x, ndc0.y), ndc0.z, true);
let ndc1 = cam.matrix().transform_point(picked, true);
approx(ndc1.x, ndc0.x);
approx(ndc1.y, ndc0.y);
assert!(
(cam.extrinsic.position() - picked).length()
< (Vec3::new(0.0, 0.0, 5.0) - picked).length(),
"zoom in must approach the picked point"
);
cam.zoom_at(
(ndc1.x, ndc1.y),
cam.matrix().transform_point(picked, true).z,
false,
);
let ndc2 = cam.matrix().transform_point(picked, true);
approx(ndc2.x, ndc0.x);
approx(ndc2.y, ndc0.y);
}
#[test]
fn adjust_depth_extent_brackets_the_bounds() {
let mut cam = perspective_test_camera();
let bounds = (Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
cam.adjust_depth_extent(bounds);
let (min, max) = bounds;
for &x in &[min.x, max.x] {
for &y in &[min.y, max.y] {
for &z in &[min.z, max.z] {
let ndc = cam.matrix().transform_point(Vec3::new(x, y, z), true);
assert!(
(-1.0001..=1.0001).contains(&ndc.z),
"corner ({x},{y},{z}) ndc z {} outside frustum",
ndc.z
);
}
}
}
}
}