Skip to main content

ling_graphics/
math.rs

1pub use glam::{IVec2, IVec3, Mat3, Mat4, Quat, UVec2, Vec2, Vec3, Vec4};
2
3// ── 4D vector (spatial, not homogeneous) ─────────────────────────────────────
4
5#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
6pub struct Vec4H {
7    pub x: f32,
8    pub y: f32,
9    pub z: f32,
10    pub w: f32,
11}
12
13impl Vec4H {
14    pub const ONE: Self = Self { x: 1.0, y: 1.0, z: 1.0, w: 1.0 };
15    pub const ZERO: Self = Self { x: 0.0, y: 0.0, z: 0.0, w: 0.0 };
16
17    pub fn new(x: f32, y: f32, z: f32, w: f32) -> Self {
18        Self { x, y, z, w }
19    }
20
21    pub fn dot(&self, other: Self) -> f32 {
22        self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w
23    }
24
25    /// Minkowski inner product: -x₀y₀ + x₁y₁ + x₂y₂ + x₃y₃ (hyperboloid model)
26    pub fn minkowski_dot(&self, other: Self) -> f32 {
27        -self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w
28    }
29
30    pub fn length(&self) -> f32 {
31        self.dot(*self).sqrt()
32    }
33
34    pub fn normalize(&self) -> Self {
35        let l = self.length();
36        if l < 1e-8 {
37            return *self;
38        }
39        Self::new(self.x / l, self.y / l, self.z / l, self.w / l)
40    }
41
42    pub fn lerp(&self, other: Self, t: f32) -> Self {
43        Self::new(
44            self.x + (other.x - self.x) * t,
45            self.y + (other.y - self.y) * t,
46            self.z + (other.z - self.z) * t,
47            self.w + (other.w - self.w) * t,
48        )
49    }
50
51    pub fn xyz(&self) -> Vec3 {
52        Vec3::new(self.x, self.y, self.z)
53    }
54}
55
56impl std::ops::Add for Vec4H {
57    type Output = Self;
58
59    fn add(self, r: Self) -> Self {
60        Self::new(self.x + r.x, self.y + r.y, self.z + r.z, self.w + r.w)
61    }
62}
63impl std::ops::Sub for Vec4H {
64    type Output = Self;
65
66    fn sub(self, r: Self) -> Self {
67        Self::new(self.x - r.x, self.y - r.y, self.z - r.z, self.w - r.w)
68    }
69}
70impl std::ops::Mul<f32> for Vec4H {
71    type Output = Self;
72
73    fn mul(self, s: f32) -> Self {
74        Self::new(self.x * s, self.y * s, self.z * s, self.w * s)
75    }
76}
77impl std::ops::Neg for Vec4H {
78    type Output = Self;
79
80    fn neg(self) -> Self {
81        Self::new(-self.x, -self.y, -self.z, -self.w)
82    }
83}
84
85// ── 5×5 matrix for homogeneous 4D transforms ─────────────────────────────────
86
87#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
88pub struct Mat5(pub [[f32; 5]; 5]);
89
90impl Mat5 {
91    pub fn identity() -> Self {
92        let mut m = [[0f32; 5]; 5];
93        for (i, row) in m.iter_mut().enumerate() {
94            row[i] = 1.0;
95        }
96        Self(m)
97    }
98
99    pub fn zero() -> Self {
100        Self([[0f32; 5]; 5])
101    }
102
103    pub fn mul_vec(&self, v: [f32; 5]) -> [f32; 5] {
104        let mut out = [0f32; 5];
105        for (i, out_elem) in out.iter_mut().enumerate() {
106            for (j, v_elem) in v.iter().enumerate() {
107                *out_elem += self.0[i][j] * v_elem;
108            }
109        }
110        out
111    }
112
113    pub fn mul(&self, rhs: &Self) -> Self {
114        let mut out = [[0f32; 5]; 5];
115        for (i, out_row) in out.iter_mut().enumerate() {
116            for (j, out_elem) in out_row.iter_mut().enumerate() {
117                for (k, self_val) in self.0[i].iter().enumerate() {
118                    *out_elem += self_val * rhs.0[k][j];
119                }
120            }
121        }
122        Self(out)
123    }
124
125    pub fn transpose(&self) -> Self {
126        let mut out = [[0f32; 5]; 5];
127        for (i, out_row) in out.iter_mut().enumerate() {
128            for (j, out_elem) in out_row.iter_mut().enumerate() {
129                *out_elem = self.0[j][i];
130            }
131        }
132        Self(out)
133    }
134
135    /// 4D translation matrix (shifts along the w axis of hyperbolic space)
136    pub fn translation_4d(delta: Vec4H) -> Self {
137        let mut m = Self::identity();
138        m.0[0][4] = delta.x;
139        m.0[1][4] = delta.y;
140        m.0[2][4] = delta.z;
141        m.0[3][4] = delta.w;
142        m
143    }
144}
145
146// ── Axis-aligned bounding box ─────────────────────────────────────────────────
147
148#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
149pub struct Aabb {
150    pub min: Vec3,
151    pub max: Vec3,
152}
153
154impl Aabb {
155    pub fn new(min: Vec3, max: Vec3) -> Self {
156        Self { min, max }
157    }
158
159    pub fn from_points(points: &[Vec3]) -> Self {
160        let mut min = Vec3::splat(f32::INFINITY);
161        let mut max = Vec3::splat(f32::NEG_INFINITY);
162        for &p in points {
163            min = min.min(p);
164            max = max.max(p);
165        }
166        Self { min, max }
167    }
168
169    pub fn center(&self) -> Vec3 {
170        (self.min + self.max) * 0.5
171    }
172
173    pub fn half_extents(&self) -> Vec3 {
174        (self.max - self.min) * 0.5
175    }
176
177    pub fn size(&self) -> Vec3 {
178        self.max - self.min
179    }
180
181    pub fn contains(&self, p: Vec3) -> bool {
182        p.x >= self.min.x
183            && p.x <= self.max.x
184            && p.y >= self.min.y
185            && p.y <= self.max.y
186            && p.z >= self.min.z
187            && p.z <= self.max.z
188    }
189
190    pub fn union(&self, other: &Self) -> Self {
191        Self { min: self.min.min(other.min), max: self.max.max(other.max) }
192    }
193
194    pub fn expand(&self, by: f32) -> Self {
195        Self {
196            min: self.min - Vec3::splat(by),
197            max: self.max + Vec3::splat(by),
198        }
199    }
200}
201
202// ── Ray ───────────────────────────────────────────────────────────────────────
203
204#[derive(Debug, Clone, Copy, PartialEq)]
205pub struct Ray3 {
206    pub origin: Vec3,
207    pub direction: Vec3,
208}
209
210impl Ray3 {
211    pub fn new(origin: Vec3, direction: Vec3) -> Self {
212        Self { origin, direction: direction.normalize() }
213    }
214
215    pub fn at(&self, t: f32) -> Vec3 {
216        self.origin + self.direction * t
217    }
218
219    pub fn intersect_sphere(&self, center: Vec3, radius: f32) -> Option<f32> {
220        let oc = self.origin - center;
221        let b = oc.dot(self.direction);
222        let c = oc.dot(oc) - radius * radius;
223        let disc = b * b - c;
224        if disc < 0.0 {
225            return None;
226        }
227        let t = -b - disc.sqrt();
228        if t > 0.0 {
229            Some(t)
230        } else {
231            let t2 = -b + disc.sqrt();
232            if t2 > 0.0 {
233                Some(t2)
234            } else {
235                None
236            }
237        }
238    }
239
240    pub fn intersect_aabb(&self, aabb: &Aabb) -> Option<f32> {
241        let inv_d = Vec3::ONE / self.direction;
242        let t1 = (aabb.min - self.origin) * inv_d;
243        let t2 = (aabb.max - self.origin) * inv_d;
244        let tmin = t1.min(t2);
245        let tmax = t1.max(t2);
246        let enter = tmin.x.max(tmin.y).max(tmin.z);
247        let exit = tmax.x.min(tmax.y).min(tmax.z);
248        if exit >= enter && exit >= 0.0 {
249            Some(enter.max(0.0))
250        } else {
251            None
252        }
253    }
254
255    pub fn intersect_plane(&self, plane: &Plane) -> Option<f32> {
256        let denom = plane.normal.dot(self.direction);
257        if denom.abs() < 1e-8 {
258            return None;
259        }
260        let t = -(plane.normal.dot(self.origin) + plane.d) / denom;
261        if t >= 0.0 {
262            Some(t)
263        } else {
264            None
265        }
266    }
267}
268
269// ── Plane ─────────────────────────────────────────────────────────────────────
270
271#[derive(Debug, Clone, Copy, PartialEq)]
272pub struct Plane {
273    pub normal: Vec3,
274    pub d: f32,
275}
276
277impl Plane {
278    pub fn new(normal: Vec3, d: f32) -> Self {
279        Self { normal: normal.normalize(), d }
280    }
281
282    pub fn from_point_normal(point: Vec3, normal: Vec3) -> Self {
283        let n = normal.normalize();
284        Self { normal: n, d: -n.dot(point) }
285    }
286
287    pub fn from_three_points(a: Vec3, b: Vec3, c: Vec3) -> Self {
288        let n = (b - a).cross(c - a).normalize();
289        Self::from_point_normal(a, n)
290    }
291
292    pub fn signed_distance(&self, p: Vec3) -> f32 {
293        self.normal.dot(p) + self.d
294    }
295}
296
297// ── View frustum ──────────────────────────────────────────────────────────────
298
299pub struct Frustum {
300    planes: [Plane; 6],
301}
302
303impl Frustum {
304    /// Extract frustum planes from a combined view-projection matrix
305    /// using the Gribb/Hartmann method.
306    pub fn from_view_proj(vp: Mat4) -> Self {
307        let cols = vp.to_cols_array_2d(); // [col][row]
308        let get_row = |i: usize| Vec4::new(cols[0][i], cols[1][i], cols[2][i], cols[3][i]);
309        let r0 = get_row(0);
310        let r1 = get_row(1);
311        let r2 = get_row(2);
312        let r3 = get_row(3);
313
314        let make = |v: Vec4| Plane::new(Vec3::new(v.x, v.y, v.z), v.w);
315
316        Self {
317            planes: [
318                make(r3 + r0), // left
319                make(r3 - r0), // right
320                make(r3 + r1), // bottom
321                make(r3 - r1), // top
322                make(r3 + r2), // near
323                make(r3 - r2), // far
324            ],
325        }
326    }
327
328    pub fn contains_point(&self, p: Vec3) -> bool {
329        self.planes
330            .iter()
331            .all(|plane| plane.signed_distance(p) >= 0.0)
332    }
333
334    pub fn contains_aabb(&self, aabb: &Aabb) -> bool {
335        for plane in &self.planes {
336            let positive = Vec3::new(
337                if plane.normal.x > 0.0 {
338                    aabb.max.x
339                } else {
340                    aabb.min.x
341                },
342                if plane.normal.y > 0.0 {
343                    aabb.max.y
344                } else {
345                    aabb.min.y
346                },
347                if plane.normal.z > 0.0 {
348                    aabb.max.z
349                } else {
350                    aabb.min.z
351                },
352            );
353            if plane.signed_distance(positive) < 0.0 {
354                return false;
355            }
356        }
357        true
358    }
359
360    pub fn contains_sphere(&self, center: Vec3, radius: f32) -> bool {
361        self.planes
362            .iter()
363            .all(|p| p.signed_distance(center) >= -radius)
364    }
365}