use crate::gfx::transform::Mat4;
use crate::math::vec3::{cross, dot, sub};
use crate::math::{sqrt, tan};
const MIN_HALF_FOV_TAN: f32 = 1.0e-6;
pub fn perspective_rh(fov_y_radians: f32, aspect: f32, near: f32, far: f32) -> Mat4 {
let ys = 1.0 / tan(fov_y_radians * 0.5).max(MIN_HALF_FOV_TAN);
let xs = ys / aspect;
let zs = far / (near - far);
[
[xs, 0.0, 0.0, 0.0],
[0.0, ys, 0.0, 0.0],
[0.0, 0.0, zs, -1.0],
[0.0, 0.0, zs * near, 0.0],
]
}
pub fn ortho_rh(left: f32, right: f32, bottom: f32, top: f32, near: f32, far: f32) -> Mat4 {
let rml = right - left;
let tmb = top - bottom;
let fmn = far - near;
[
[2.0 / rml, 0.0, 0.0, 0.0],
[0.0, 2.0 / tmb, 0.0, 0.0],
[0.0, 0.0, -1.0 / fmn, 0.0],
[
-(right + left) / rml,
-(top + bottom) / tmb,
-near / fmn,
1.0,
],
]
}
pub fn view_from_basis(eye: [f32; 3], right: [f32; 3], up: [f32; 3], forward: [f32; 3]) -> Mat4 {
[
[right[0], up[0], -forward[0], 0.0],
[right[1], up[1], -forward[1], 0.0],
[right[2], up[2], -forward[2], 0.0],
[-dot(right, eye), -dot(up, eye), dot(forward, eye), 1.0],
]
}
pub fn look_at(eye: [f32; 3], centre: [f32; 3], up: [f32; 3]) -> Mat4 {
let f = normalize3(sub(centre, eye));
let r = normalize3(cross(f, up));
view_from_basis(eye, r, cross(r, f), f)
}
pub fn normalize3(v: [f32; 3]) -> [f32; 3] {
let len = sqrt(dot(v, v)).max(1e-6);
[v[0] / len, v[1] / len, v[2] / len]
}
pub fn up_for(dir: [f32; 3]) -> [f32; 3] {
if dir[1].abs() > 0.99 {
[0.0, 0.0, 1.0]
} else {
[0.0, 1.0, 0.0]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn transform(m: Mat4, p: [f32; 3]) -> [f32; 4] {
let mut out = [0.0f32; 4];
for (row, o) in out.iter_mut().enumerate() {
*o = m[0][row] * p[0] + m[1][row] * p[1] + m[2][row] * p[2] + m[3][row];
}
out
}
#[test]
fn near_and_far_map_to_zero_and_one() {
let p = perspective_rh(75.0f32.to_radians(), 1.6, 0.1, 500.0);
let near = transform(p, [0.0, 0.0, -0.1]);
let far = transform(p, [0.0, 0.0, -500.0]);
assert!((near[2] / near[3]).abs() < 1e-4);
assert!((far[2] / far[3] - 1.0).abs() < 1e-4);
}
#[test]
fn the_frustum_edge_lands_on_the_ndc_boundary() {
let p = perspective_rh(90.0f32.to_radians(), 1.0, 0.1, 50.0);
let edge = transform(p, [10.0, 0.0, -10.0]);
assert!((edge[0] / edge[3] - 1.0).abs() < 1e-4);
}
#[test]
fn a_degenerate_fov_stays_finite() {
let p = perspective_rh(0.0, 1.0, 0.1, 100.0);
assert!(p.iter().flatten().all(|v| v.is_finite()));
}
#[test]
fn the_reassociated_form_agrees_to_within_one_ulp() {
fn other(fov_y_radians: f32, aspect: f32, near: f32, far: f32) -> Mat4 {
let t = tan(fov_y_radians * 0.5).max(MIN_HALF_FOV_TAN);
let fmn = far - near;
[
[1.0 / (aspect * t), 0.0, 0.0, 0.0],
[0.0, 1.0 / t, 0.0, 0.0],
[0.0, 0.0, -far / fmn, -1.0],
[0.0, 0.0, -(far * near) / fmn, 0.0],
]
}
for fov_deg in [10.0f32, 45.0, 60.0, 75.0, 90.0, 140.0, 179.0] {
for aspect in [0.5f32, 1.0, 1.6, 2.35, 3.0] {
for near in [0.01f32, 0.1, 1.0] {
for far in [10.0f32, 500.0, 10_000.0] {
let fov = fov_deg.to_radians();
let a = perspective_rh(fov, aspect, near, far);
let b = other(fov, aspect, near, far);
for col in 0..4 {
for row in 0..4 {
let ulps = (i64::from(a[col][row].to_bits())
- i64::from(b[col][row].to_bits()))
.abs();
assert!(
ulps <= 1,
"fov={fov_deg} aspect={aspect} near={near} far={far} \
[{col}][{row}]: {} vs {} ({ulps} ulps)",
a[col][row],
b[col][row]
);
}
}
}
}
}
}
}
#[test]
fn ortho_maps_the_box_onto_the_ndc_cube() {
let p = ortho_rh(-2.0, 2.0, -1.0, 1.0, 1.0, 11.0);
let near = transform(p, [0.0, 0.0, -1.0]);
let far = transform(p, [2.0, 1.0, -11.0]);
assert!(near[2].abs() < 1e-5, "near depth {}", near[2]);
assert!((far[2] - 1.0).abs() < 1e-5, "far depth {}", far[2]);
assert!((far[0] - 1.0).abs() < 1e-5 && (far[1] - 1.0).abs() < 1e-5);
}
#[test]
fn look_at_puts_the_eye_at_the_origin_looking_down_negative_z() {
let v = look_at([0.0, 5.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
let eye = transform(v, [0.0, 5.0, 0.0]);
assert!(eye[0].abs() < 1e-5 && eye[1].abs() < 1e-5 && eye[2].abs() < 1e-5);
let target = transform(v, [0.0, 0.0, 0.0]);
assert!((target[2] + 5.0).abs() < 1e-5);
}
#[test]
fn look_at_agrees_with_the_basis_it_derives() {
let eye = [3.0, -1.5, 2.0];
let centre = [0.4, 0.9, -2.0];
let up = [0.0, 1.0, 0.0];
let f = normalize3(sub(centre, eye));
let r = normalize3(cross(f, up));
assert_eq!(
look_at(eye, centre, up),
view_from_basis(eye, r, cross(r, f), f)
);
}
#[test]
fn up_for_avoids_a_degenerate_basis() {
assert_eq!(up_for([0.0, -1.0, 0.0]), [0.0, 0.0, 1.0]);
assert_eq!(up_for([0.0, 1.0, 0.0]), [0.0, 0.0, 1.0]);
assert_eq!(up_for([1.0, 0.0, 0.0]), [0.0, 1.0, 0.0]);
for dir in [[0.0, -1.0, 0.0], [0.3, -0.9, 0.2], [1.0, 0.0, 0.0]] {
let d = normalize3(dir);
assert!(dot(d, up_for(d)).abs() < 0.999);
}
}
#[test]
fn a_degenerate_direction_stays_finite() {
assert!(normalize3([0.0; 3]).iter().all(|v| v.is_finite()));
}
}