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}