Skip to main content

yog_graphics/
math.rs

1//! Vectors and 4×4 matrices (column-major, as JOML and the hosts'
2//! `PoseStack` keep them).
3
4use std::ops::{Add, Mul, Neg, Sub};
5
6/// A point or a direction in a plane.
7#[derive(Debug, Clone, Copy, PartialEq, Default)]
8pub struct Vec2 {
9    pub x: f32,
10    pub y: f32,
11}
12
13impl Vec2 {
14    pub const ZERO: Self = Self::new(0.0, 0.0);
15
16    pub const fn new(x: f32, y: f32) -> Self {
17        Self { x, y }
18    }
19
20    pub fn dot(self, o: Self) -> f32 {
21        self.x * o.x + self.y * o.y
22    }
23
24    pub fn length(self) -> f32 {
25        self.dot(self).sqrt()
26    }
27
28    pub fn distance(self, o: Self) -> f32 {
29        (self - o).length()
30    }
31
32    /// This one scaled to length 1 (zero stays zero).
33    pub fn normalize(self) -> Self {
34        let l = self.length();
35        if l < 1e-6 { self } else { self * (1.0 / l) }
36    }
37
38    pub fn lerp(self, to: Self, t: f32) -> Self {
39        self + (to - self) * t
40    }
41
42    /// Turned a quarter left (`y` up) — a line's normal.
43    pub fn perp(self) -> Self {
44        Self::new(-self.y, self.x)
45    }
46}
47
48impl Add for Vec2 {
49    type Output = Self;
50    fn add(self, o: Self) -> Self {
51        Self::new(self.x + o.x, self.y + o.y)
52    }
53}
54
55impl Sub for Vec2 {
56    type Output = Self;
57    fn sub(self, o: Self) -> Self {
58        Self::new(self.x - o.x, self.y - o.y)
59    }
60}
61
62impl Mul<f32> for Vec2 {
63    type Output = Self;
64    fn mul(self, k: f32) -> Self {
65        Self::new(self.x * k, self.y * k)
66    }
67}
68
69impl Neg for Vec2 {
70    type Output = Self;
71    fn neg(self) -> Self {
72        Self::new(-self.x, -self.y)
73    }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Default)]
77pub struct Vec3 {
78    pub x: f32,
79    pub y: f32,
80    pub z: f32,
81}
82
83impl Vec3 {
84    pub const ZERO: Self = Self::new(0.0, 0.0, 0.0);
85    pub const ONE: Self = Self::new(1.0, 1.0, 1.0);
86
87    pub const fn new(x: f32, y: f32, z: f32) -> Self {
88        Self { x, y, z }
89    }
90
91    pub fn from_array(a: [f32; 3]) -> Self {
92        Self::new(a[0], a[1], a[2])
93    }
94
95    pub fn to_array(self) -> [f32; 3] {
96        [self.x, self.y, self.z]
97    }
98
99    pub fn dot(self, o: Self) -> f32 {
100        self.x * o.x + self.y * o.y + self.z * o.z
101    }
102
103    pub fn cross(self, o: Self) -> Self {
104        Self::new(self.y * o.z - self.z * o.y, self.z * o.x - self.x * o.z, self.x * o.y - self.y * o.x)
105    }
106
107    pub fn length(self) -> f32 {
108        self.dot(self).sqrt()
109    }
110
111    /// This one scaled to length 1 (zero stays zero).
112    pub fn normalize(self) -> Self {
113        let l = self.length();
114        if l < 1e-6 { self } else { self * (1.0 / l) }
115    }
116
117    pub fn lerp(self, to: Self, t: f32) -> Self {
118        self + (to - self) * t
119    }
120
121    /// Component-wise product.
122    pub fn scale(self, o: Self) -> Self {
123        Self::new(self.x * o.x, self.y * o.y, self.z * o.z)
124    }
125}
126
127impl Add for Vec3 {
128    type Output = Self;
129    fn add(self, o: Self) -> Self {
130        Self::new(self.x + o.x, self.y + o.y, self.z + o.z)
131    }
132}
133
134impl Sub for Vec3 {
135    type Output = Self;
136    fn sub(self, o: Self) -> Self {
137        Self::new(self.x - o.x, self.y - o.y, self.z - o.z)
138    }
139}
140
141impl Mul<f32> for Vec3 {
142    type Output = Self;
143    fn mul(self, k: f32) -> Self {
144        Self::new(self.x * k, self.y * k, self.z * k)
145    }
146}
147
148impl Neg for Vec3 {
149    type Output = Self;
150    fn neg(self) -> Self {
151        Self::new(-self.x, -self.y, -self.z)
152    }
153}
154
155/// A 4×4 matrix, column-major: `m[col * 4 + row]`.
156#[derive(Debug, Clone, Copy, PartialEq)]
157pub struct Mat4(pub [f32; 16]);
158
159impl Default for Mat4 {
160    fn default() -> Self {
161        Self::IDENTITY
162    }
163}
164
165impl Mat4 {
166    pub const IDENTITY: Self = Self([1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]);
167
168    fn at(&self, row: usize, col: usize) -> f32 {
169        self.0[col * 4 + row]
170    }
171
172    pub fn translation(v: Vec3) -> Self {
173        let mut m = Self::IDENTITY;
174        m.0[12] = v.x;
175        m.0[13] = v.y;
176        m.0[14] = v.z;
177        m
178    }
179
180    pub fn scaling(v: Vec3) -> Self {
181        let mut m = Self::IDENTITY;
182        m.0[0] = v.x;
183        m.0[5] = v.y;
184        m.0[10] = v.z;
185        m
186    }
187
188    /// Turned `a` radians about x (right-handed, as `Axis.XP`).
189    pub fn rotation_x(a: f32) -> Self {
190        let (s, c) = a.sin_cos();
191        Self([1.0, 0.0, 0.0, 0.0, 0.0, c, s, 0.0, 0.0, -s, c, 0.0, 0.0, 0.0, 0.0, 1.0])
192    }
193
194    pub fn rotation_y(a: f32) -> Self {
195        let (s, c) = a.sin_cos();
196        Self([c, 0.0, -s, 0.0, 0.0, 1.0, 0.0, 0.0, s, 0.0, c, 0.0, 0.0, 0.0, 0.0, 1.0])
197    }
198
199    pub fn rotation_z(a: f32) -> Self {
200        let (s, c) = a.sin_cos();
201        Self([c, s, 0.0, 0.0, -s, c, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0])
202    }
203
204    /// JOML's `rotationZYX(z, y, x)` (a model part's turn) — z, then y,
205    /// then x applied to the vector last.
206    pub fn rotation_zyx(r: Vec3) -> Self {
207        Self::rotation_z(r.z) * Self::rotation_y(r.y) * Self::rotation_x(r.x)
208    }
209
210    pub fn transform_point(&self, p: Vec3) -> Vec3 {
211        Vec3::new(
212            self.at(0, 0) * p.x + self.at(0, 1) * p.y + self.at(0, 2) * p.z + self.at(0, 3),
213            self.at(1, 0) * p.x + self.at(1, 1) * p.y + self.at(1, 2) * p.z + self.at(1, 3),
214            self.at(2, 0) * p.x + self.at(2, 1) * p.y + self.at(2, 2) * p.z + self.at(2, 3),
215        )
216    }
217
218    /// A direction: turned and scaled, not moved.
219    pub fn transform_dir(&self, d: Vec3) -> Vec3 {
220        Vec3::new(
221            self.at(0, 0) * d.x + self.at(0, 1) * d.y + self.at(0, 2) * d.z,
222            self.at(1, 0) * d.x + self.at(1, 1) * d.y + self.at(1, 2) * d.z,
223            self.at(2, 0) * d.x + self.at(2, 1) * d.y + self.at(2, 2) * d.z,
224        )
225    }
226
227    /// A normal, kept a unit vector (good for rotations and uniform scales,
228    /// which is what bodies use).
229    pub fn transform_normal(&self, n: Vec3) -> Vec3 {
230        self.transform_dir(n).normalize()
231    }
232}
233
234impl Mul for Mat4 {
235    type Output = Self;
236    fn mul(self, o: Self) -> Self {
237        let mut r = [0.0; 16];
238        for col in 0..4 {
239            for row in 0..4 {
240                r[col * 4 + row] = (0..4).map(|k| self.at(row, k) * o.at(k, col)).sum();
241            }
242        }
243        Self(r)
244    }
245}
246
247/// Degrees to radians, for the formats (which are in degrees).
248pub fn rad(deg: Vec3) -> Vec3 {
249    deg * (std::f32::consts::PI / 180.0)
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    fn close(a: Vec3, b: Vec3) -> bool {
257        (a - b).length() < 1e-5
258    }
259
260    #[test]
261    fn rotations_are_right_handed() {
262        let half = std::f32::consts::FRAC_PI_2;
263        assert!(close(Mat4::rotation_x(half).transform_point(Vec3::new(0.0, 1.0, 0.0)), Vec3::new(0.0, 0.0, 1.0)));
264        assert!(close(Mat4::rotation_y(half).transform_point(Vec3::new(0.0, 0.0, 1.0)), Vec3::new(1.0, 0.0, 0.0)));
265        assert!(close(Mat4::rotation_z(half).transform_point(Vec3::new(1.0, 0.0, 0.0)), Vec3::new(0.0, 1.0, 0.0)));
266    }
267
268    #[test]
269    fn moving_comes_after_turning() {
270        let m = Mat4::translation(Vec3::new(5.0, 0.0, 0.0)) * Mat4::rotation_zyx(Vec3::new(0.0, 0.0, std::f32::consts::FRAC_PI_2)) * Mat4::scaling(Vec3::ONE);
271        assert!(close(m.transform_point(Vec3::new(1.0, 0.0, 0.0)), Vec3::new(5.0, 1.0, 0.0)));
272    }
273}