Skip to main content

khora_core/math/
affine_transform.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Affine transformations for 2D and 3D space.
16
17use bincode::{Decode, Encode};
18use serde::{Deserialize, Serialize};
19
20use crate::math::{Mat4, Quaternion, Vec3, Vec4};
21
22/// Represents a 3D affine transformation (translation, rotation, scale).
23///
24/// This is a semantic wrapper around a `Mat4` that guarantees the matrix
25/// represents a valid affine transform. It provides a dedicated API for
26/// creating and manipulating these transformations.
27#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Encode, Decode)]
28#[repr(transparent)]
29pub struct AffineTransform(pub Mat4);
30
31impl AffineTransform {
32    /// The identity transform, which results in no change.
33    pub const IDENTITY: Self = Self(Mat4::IDENTITY);
34
35    // --- CONSTRUCTORS ---
36    /// Creates an `AffineTransform` from a translation vector.
37    ///
38    /// # Arguments
39    ///
40    /// * `v` - The translation vector to apply
41    ///
42    /// # Example
43    ///
44    /// ```rust
45    /// use khora_core::math::Vec3;
46    /// use khora_core::math::affine_transform::AffineTransform;
47    ///
48    /// let transform = AffineTransform::from_translation(Vec3::new(1.0, 2.0, 3.0));
49    /// assert_eq!(transform.translation(), Vec3::new(1.0, 2.0, 3.0));
50    /// ```
51    #[inline]
52    pub fn from_translation(v: Vec3) -> Self {
53        Self(Mat4::from_cols(
54            Vec4::new(1.0, 0.0, 0.0, 0.0),
55            Vec4::new(0.0, 1.0, 0.0, 0.0),
56            Vec4::new(0.0, 0.0, 1.0, 0.0),
57            Vec4::new(v.x, v.y, v.z, 1.0),
58        ))
59    }
60
61    /// Creates an `AffineTransform` from a non-uniform scale vector.
62    ///
63    /// # Arguments
64    ///
65    /// * `scale` - The scale vector to apply to each axis
66    ///
67    /// # Example
68    ///
69    /// ```rust
70    /// use khora_core::math::Vec3;
71    /// use khora_core::math::affine_transform::AffineTransform;
72    ///
73    /// let transform = AffineTransform::from_scale(Vec3::new(2.0, 1.5, 0.5));
74    /// ```
75    #[inline]
76    pub fn from_scale(scale: Vec3) -> Self {
77        Self(Mat4::from_cols(
78            Vec4::new(scale.x, 0.0, 0.0, 0.0),
79            Vec4::new(0.0, scale.y, 0.0, 0.0),
80            Vec4::new(0.0, 0.0, scale.z, 0.0),
81            Vec4::new(0.0, 0.0, 0.0, 1.0),
82        ))
83    }
84
85    /// Creates an `AffineTransform` from a rotation around the X axis.
86    ///
87    /// # Arguments
88    ///
89    /// * `angle` - The angle of rotation in radians
90    ///
91    /// # Example
92    ///
93    /// ```rust
94    /// use khora_core::math::Vec3;
95    /// use khora_core::math::affine_transform::AffineTransform;
96    /// use std::f32::consts::PI;
97    ///
98    /// let transform = AffineTransform::from_rotation_x(PI / 2.0);
99    /// ```
100    #[inline]
101    pub fn from_rotation_x(angle: f32) -> Self {
102        let c = angle.cos();
103        let s = angle.sin();
104        Self(Mat4::from_cols(
105            Vec4::new(1.0, 0.0, 0.0, 0.0),
106            Vec4::new(0.0, c, s, 0.0),
107            Vec4::new(0.0, -s, c, 0.0),
108            Vec4::new(0.0, 0.0, 0.0, 1.0),
109        ))
110    }
111
112    /// Creates an `AffineTransform` from a rotation around the Y axis.
113    ///
114    /// # Arguments
115    ///
116    /// * `angle` - The angle of rotation in radians
117    ///
118    /// # Example
119    ///
120    /// ```rust
121    /// use khora_core::math::Vec3;
122    /// use khora_core::math::affine_transform::AffineTransform;
123    /// use std::f32::consts::PI;
124    ///
125    /// let transform = AffineTransform::from_rotation_y(PI / 2.0);
126    /// ```
127    #[inline]
128    pub fn from_rotation_y(angle: f32) -> Self {
129        let c = angle.cos();
130        let s = angle.sin();
131        Self(Mat4::from_cols(
132            Vec4::new(c, 0.0, -s, 0.0),
133            Vec4::new(0.0, 1.0, 0.0, 0.0),
134            Vec4::new(s, 0.0, c, 0.0),
135            Vec4::new(0.0, 0.0, 0.0, 1.0),
136        ))
137    }
138
139    /// Creates an `AffineTransform` from a rotation around the Z axis.
140    ///
141    /// # Arguments
142    ///
143    /// * `angle` - The angle of rotation in radians
144    ///
145    /// # Example
146    ///
147    /// ```rust
148    /// use std::f32::consts::PI;
149    /// use khora_core::math::Vec3;
150    /// use khora_core::math::affine_transform::AffineTransform;
151    ///
152    /// let transform = AffineTransform::from_rotation_z(PI / 2.0);
153    /// ```
154    #[inline]
155    pub fn from_rotation_z(angle: f32) -> Self {
156        let c = angle.cos();
157        let s = angle.sin();
158        Self(Mat4::from_cols(
159            Vec4::new(c, s, 0.0, 0.0),
160            Vec4::new(-s, c, 0.0, 0.0),
161            Vec4::new(0.0, 0.0, 1.0, 0.0),
162            Vec4::new(0.0, 0.0, 0.0, 1.0),
163        ))
164    }
165
166    /// Creates an `AffineTransform` from a rotation around an arbitrary axis.
167    ///
168    /// Uses Rodrigues' rotation formula to create a rotation matrix.
169    ///
170    /// # Arguments
171    ///
172    /// * `axis` - The axis of rotation (will be normalized automatically)
173    /// * `angle` - The angle of rotation in radians
174    ///
175    /// # Example
176    ///
177    /// ```rust
178    /// use std::f32::consts::PI;
179    /// use khora_core::math::Vec3;
180    /// use khora_core::math::affine_transform::AffineTransform;
181    ///
182    /// let axis = Vec3::new(1.0, 1.0, 0.0);
183    /// let transform = AffineTransform::from_axis_angle(axis, PI / 4.0);
184    /// ```
185    #[inline]
186    pub fn from_axis_angle(axis: Vec3, angle: f32) -> Self {
187        let c = angle.cos();
188        let s = angle.sin();
189        let t = 1.0 - c;
190        let x = axis.x;
191        let y = axis.y;
192        let z = axis.z;
193
194        Self(Mat4::from_cols(
195            Vec4::new(t * x * x + c, t * x * y - s * z, t * x * z + s * y, 0.0),
196            Vec4::new(t * y * x + s * z, t * y * y + c, t * y * z - s * x, 0.0),
197            Vec4::new(t * z * x - s * y, t * z * y + s * x, t * z * z + c, 0.0),
198            Vec4::new(0.0, 0.0, 0.0, 1.0),
199        ))
200    }
201
202    /// Creates an `AffineTransform` from a quaternion representing a rotation.
203    ///
204    /// # Arguments
205    ///
206    /// * `q` - The quaternion representing the rotation
207    ///
208    /// # Example
209    ///
210    /// ```rust
211    /// use khora_core::math::{Quaternion, Vec3};
212    /// use khora_core::math::affine_transform::AffineTransform;
213    /// use std::f32::consts::PI;
214    ///
215    /// let q = Quaternion::from_axis_angle(Vec3::Y, PI / 2.0);
216    /// let transform = AffineTransform::from_quat(q);
217    /// ```
218    #[inline]
219    pub fn from_quat(q: Quaternion) -> Self {
220        Self(Mat4::from_quat(q))
221    }
222
223    // --- SEMANTIC ACCESSORS---
224    /// Converts the `AffineTransform` to a `Mat4`.
225    ///
226    /// This is useful when you need to pass the transformation matrix to shaders
227    /// or other systems that expect a raw matrix.
228    ///
229    /// # Example
230    ///
231    /// ```rust
232    /// use khora_core::math::Vec3;
233    /// use khora_core::math::affine_transform::AffineTransform;
234    ///
235    /// let transform = AffineTransform::IDENTITY;
236    /// let matrix = transform.to_matrix();
237    /// ```
238    #[inline]
239    pub fn to_matrix(&self) -> Mat4 {
240        self.0
241    }
242
243    /// Extracts the translation component from the affine transform.
244    ///
245    /// Returns the translation vector representing the position offset
246    /// applied by this transformation.
247    ///
248    /// # Example
249    ///
250    /// ```rust
251    /// use khora_core::math::Vec3;
252    /// use khora_core::math::affine_transform::AffineTransform;
253    ///
254    /// let transform = AffineTransform::from_translation(Vec3::new(1.0, 2.0, 3.0));
255    /// assert_eq!(transform.translation(), Vec3::new(1.0, 2.0, 3.0));
256    /// ```
257    #[inline]
258    pub fn translation(&self) -> Vec3 {
259        self.0.cols[3].truncate()
260    }
261
262    /// Extracts the right direction vector from the affine transform.
263    ///
264    /// This returns the first column of the transformation matrix (excluding the w component),
265    /// which represents the transformed positive X-axis direction.
266    ///
267    /// # Example
268    ///
269    /// ```rust
270    /// use khora_core::math::Vec3;
271    /// use khora_core::math::affine_transform::AffineTransform;
272    /// use std::f32::consts::PI;
273    ///
274    /// let transform = AffineTransform::from_rotation_z(PI / 2.0);
275    /// let right = transform.right();
276    /// // After 90° rotation around Z, right vector points in -Y direction
277    /// ```
278    #[inline]
279    pub fn right(&self) -> Vec3 {
280        self.0.cols[0].truncate()
281    }
282
283    /// Extracts the up direction vector from the affine transform.
284    ///
285    /// This returns the second column of the transformation matrix (excluding the w component),
286    /// which represents the transformed positive Y-axis direction.
287    ///
288    /// # Example
289    ///
290    /// ```rust
291    /// use std::f32::consts::PI;
292    /// use khora_core::math::Vec3;
293    /// use khora_core::math::affine_transform::AffineTransform;
294    ///
295    /// let transform = AffineTransform::from_rotation_x(PI / 2.0);
296    /// let up = transform.up();
297    /// // After 90° rotation around X, up vector points in -Z direction
298    /// ```
299    #[inline]
300    pub fn up(&self) -> Vec3 {
301        self.0.cols[1].truncate()
302    }
303
304    /// Extracts the forward direction vector from the affine transform.
305    ///
306    /// This returns the third column of the transformation matrix (excluding the w component),
307    /// which represents the transformed positive Z-axis direction.
308    ///
309    /// # Example
310    ///
311    /// ```rust
312    /// use std::f32::consts::PI;
313    /// use khora_core::math::Vec3;
314    /// use khora_core::math::affine_transform::AffineTransform;
315    ///
316    /// let transform = AffineTransform::from_rotation_y(PI / 2.0);
317    /// let forward = transform.forward();
318    /// // After 90° rotation around Y, forward vector points in X direction
319    /// ```
320    #[inline]
321    pub fn forward(&self) -> Vec3 {
322        self.0.cols[2].truncate()
323    }
324
325    /// Extracts the rotation component as a quaternion.
326    ///
327    /// This method extracts the rotation represented by the upper-left 3x3
328    /// portion of the transformation matrix.
329    ///
330    /// # Note
331    ///
332    /// This assumes the transform has uniform or no scale. For transforms
333    /// with non-uniform scale, the result may not represent a pure rotation.
334    /// In such cases, consider normalizing the direction vectors first.
335    ///
336    /// # Example
337    ///
338    /// ```rust
339    /// use khora_core::math::{Quaternion, Vec3};
340    /// use khora_core::math::affine_transform::AffineTransform;
341    /// use std::f32::consts::PI;
342    ///
343    /// let q = Quaternion::from_axis_angle(Vec3::Y, PI / 2.0);
344    /// let transform = AffineTransform::from_quat(q);
345    /// let extracted = transform.rotation();
346    /// // extracted should be approximately equal to q
347    /// ```
348    #[inline]
349    pub fn rotation(&self) -> Quaternion {
350        Quaternion::from_rotation_matrix(&self.0)
351    }
352
353    /// Extracts the non-negative scale factors along each local axis.
354    ///
355    /// Returns the Euclidean lengths of the three basis columns (right, up,
356    /// forward). For a transform built as translation ∘ rotation ∘ scale this
357    /// recovers the original scale vector. Mirror/negative scales are reported
358    /// as their magnitudes (sign is folded into the rotation extraction).
359    #[inline]
360    pub fn scale(&self) -> Vec3 {
361        Vec3::new(
362            self.right().length(),
363            self.up().length(),
364            self.forward().length(),
365        )
366    }
367
368    /// Interpolates between two affine transforms for smooth rendering
369    /// between discrete simulation steps.
370    ///
371    /// Decomposes both transforms into translation / rotation / scale and
372    /// blends each channel independently: translation and scale by linear
373    /// interpolation, rotation by spherical interpolation (shortest-path
374    /// `slerp`). `alpha` is clamped to `[0, 1]`; `alpha = 0` returns `self`
375    /// (the *previous* transform) and `alpha = 1` returns `other` (the
376    /// *current* transform).
377    ///
378    /// This is a render-only blend: it never feeds back into the authoritative
379    /// simulation state.
380    ///
381    /// # Examples
382    ///
383    /// ```rust
384    /// use khora_core::math::Vec3;
385    /// use khora_core::math::affine_transform::AffineTransform;
386    ///
387    /// let prev = AffineTransform::from_translation(Vec3::ZERO);
388    /// let curr = AffineTransform::from_translation(Vec3::new(10.0, 0.0, 0.0));
389    ///
390    /// // The endpoints round-trip the inputs; the midpoint blends them.
391    /// assert_eq!(prev.interpolate(&curr, 0.0).translation(), Vec3::ZERO);
392    /// assert_eq!(prev.interpolate(&curr, 1.0).translation(), curr.translation());
393    ///
394    /// let mid = prev.interpolate(&curr, 0.5).translation();
395    /// assert!((mid - Vec3::new(5.0, 0.0, 0.0)).length() < 1e-5);
396    /// ```
397    #[inline]
398    pub fn interpolate(&self, other: &Self, alpha: f32) -> Self {
399        let alpha = alpha.clamp(0.0, 1.0);
400        let translation = Vec3::lerp(self.translation(), other.translation(), alpha);
401        let rotation = Quaternion::slerp(self.rotation(), other.rotation(), alpha);
402        let scale = Vec3::lerp(self.scale(), other.scale(), alpha);
403
404        let mat = Mat4::from_translation(translation)
405            * Mat4::from_quat(rotation)
406            * Mat4::from_scale(scale);
407        Self(mat)
408    }
409
410    /// Computes the inverse of the affine transformation.
411    ///
412    /// This uses an optimized affine inverse algorithm that's more efficient than
413    /// a general matrix inverse, taking advantage of the affine transform structure.
414    /// Returns `None` if the transformation is not invertible (e.g., zero scale).
415    ///
416    /// # Returns
417    ///
418    /// `Some(AffineTransform)` if the inverse exists, `None` otherwise.
419    ///
420    /// # Example
421    ///
422    /// ```rust
423    /// use khora_core::math::Vec3;
424    /// use khora_core::math::affine_transform::AffineTransform;
425    ///
426    /// let transform = AffineTransform::from_translation(Vec3::new(1.0, 2.0, 3.0));
427    /// let inverse = transform.inverse().unwrap();
428    /// // The inverse should translate by (-1, -2, -3)
429    /// ```
430    #[inline]
431    pub fn inverse(&self) -> Option<Self> {
432        self.0.affine_inverse().map(Self)
433    }
434}
435
436impl Default for AffineTransform {
437    /// Returns the identity `AffineTransform`.
438    fn default() -> Self {
439        Self::IDENTITY
440    }
441}
442
443// Allow easy conversion to the underlying Mat4 for sending to the GPU, etc.
444impl From<AffineTransform> for Mat4 {
445    /// Converts the `AffineTransform` into its inner `Mat4`.
446    #[inline]
447    fn from(transform: AffineTransform) -> Self {
448        transform.0
449    }
450}
451
452impl From<Mat4> for AffineTransform {
453    /// Converts a `Mat4` into an `AffineTransform`.
454    ///
455    /// # Panics
456    ///
457    /// Panics if the matrix is not a valid affine transformation.
458    #[inline]
459    fn from(val: Mat4) -> Self {
460        // Validate that the matrix is affine (last row must be [0, 0, 0, 1])
461        let last_row = val.get_row(3);
462        assert!(
463            last_row == Vec4::new(0.0, 0.0, 0.0, 1.0),
464            "Matrix is not a valid affine transformation"
465        );
466        AffineTransform(val)
467    }
468}
469
470#[cfg(test)]
471mod interpolation_tests {
472    use super::*;
473    use crate::math::Quaternion;
474    use std::f32::consts::PI;
475
476    fn vec3_close(a: Vec3, b: Vec3) -> bool {
477        (a - b).length() < 1e-4
478    }
479
480    #[test]
481    fn interpolate_endpoints_return_inputs() {
482        let prev = AffineTransform::from_translation(Vec3::new(0.0, 0.0, 0.0));
483        let curr = AffineTransform::from_translation(Vec3::new(10.0, 0.0, 0.0));
484
485        let at_zero = prev.interpolate(&curr, 0.0);
486        let at_one = prev.interpolate(&curr, 1.0);
487
488        assert!(vec3_close(at_zero.translation(), prev.translation()));
489        assert!(vec3_close(at_one.translation(), curr.translation()));
490    }
491
492    #[test]
493    fn interpolate_midpoint_blends_translation_and_scale() {
494        let prev = AffineTransform(
495            Mat4::from_translation(Vec3::new(0.0, 0.0, 0.0))
496                * Mat4::from_scale(Vec3::new(1.0, 1.0, 1.0)),
497        );
498        let curr = AffineTransform(
499            Mat4::from_translation(Vec3::new(4.0, 8.0, 2.0))
500                * Mat4::from_scale(Vec3::new(3.0, 3.0, 3.0)),
501        );
502
503        let mid = prev.interpolate(&curr, 0.5);
504
505        assert!(vec3_close(mid.translation(), Vec3::new(2.0, 4.0, 1.0)));
506        assert!(vec3_close(mid.scale(), Vec3::new(2.0, 2.0, 2.0)));
507    }
508
509    #[test]
510    fn interpolate_midpoint_blends_rotation() {
511        let y_axis = Vec3::new(0.0, 1.0, 0.0);
512        let prev = AffineTransform::from_quat(Quaternion::IDENTITY);
513        let curr = AffineTransform::from_quat(Quaternion::from_axis_angle(y_axis, PI / 2.0));
514
515        let mid = prev.interpolate(&curr, 0.5);
516        let expected = Quaternion::from_axis_angle(y_axis, PI / 4.0);
517
518        // Rotating a forward vector by the interpolated quaternion must match
519        // rotating it by the half-angle rotation (slerp is constant-speed).
520        let probe = Vec3::new(0.0, 0.0, 1.0);
521        let got = mid.rotation() * probe;
522        let want = expected * probe;
523        assert!(vec3_close(got, want), "got {got:?}, want {want:?}");
524    }
525
526    #[test]
527    fn interpolate_clamps_alpha() {
528        let prev = AffineTransform::from_translation(Vec3::ZERO);
529        let curr = AffineTransform::from_translation(Vec3::new(10.0, 0.0, 0.0));
530
531        let below = prev.interpolate(&curr, -1.0);
532        let above = prev.interpolate(&curr, 2.0);
533
534        assert!(vec3_close(below.translation(), prev.translation()));
535        assert!(vec3_close(above.translation(), curr.translation()));
536    }
537}