Skip to main content

cranpose_ui_graphics/space/
math.rs

1use std::ops::{Add, Mul, Neg, Sub};
2
3/// A point or a direction in a 3D scene. Scene space uses logical pixels with
4/// x to the right, y down and z away from the viewer, so a scene seen from
5/// straight ahead lines up with the screen.
6#[derive(Clone, Copy, Debug, Default, PartialEq)]
7pub struct Vec3 {
8    /// Distance to the right.
9    pub x: f32,
10    /// Distance down.
11    pub y: f32,
12    /// Distance away from the viewer.
13    pub z: f32,
14}
15
16impl Vec3 {
17    /// The origin.
18    pub const ZERO: Self = Self::new(0.0, 0.0, 0.0);
19
20    /// A vector from its three coordinates.
21    pub const fn new(x: f32, y: f32, z: f32) -> Self {
22        Self { x, y, z }
23    }
24
25    /// The vector as an array of its coordinates.
26    pub const fn to_array(self) -> [f32; 3] {
27        [self.x, self.y, self.z]
28    }
29
30    /// The vector from an array of its coordinates.
31    pub const fn from_array([x, y, z]: [f32; 3]) -> Self {
32        Self { x, y, z }
33    }
34
35    /// The dot product.
36    pub fn dot(self, other: Self) -> f32 {
37        self.x * other.x + self.y * other.y + self.z * other.z
38    }
39
40    /// The cross product. In scene space x cross y is z.
41    pub fn cross(self, other: Self) -> Self {
42        Self::new(
43            self.y * other.z - self.z * other.y,
44            self.z * other.x - self.x * other.z,
45            self.x * other.y - self.y * other.x,
46        )
47    }
48
49    /// The vector's length.
50    pub fn length(self) -> f32 {
51        self.dot(self).sqrt()
52    }
53
54    /// The vector scaled to length one, or `None` for a vector of no length.
55    pub fn normalized(self) -> Option<Self> {
56        let length = self.length();
57        (length.is_normal()).then(|| self * (1.0 / length))
58    }
59
60    /// The point `t` of the way from `self` to `other`.
61    pub fn lerp(self, other: Self, t: f32) -> Self {
62        self + (other - self) * t
63    }
64
65    /// The lower of each coordinate of `self` and `other`.
66    pub fn min(self, other: Self) -> Self {
67        Self::new(
68            self.x.min(other.x),
69            self.y.min(other.y),
70            self.z.min(other.z),
71        )
72    }
73
74    /// The higher of each coordinate of `self` and `other`.
75    pub fn max(self, other: Self) -> Self {
76        Self::new(
77            self.x.max(other.x),
78            self.y.max(other.y),
79            self.z.max(other.z),
80        )
81    }
82}
83
84impl Add for Vec3 {
85    type Output = Self;
86
87    fn add(self, other: Self) -> Self {
88        Self::new(self.x + other.x, self.y + other.y, self.z + other.z)
89    }
90}
91
92impl Sub for Vec3 {
93    type Output = Self;
94
95    fn sub(self, other: Self) -> Self {
96        Self::new(self.x - other.x, self.y - other.y, self.z - other.z)
97    }
98}
99
100impl Mul<f32> for Vec3 {
101    type Output = Self;
102
103    fn mul(self, factor: f32) -> Self {
104        Self::new(self.x * factor, self.y * factor, self.z * factor)
105    }
106}
107
108impl Neg for Vec3 {
109    type Output = Self;
110
111    fn neg(self) -> Self {
112        Self::new(-self.x, -self.y, -self.z)
113    }
114}
115
116/// A 4x4 matrix that moves, turns and scales points in 3D. It maps a point
117/// `p` to `M * (p, 1)`, so the last column holds the translation.
118#[derive(Clone, Copy, Debug, PartialEq)]
119pub struct Mat4 {
120    rows: [[f32; 4]; 4],
121}
122
123impl Default for Mat4 {
124    fn default() -> Self {
125        Self::IDENTITY
126    }
127}
128
129impl Mat4 {
130    /// The matrix that leaves every point where it is.
131    pub const IDENTITY: Self = Self::from_rows([
132        [1.0, 0.0, 0.0, 0.0],
133        [0.0, 1.0, 0.0, 0.0],
134        [0.0, 0.0, 1.0, 0.0],
135        [0.0, 0.0, 0.0, 1.0],
136    ]);
137
138    /// The matrix with these rows.
139    pub const fn from_rows(rows: [[f32; 4]; 4]) -> Self {
140        Self { rows }
141    }
142
143    /// The matrix's rows.
144    pub const fn rows(&self) -> [[f32; 4]; 4] {
145        self.rows
146    }
147
148    /// The matrix whose columns map the x, y and z axes onto `x`, `y` and
149    /// `z` and the origin onto `origin`.
150    pub const fn from_axes(x: Vec3, y: Vec3, z: Vec3, origin: Vec3) -> Self {
151        Self::from_rows([
152            [x.x, y.x, z.x, origin.x],
153            [x.y, y.y, z.y, origin.y],
154            [x.z, y.z, z.z, origin.z],
155            [0.0, 0.0, 0.0, 1.0],
156        ])
157    }
158
159    /// Moves every point by `offset`.
160    pub const fn translation(offset: Vec3) -> Self {
161        Self::from_axes(
162            Vec3::new(1.0, 0.0, 0.0),
163            Vec3::new(0.0, 1.0, 0.0),
164            Vec3::new(0.0, 0.0, 1.0),
165            offset,
166        )
167    }
168
169    /// Scales each axis by its factor about the origin.
170    pub const fn scaling(factors: Vec3) -> Self {
171        Self::from_axes(
172            Vec3::new(factors.x, 0.0, 0.0),
173            Vec3::new(0.0, factors.y, 0.0),
174            Vec3::new(0.0, 0.0, factors.z),
175            Vec3::ZERO,
176        )
177    }
178
179    /// Turns points about the x axis by `radians`. A positive angle tips the
180    /// y axis, which points down, away from the viewer.
181    pub fn rotation_x(radians: f32) -> Self {
182        let (sin, cos) = radians.sin_cos();
183        Self::from_axes(
184            Vec3::new(1.0, 0.0, 0.0),
185            Vec3::new(0.0, cos, sin),
186            Vec3::new(0.0, -sin, cos),
187            Vec3::ZERO,
188        )
189    }
190
191    /// Turns points about the y axis by `radians`. A positive angle swings
192    /// the x axis away from the viewer.
193    pub fn rotation_y(radians: f32) -> Self {
194        let (sin, cos) = radians.sin_cos();
195        Self::from_axes(
196            Vec3::new(cos, 0.0, sin),
197            Vec3::new(0.0, 1.0, 0.0),
198            Vec3::new(-sin, 0.0, cos),
199            Vec3::ZERO,
200        )
201    }
202
203    /// Turns points about the z axis by `radians`. A positive angle turns
204    /// clockwise on screen.
205    pub fn rotation_z(radians: f32) -> Self {
206        let (sin, cos) = radians.sin_cos();
207        Self::from_axes(
208            Vec3::new(cos, sin, 0.0),
209            Vec3::new(-sin, cos, 0.0),
210            Vec3::new(0.0, 0.0, 1.0),
211            Vec3::ZERO,
212        )
213    }
214
215    /// This matrix followed by `next`: `next * self`.
216    pub fn then(&self, next: &Self) -> Self {
217        let (a, b) = (next.rows, self.rows);
218        Self::from_rows(std::array::from_fn(|row| {
219            std::array::from_fn(|column| (0..4).map(|k| a[row][k] * b[k][column]).sum())
220        }))
221    }
222
223    /// Maps `point`, dividing by the resulting w.
224    pub fn transform_point(&self, point: Vec3) -> Vec3 {
225        let [x, y, z, w] = self.transform_homogeneous(point);
226        if w == 1.0 || w == 0.0 {
227            Vec3::new(x, y, z)
228        } else {
229            Vec3::new(x / w, y / w, z / w)
230        }
231    }
232
233    /// Maps `point` and returns all four coordinates of the result, before
234    /// any division by w.
235    pub fn transform_homogeneous(&self, point: Vec3) -> [f32; 4] {
236        self.rows
237            .map(|row| row[0] * point.x + row[1] * point.y + row[2] * point.z + row[3])
238    }
239
240    /// Maps a direction: the translation does not apply.
241    pub fn transform_vector(&self, vector: Vec3) -> Vec3 {
242        let [x, y, z, _] = self
243            .rows
244            .map(|row| row[0] * vector.x + row[1] * vector.y + row[2] * vector.z);
245        Vec3::new(x, y, z)
246    }
247
248    /// How long the map makes a unit length, averaged over the three axes:
249    /// the cube root of how much it scales volume.
250    pub fn length_scale(&self) -> f32 {
251        let axis = |axis: Vec3| self.transform_vector(axis).length();
252        (axis(Vec3::new(1.0, 0.0, 0.0))
253            * axis(Vec3::new(0.0, 1.0, 0.0))
254            * axis(Vec3::new(0.0, 0.0, 1.0)))
255        .cbrt()
256    }
257
258    /// The transpose.
259    pub fn transpose(&self) -> Self {
260        Self::from_rows(std::array::from_fn(|row| {
261            std::array::from_fn(|column| self.rows[column][row])
262        }))
263    }
264
265    /// The inverse, or `None` when the matrix flattens space.
266    pub fn inverse(&self) -> Option<Self> {
267        let m = self.rows;
268        let cofactor = |row: usize, column: usize| {
269            let pick = |skip: usize| -> [usize; 3] {
270                let mut picked = [0; 3];
271                let mut next = 0;
272                for index in 0..4 {
273                    if index != skip {
274                        picked[next] = index;
275                        next += 1;
276                    }
277                }
278                picked
279            };
280            let [r0, r1, r2] = pick(row);
281            let [c0, c1, c2] = pick(column);
282            let minor = m[r0][c0] * (m[r1][c1] * m[r2][c2] - m[r1][c2] * m[r2][c1])
283                - m[r0][c1] * (m[r1][c0] * m[r2][c2] - m[r1][c2] * m[r2][c0])
284                + m[r0][c2] * (m[r1][c0] * m[r2][c1] - m[r1][c1] * m[r2][c0]);
285            if (row + column).is_multiple_of(2) {
286                minor
287            } else {
288                -minor
289            }
290        };
291        let cofactors: [[f32; 4]; 4] =
292            std::array::from_fn(|row| std::array::from_fn(|column| cofactor(row, column)));
293        let determinant: f32 = (0..4)
294            .map(|column| m[0][column] * cofactors[0][column])
295            .sum();
296        if !determinant.is_normal() {
297            return None;
298        }
299        let scale = 1.0 / determinant;
300        Some(Self::from_rows(std::array::from_fn(|row| {
301            std::array::from_fn(|column| cofactors[column][row] * scale)
302        })))
303    }
304}