Skip to main content

concinnity_render/
mat.rs

1//! The view and orthographic-projection builders the shadow passes share
2//! (`csm.rs` for the directional cascades, `spot_shadow.rs` for the spot slices).
3//! Right-handed with depth mapped to [0, 1], matching
4//! [`concinnity_core::gfx::projection`], so the matrices built here are valid for
5//! every backend's shadow sampling.
6
7use concinnity_core::math::sqrt;
8use concinnity_core::math::vec3::{cross, dot, sub};
9
10/// The 4x4 identity, column-major.
11pub const IDENTITY4: [[f32; 4]; 4] = [
12    [1.0, 0.0, 0.0, 0.0],
13    [0.0, 1.0, 0.0, 0.0],
14    [0.0, 0.0, 1.0, 0.0],
15    [0.0, 0.0, 0.0, 1.0],
16];
17
18pub(crate) fn look_at(eye: [f32; 3], centre: [f32; 3], up: [f32; 3]) -> [[f32; 4]; 4] {
19    let f = normalize3(sub(centre, eye));
20    let r = normalize3(cross(f, up));
21    let u = cross(r, f);
22    [
23        [r[0], u[0], -f[0], 0.0],
24        [r[1], u[1], -f[1], 0.0],
25        [r[2], u[2], -f[2], 0.0],
26        [-dot(r, eye), -dot(u, eye), dot(f, eye), 1.0],
27    ]
28}
29
30// Right-handed orthographic projection with depth mapped to [0, 1].
31pub(crate) fn ortho_rh(
32    left: f32,
33    right: f32,
34    bottom: f32,
35    top: f32,
36    near: f32,
37    far: f32,
38) -> [[f32; 4]; 4] {
39    let rml = right - left;
40    let tmb = top - bottom;
41    let fmn = far - near;
42    [
43        [2.0 / rml, 0.0, 0.0, 0.0],
44        [0.0, 2.0 / tmb, 0.0, 0.0],
45        [0.0, 0.0, -1.0 / fmn, 0.0],
46        [
47            -(right + left) / rml,
48            -(top + bottom) / tmb,
49            -near / fmn,
50            1.0,
51        ],
52    ]
53}
54
55// Unit-length `v`, with the length floored so a degenerate input yields a huge
56// but finite vector rather than NaNs in the shadow basis.
57pub(crate) fn normalize3(v: [f32; 3]) -> [f32; 3] {
58    let len = sqrt(dot(v, v)).max(1e-6);
59    [v[0] / len, v[1] / len, v[2] / len]
60}
61
62// An axis not parallel to `dir`, for building a look-at basis. Cone axes are
63// commonly straight up or down, where the usual +Y up vector is degenerate.
64pub(crate) fn up_for(dir: [f32; 3]) -> [f32; 3] {
65    if dir[1].abs() > 0.99 {
66        [0.0, 0.0, 1.0]
67    } else {
68        [0.0, 1.0, 0.0]
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    fn transform(m: [[f32; 4]; 4], p: [f32; 3]) -> [f32; 4] {
77        let mut out = [0.0_f32; 4];
78        for row in 0..4 {
79            out[row] = m[0][row] * p[0] + m[1][row] * p[1] + m[2][row] * p[2] + m[3][row];
80        }
81        out
82    }
83
84    #[test]
85    fn look_at_puts_the_eye_at_the_origin_looking_down_negative_z() {
86        let v = look_at([0.0, 5.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
87        let eye = transform(v, [0.0, 5.0, 0.0]);
88        assert!(eye[0].abs() < 1e-5 && eye[1].abs() < 1e-5 && eye[2].abs() < 1e-5);
89        // The target sits 5 units ahead, i.e. at -5 on the view Z axis.
90        let target = transform(v, [0.0, 0.0, 0.0]);
91        assert!((target[2] + 5.0).abs() < 1e-5);
92    }
93
94    // A straight-up or straight-down axis must not pick a parallel up vector,
95    // or the look-at basis collapses.
96    #[test]
97    fn up_for_avoids_a_degenerate_basis() {
98        assert_eq!(up_for([0.0, -1.0, 0.0]), [0.0, 0.0, 1.0]);
99        assert_eq!(up_for([0.0, 1.0, 0.0]), [0.0, 0.0, 1.0]);
100        assert_eq!(up_for([1.0, 0.0, 0.0]), [0.0, 1.0, 0.0]);
101        // The chosen up is never parallel to the axis.
102        for dir in [[0.0, -1.0, 0.0], [0.3, -0.9, 0.2], [1.0, 0.0, 0.0]] {
103            let d = normalize3(dir);
104            assert!(dot(d, up_for(d)).abs() < 0.999);
105        }
106    }
107}