Skip to main content

cumulus3d_core/
geometry.rs

1/*
2 * geometry.rs
3 *
4 * Copyright (c) 2026 ParkSangWoo
5 *
6 * Author(s):
7 *
8 *      ParkSangWoo <dev.parksangwoo@gmail.com>
9 *
10 *
11 * This file is part of cumulus3d.
12 *
13 * Licensed under the Apache License, Version 2.0 (the "License");
14 * you may not use this file except in compliance with the License.
15 * You may obtain a copy of the License at
16 *
17 *      http://www.apache.org/licenses/LICENSE-2.0
18 *
19 * Unless required by applicable law or agreed to in writing, software
20 * distributed under the License is distributed on an "AS IS" BASIS,
21 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 * See the License for the specific language governing permissions and
23 * limitations under the License.
24 *
25 * SPDX-License-Identifier: Apache-2.0
26 */
27
28//! Geometry: quaternions, rigid transforms (Rigid3), similarity transforms (Sim3), angle utilities.
29
30use nalgebra::{Matrix3, Matrix3x4, UnitQuaternion, Vector2, Vector3};
31use std::ops::Mul;
32
33/// 2D f64 vector.
34pub type Vec2 = Vector2<f64>;
35/// 3D f64 vector.
36pub type Vec3 = Vector3<f64>;
37/// 3×3 f64 matrix.
38pub type Mat3 = Matrix3<f64>;
39/// 3×4 f64 matrix.
40pub type Mat3x4 = Matrix3x4<f64>;
41
42/// Hamilton quaternion (i²=j²=k²=ijk=−1, active rotation v' = q v q*). Field order w, x, y, z.
43#[derive(Clone, Copy, Debug, PartialEq)]
44pub struct Quat {
45    /// Real part.
46    pub w: f64,
47    /// i component.
48    pub x: f64,
49    /// j component.
50    pub y: f64,
51    /// k component.
52    pub z: f64,
53}
54
55impl Default for Quat {
56    fn default() -> Self {
57        Self::IDENTITY
58    }
59}
60
61impl Quat {
62    /// Identity rotation.
63    pub const IDENTITY: Quat = Quat { w: 1.0, x: 0.0, y: 0.0, z: 0.0 };
64
65    /// Creates from (w, x, y, z) (not normalized).
66    pub fn new(w: f64, x: f64, y: f64, z: f64) -> Self {
67        Self { w, x, y, z }
68    }
69    /// From file order [w, x, y, z].
70    pub fn from_wxyz(v: [f64; 4]) -> Self {
71        Self { w: v[0], x: v[1], y: v[2], z: v[3] }
72    }
73    /// `[w, x, y, z]` array.
74    pub fn to_wxyz(&self) -> [f64; 4] {
75        [self.w, self.x, self.y, self.z]
76    }
77    /// Norm.
78    pub fn norm(&self) -> f64 {
79        (self.w * self.w + self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
80    }
81    /// Normalizes. Identity if the norm is 0.
82    pub fn normalized(&self) -> Self {
83        let n = self.norm();
84        if n == 0.0 || !n.is_finite() {
85            return Self::IDENTITY;
86        }
87        Self { w: self.w / n, x: self.x / n, y: self.y / n, z: self.z / n }
88    }
89    /// Conjugate.
90    pub fn conjugate(&self) -> Self {
91        Self { w: self.w, x: -self.x, y: -self.y, z: -self.z }
92    }
93    /// Inverse of a unit quaternion = conjugate.
94    pub fn inverse(&self) -> Self {
95        self.conjugate()
96    }
97    /// 4D dot product.
98    pub fn dot(&self, o: &Quat) -> f64 {
99        self.w * o.w + self.x * o.x + self.y * o.y + self.z * o.z
100    }
101    /// Hamilton product self ⊗ o (rotates by o first, then self).
102    pub fn hamilton(&self, o: &Quat) -> Quat {
103        Quat {
104            w: self.w * o.w - self.x * o.x - self.y * o.y - self.z * o.z,
105            x: self.w * o.x + self.x * o.w + self.y * o.z - self.z * o.y,
106            y: self.w * o.y - self.x * o.z + self.y * o.w + self.z * o.x,
107            z: self.w * o.z + self.x * o.y - self.y * o.x + self.z * o.w,
108        }
109    }
110    /// Rotation matrix. Computed as a normalized rotation even if not unit.
111    pub fn to_rotation_matrix(&self) -> Mat3 {
112        let n2 = self.w * self.w + self.x * self.x + self.y * self.y + self.z * self.z;
113        let s = if n2 > 0.0 { 2.0 / n2 } else { 0.0 };
114        let (w, x, y, z) = (self.w, self.x, self.y, self.z);
115        let (xx, yy, zz) = (x * x * s, y * y * s, z * z * s);
116        let (xy, xz, yz) = (x * y * s, x * z * s, y * z * s);
117        let (wx, wy, wz) = (w * x * s, w * y * s, w * z * s);
118        Mat3::new(1.0 - yy - zz, xy - wz, xz + wy, xy + wz, 1.0 - xx - zz, yz - wx, xz - wy, yz + wx, 1.0 - xx - yy)
119    }
120    /// Rotation matrix -> unit quaternion (Shepperd's method).
121    pub fn from_rotation_matrix(m: &Mat3) -> Quat {
122        let tr = m[(0, 0)] + m[(1, 1)] + m[(2, 2)];
123        let q = if tr > 0.0 {
124            let s = (tr + 1.0).sqrt() * 2.0;
125            Quat { w: 0.25 * s, x: (m[(2, 1)] - m[(1, 2)]) / s, y: (m[(0, 2)] - m[(2, 0)]) / s, z: (m[(1, 0)] - m[(0, 1)]) / s }
126        } else if m[(0, 0)] > m[(1, 1)] && m[(0, 0)] > m[(2, 2)] {
127            let s = (1.0 + m[(0, 0)] - m[(1, 1)] - m[(2, 2)]).sqrt() * 2.0;
128            Quat { w: (m[(2, 1)] - m[(1, 2)]) / s, x: 0.25 * s, y: (m[(0, 1)] + m[(1, 0)]) / s, z: (m[(0, 2)] + m[(2, 0)]) / s }
129        } else if m[(1, 1)] > m[(2, 2)] {
130            let s = (1.0 + m[(1, 1)] - m[(0, 0)] - m[(2, 2)]).sqrt() * 2.0;
131            Quat { w: (m[(0, 2)] - m[(2, 0)]) / s, x: (m[(0, 1)] + m[(1, 0)]) / s, y: 0.25 * s, z: (m[(1, 2)] + m[(2, 1)]) / s }
132        } else {
133            let s = (1.0 + m[(2, 2)] - m[(0, 0)] - m[(1, 1)]).sqrt() * 2.0;
134            Quat { w: (m[(1, 0)] - m[(0, 1)]) / s, x: (m[(0, 2)] + m[(2, 0)]) / s, y: (m[(1, 2)] + m[(2, 1)]) / s, z: 0.25 * s }
135        };
136        q.normalized()
137    }
138    /// Axis-angle (radians). The axis is normalized.
139    pub fn from_axis_angle(axis: &Vec3, angle: f64) -> Quat {
140        let n = axis.norm();
141        if n == 0.0 {
142            return Self::IDENTITY;
143        }
144        let a = axis / n;
145        let (s, c) = (0.5 * angle).sin_cos();
146        Quat { w: c, x: a.x * s, y: a.y * s, z: a.z * s }
147    }
148    /// Rotation vector (exponential map) -> quaternion.
149    pub fn from_rotation_vector(rv: &Vec3) -> Quat {
150        let theta = rv.norm();
151        if theta < 1e-12 {
152            // First-order approximation (keeps accuracy at small angles).
153            return Quat { w: 1.0, x: 0.5 * rv.x, y: 0.5 * rv.y, z: 0.5 * rv.z }.normalized();
154        }
155        Self::from_axis_angle(rv, theta)
156    }
157    /// Quaternion -> rotation vector (logarithm map), angle ∈ [0, π].
158    pub fn to_rotation_vector(&self) -> Vec3 {
159        let q = self.normalized();
160        let q = if q.w < 0.0 { Quat { w: -q.w, x: -q.x, y: -q.y, z: -q.z } } else { q };
161        let v = Vec3::new(q.x, q.y, q.z);
162        let s = v.norm();
163        if s < 1e-12 {
164            return 2.0 * v;
165        }
166        let angle = 2.0 * s.atan2(q.w);
167        v * (angle / s)
168    }
169    /// Rotates a vector.
170    pub fn rotate(&self, v: &Vec3) -> Vec3 {
171        self.to_rotation_matrix() * v
172    }
173    /// Angular distance between two rotations (radians) = 2·acos|⟨q1,q2⟩|.
174    pub fn angular_distance(&self, other: &Quat) -> f64 {
175        let d = self.normalized().dot(&other.normalized()).abs().min(1.0);
176        2.0 * d.acos()
177    }
178    /// Converts to a nalgebra unit quaternion.
179    pub fn to_nalgebra(&self) -> UnitQuaternion<f64> {
180        UnitQuaternion::from_quaternion(nalgebra::Quaternion::new(self.w, self.x, self.y, self.z))
181    }
182    /// Converts from a nalgebra unit quaternion.
183    pub fn from_nalgebra(q: &UnitQuaternion<f64>) -> Quat {
184        Quat { w: q.w, x: q.i, y: q.j, z: q.k }
185    }
186}
187
188impl Mul for Quat {
189    type Output = Quat;
190    fn mul(self, rhs: Quat) -> Quat {
191        self.hamilton(&rhs)
192    }
193}
194
195/// Skew-symmetric (cross-product) matrix `[v]×`.
196pub fn skew(v: &Vec3) -> Mat3 {
197    Mat3::new(0.0, -v.z, v.y, v.z, 0.0, -v.x, -v.y, v.x, 0.0)
198}
199
200/// Rigid transform A_to_B: X_B = R·X_A + t.
201#[derive(Clone, Copy, Debug, PartialEq, Default)]
202pub struct Rigid3 {
203    /// Rotation R.
204    pub rotation: Quat,
205    /// Translation t.
206    pub translation: Vec3,
207}
208
209impl Rigid3 {
210    /// Identity transform.
211    pub fn identity() -> Self {
212        Self { rotation: Quat::IDENTITY, translation: Vec3::zeros() }
213    }
214    /// Creates from (R, t).
215    pub fn new(rotation: Quat, translation: Vec3) -> Self {
216        Self { rotation, translation }
217    }
218    /// Creates from a rotation matrix and a translation.
219    pub fn from_rotation_matrix(r: &Mat3, t: Vec3) -> Self {
220        Self { rotation: Quat::from_rotation_matrix(r), translation: t }
221    }
222    /// File order [qw, qx, qy, qz, tx, ty, tz].
223    pub fn from_params(p: &[f64; 7]) -> Self {
224        Self { rotation: Quat::new(p[0], p[1], p[2], p[3]), translation: Vec3::new(p[4], p[5], p[6]) }
225    }
226    /// File order `[qw, qx, qy, qz, tx, ty, tz]`.
227    pub fn to_params(&self) -> [f64; 7] {
228        let q = self.rotation;
229        let t = self.translation;
230        [q.w, q.x, q.y, q.z, t.x, t.y, t.z]
231    }
232    /// Rotation matrix R.
233    pub fn rotation_matrix(&self) -> Mat3 {
234        self.rotation.to_rotation_matrix()
235    }
236    /// [R | t].
237    pub fn matrix(&self) -> Mat3x4 {
238        let r = self.rotation_matrix();
239        let mut m = Mat3x4::zeros();
240        m.fixed_view_mut::<3, 3>(0, 0).copy_from(&r);
241        m.set_column(3, &self.translation);
242        m
243    }
244    /// Creates from a `[R | t]` matrix.
245    pub fn from_matrix(m: &Mat3x4) -> Self {
246        let r: Mat3 = m.fixed_view::<3, 3>(0, 0).into_owned();
247        Self::from_rotation_matrix(&r, m.column(3).into_owned())
248    }
249    /// X_B = R X_A + t.
250    pub fn transform_point(&self, p: &Vec3) -> Vec3 {
251        self.rotation_matrix() * p + self.translation
252    }
253    /// Inverse transform B_to_A: R' = Rᵀ, t' = −Rᵀ t.
254    pub fn inverse(&self) -> Self {
255        let rinv = self.rotation.conjugate();
256        let rt = rinv.to_rotation_matrix();
257        Self { rotation: rinv, translation: -(rt * self.translation) }
258    }
259    /// Composition A_to_C = self(B_to_C) ∘ rhs(A_to_B). The resulting rotation is normalized.
260    pub fn compose(&self, rhs: &Rigid3) -> Rigid3 {
261        Rigid3 {
262            rotation: self.rotation.hamilton(&rhs.rotation).normalized(),
263            translation: self.translation + self.rotation_matrix() * rhs.translation,
264        }
265    }
266    /// For world_to_cam, the projection center C = −Rᵀ t.
267    pub fn center(&self) -> Vec3 {
268        -(self.rotation_matrix().transpose() * self.translation)
269    }
270    /// For world_to_cam, the viewing direction (camera +z in world coordinates) = third row of R.
271    pub fn viewing_direction(&self) -> Vec3 {
272        let r = self.rotation_matrix();
273        Vec3::new(r[(2, 0)], r[(2, 1)], r[(2, 2)])
274    }
275}
276
277impl Mul for Rigid3 {
278    type Output = Rigid3;
279    fn mul(self, rhs: Rigid3) -> Rigid3 {
280        self.compose(&rhs)
281    }
282}
283
284impl Mul<Vec3> for Rigid3 {
285    type Output = Vec3;
286    fn mul(self, rhs: Vec3) -> Vec3 {
287        self.transform_point(&rhs)
288    }
289}
290
291/// Similarity transform new_from_old: X_new = s·R·X_old + t.
292#[derive(Clone, Copy, Debug, PartialEq)]
293pub struct Sim3 {
294    /// Scale s.
295    pub scale: f64,
296    /// Rotation R.
297    pub rotation: Quat,
298    /// Translation t.
299    pub translation: Vec3,
300}
301
302impl Default for Sim3 {
303    fn default() -> Self {
304        Self::identity()
305    }
306}
307
308impl Sim3 {
309    /// Identity transform.
310    pub fn identity() -> Self {
311        Self { scale: 1.0, rotation: Quat::IDENTITY, translation: Vec3::zeros() }
312    }
313    /// Creates from (s, R, t).
314    pub fn new(scale: f64, rotation: Quat, translation: Vec3) -> Self {
315        Self { scale, rotation, translation }
316    }
317    /// [sR | t].
318    pub fn matrix(&self) -> Mat3x4 {
319        let r = self.rotation.to_rotation_matrix() * self.scale;
320        let mut m = Mat3x4::zeros();
321        m.fixed_view_mut::<3, 3>(0, 0).copy_from(&r);
322        m.set_column(3, &self.translation);
323        m
324    }
325    /// [sR | t] -> Sim3. s = norm of the first column, R = upper-left 3×3 / s.
326    pub fn from_matrix(m: &Mat3x4) -> Self {
327        let sr: Mat3 = m.fixed_view::<3, 3>(0, 0).into_owned();
328        let s = sr.column(0).norm();
329        Self { scale: s, rotation: Quat::from_rotation_matrix(&(sr / s)), translation: m.column(3).into_owned() }
330    }
331    /// X_new = s·R·X_old + t.
332    pub fn transform_point(&self, p: &Vec3) -> Vec3 {
333        self.scale * (self.rotation.to_rotation_matrix() * p) + self.translation
334    }
335    /// Inverse transform: s' = 1/s, R' = Rᵀ, t' = −Rᵀ t / s.
336    pub fn inverse(&self) -> Self {
337        let rinv = self.rotation.conjugate();
338        Self { scale: 1.0 / self.scale, rotation: rinv, translation: -(rinv.to_rotation_matrix() * self.translation) / self.scale }
339    }
340    /// Composition self ∘ rhs.
341    pub fn compose(&self, rhs: &Sim3) -> Sim3 {
342        Sim3 {
343            scale: self.scale * rhs.scale,
344            rotation: self.rotation.hamilton(&rhs.rotation).normalized(),
345            translation: self.scale * (self.rotation.to_rotation_matrix() * rhs.translation) + self.translation,
346        }
347    }
348    /// Applies to a pose (world_to_cam or world_to_rig): R' = R Qᵀ, t' = s t − R' T.
349    pub fn transform_pose(&self, world_to_cam: &Rigid3) -> Rigid3 {
350        let rot = world_to_cam.rotation.hamilton(&self.rotation.conjugate()).normalized();
351        let t = self.scale * world_to_cam.translation - rot.to_rotation_matrix() * self.translation;
352        Rigid3 { rotation: rot, translation: t }
353    }
354}
355
356impl Mul for Sim3 {
357    type Output = Sim3;
358    fn mul(self, rhs: Sim3) -> Sim3 {
359        self.compose(&rhs)
360    }
361}
362
363/// Degrees -> radians.
364pub fn deg_to_rad(d: f64) -> f64 {
365    d.to_radians()
366}
367/// Radians -> degrees.
368pub fn rad_to_deg(r: f64) -> f64 {
369    r.to_degrees()
370}
371
372/// Angle between two vectors (radians), acos(clamp(dot/norm product)). 0 if a norm is 0.
373pub fn angle_between(a: &Vec3, b: &Vec3) -> f64 {
374    let n = a.norm() * b.norm();
375    if n == 0.0 {
376        return 0.0;
377    }
378    (a.dot(b) / n).clamp(-1.0, 1.0).acos()
379}
380
381/// Triangulation angle: min(θ, π−θ) of the angle θ between (X−C1) and (X−C2).
382pub fn triangulation_angle(center1: &Vec3, center2: &Vec3, point: &Vec3) -> f64 {
383    let theta = angle_between(&(point - center1), &(point - center2));
384    theta.min(std::f64::consts::PI - theta)
385}
386
387/// Baseline length ‖C1 − C2‖.
388pub fn baseline(center1: &Vec3, center2: &Vec3) -> f64 {
389    (center1 - center2).norm()
390}
391
392/// Maximum triangulation angle (radians) over several projection-center pairs. 0 if fewer than 2 centers.
393pub fn max_triangulation_angle(centers: &[Vec3], point: &Vec3) -> f64 {
394    let mut best = 0.0f64;
395    for i in 0..centers.len() {
396        for j in 0..i {
397            best = best.max(triangulation_angle(&centers[i], &centers[j], point));
398        }
399    }
400    best
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    fn rand_quat(seed: u64) -> Quat {
408        let f = |k: u64| (((seed * 7919 + k * 104729) % 1000) as f64 / 500.0) - 1.0;
409        Quat::new(f(1), f(2), f(3), f(4) + 0.1).normalized()
410    }
411
412    #[test]
413    fn quat_matrix_roundtrip() {
414        for s in 0..50 {
415            let q = rand_quat(s);
416            let m = q.to_rotation_matrix();
417            assert!((m * m.transpose() - Mat3::identity()).norm() < 1e-12);
418            assert!((m.determinant() - 1.0).abs() < 1e-12);
419            let q2 = Quat::from_rotation_matrix(&m);
420            assert!(q.angular_distance(&q2) < 1e-7);
421            assert!((q2.to_rotation_matrix() - m).norm() < 1e-12);
422        }
423    }
424
425    #[test]
426    fn hamilton_convention() {
427        // i*j = k
428        let i = Quat::new(0.0, 1.0, 0.0, 0.0);
429        let j = Quat::new(0.0, 0.0, 1.0, 0.0);
430        assert_eq!(i * j, Quat::new(0.0, 0.0, 0.0, 1.0));
431        // 90° about z maps x to y (active rotation)
432        let q = Quat::from_axis_angle(&Vec3::z(), std::f64::consts::FRAC_PI_2);
433        assert!((q.rotate(&Vec3::x()) - Vec3::y()).norm() < 1e-15);
434        // matrix product matches quaternion product
435        let a = rand_quat(3);
436        let b = rand_quat(4);
437        let m = (a * b).to_rotation_matrix();
438        assert!((m - a.to_rotation_matrix() * b.to_rotation_matrix()).norm() < 1e-12);
439        // nalgebra agrees
440        let na = a.to_nalgebra();
441        assert!((na.to_rotation_matrix().matrix() - a.to_rotation_matrix()).norm() < 1e-12);
442    }
443
444    #[test]
445    fn rotation_vector_roundtrip() {
446        for s in 0..20 {
447            let q = rand_quat(s);
448            let rv = q.to_rotation_vector();
449            let q2 = Quat::from_rotation_vector(&rv);
450            assert!(q.angular_distance(&q2) < 1e-7);
451        }
452    }
453
454    #[test]
455    fn rigid3_identities() {
456        let a = Rigid3::new(rand_quat(1), Vec3::new(1.0, -2.0, 3.0));
457        let b = Rigid3::new(rand_quat(2), Vec3::new(-0.5, 0.2, 4.0));
458        let p = Vec3::new(0.3, 0.7, -1.1);
459        let ab = a * b;
460        assert!(((ab * p) - (a * (b * p))).norm() < 1e-12);
461        let ia = a.inverse();
462        assert!(((ia * (a * p)) - p).norm() < 1e-12);
463        let id = a * ia;
464        assert!(id.translation.norm() < 1e-12);
465        assert!(id.rotation.angular_distance(&Quat::IDENTITY) < 1e-7);
466        // center maps to camera origin
467        assert!((a * a.center()).norm() < 1e-12);
468        let vd = a.viewing_direction();
469        assert!((a.rotation_matrix() * vd - Vec3::z()).norm() < 1e-12);
470        let m = Rigid3::from_matrix(&a.matrix());
471        assert!(((m * p) - (a * p)).norm() < 1e-12);
472    }
473
474    #[test]
475    fn sim3_identities() {
476        let s = Sim3::new(2.5, rand_quat(5), Vec3::new(1.0, 2.0, 3.0));
477        let t = Sim3::new(0.3, rand_quat(6), Vec3::new(-1.0, 0.0, 0.5));
478        let p = Vec3::new(0.1, -0.2, 0.3);
479        assert!(((s * t).transform_point(&p) - s.transform_point(&t.transform_point(&p))).norm() < 1e-12);
480        assert!((s.inverse().transform_point(&s.transform_point(&p)) - p).norm() < 1e-12);
481        let m = Sim3::from_matrix(&s.matrix());
482        assert!((m.scale - s.scale).abs() < 1e-12);
483        assert!((m.transform_point(&p) - s.transform_point(&p)).norm() < 1e-12);
484        // pose transform: center moves like a point, projections unchanged up to scale
485        let pose = Rigid3::new(rand_quat(7), Vec3::new(0.2, 0.1, 5.0));
486        let np = s.transform_pose(&pose);
487        assert!((np.center() - s.transform_point(&pose.center())).norm() < 1e-10);
488        let x = Vec3::new(0.5, 0.5, 2.0);
489        let c_old = pose * x;
490        let c_new = np * s.transform_point(&x);
491        assert!((c_new - s.scale * c_old).norm() < 1e-10);
492    }
493
494    #[test]
495    fn tri_angle_symmetric() {
496        let c1 = Vec3::new(-1.0, 0.0, 0.0);
497        let c2 = Vec3::new(1.0, 0.0, 0.0);
498        let x = Vec3::new(0.0, 0.0, 1.0);
499        assert!((triangulation_angle(&c1, &c2, &x) - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
500        let x = Vec3::new(0.0, 0.0, 3.0_f64.sqrt());
501        assert!((triangulation_angle(&c1, &c2, &x) - std::f64::consts::FRAC_PI_3).abs() < 1e-12);
502        // obtuse folded: angle 120° → 60°
503        let x = Vec3::new(0.0, 0.0, 1.0 / 3.0_f64.sqrt());
504        assert!((triangulation_angle(&c1, &c2, &x) - std::f64::consts::FRAC_PI_3).abs() < 1e-12);
505        assert!(triangulation_angle(&c1, &c1, &x) < 1e-7);
506        assert_eq!(triangulation_angle(&c1, &c2, &c1), 0.0);
507    }
508}