Skip to main content

munum/
transform.rs

1//! Transformation matrix functions.
2
3use crate::{scalar, FloatOps, Mat3, Mat4, Quaternion, Vec3, Vec4};
4use num::{
5    traits::{float::FloatCore, NumAssign},
6    Signed,
7};
8
9// region: Affine transformations
10
11/// Creates a 4x4 transformation matrix that represents a translation of (x, y, z).
12///
13/// # Examples
14/// ```
15/// # use munum::{transform, vec3};
16/// assert_eq!(*transform::translation(vec3(2_i32, 3, 5)).as_ref(), [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 2, 3, 5, 1]);
17/// ```
18pub fn translation<T: Copy + NumAssign>(v: Vec3<T>) -> Mat4<T> {
19    let mut result = Mat4::identity();
20    result[(0, 3)] = v[0];
21    result[(1, 3)] = v[1];
22    result[(2, 3)] = v[2];
23    result
24}
25
26/// Creates a 4x4 transformation matrix that represents a scaling of (x, y, z).
27///
28/// # Examples
29/// ```
30/// # use munum::{transform, vec3};
31/// assert_eq!(*transform::scaling(vec3(2_i32, 3, 5)).as_ref(), [2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 1]);
32/// ```
33pub fn scaling<T: Copy + NumAssign>(v: Vec3<T>) -> Mat4<T> {
34    let mut result = Mat4::identity();
35    result[(0, 0)] = v[0];
36    result[(1, 1)] = v[1];
37    result[(2, 2)] = v[2];
38    result
39}
40
41/// Creates a 4x4 transformation matrix that represents a rotation by a quaternion.
42///
43/// # Examples
44/// ```
45/// # use munum::{transform, quat, assert_float_eq};
46/// assert_float_eq!(
47///     transform::rotation(quat(1./3_f32.sqrt(), 1./3_f32.sqrt(), 1./3_f32.sqrt(), 0.)).as_ref(),
48///     &[-1./3., 2./3., 2./3., 0., 2./3., -1./3., 2./3., 0., 2./3., 2./3., -1./3., 0., 0., 0., 0., 1.]
49/// );
50/// ```
51#[inline]
52pub fn rotation<T: Copy + NumAssign>(q: Quaternion<T>) -> Mat4<T> {
53    Mat3::from(q).into()
54}
55
56/// Creates a 4x4 matrix that represents a transformation in TRS order (= translation * rotation * scaling).
57///
58/// # Examples
59/// ```
60/// # use munum::{transform, quat, vec3, assert_float_eq};
61/// assert_float_eq!(
62///     transform::transformation(
63///         vec3(1., 2., 3.),
64///         quat(0.5/3_f32.sqrt(), 0.5/3_f32.sqrt(), 0.5/3_f32.sqrt(), 3_f32.sqrt()/2.),
65///         vec3(3., 6., 9.),
66///     ).as_ref(),
67///     &[2., 2., -1., 0., -2., 4., 4., 0., 6., -3., 6., 0., 1., 2., 3., 1.]
68/// );
69/// ```
70pub fn transformation<T: Copy + NumAssign>(
71    translation: Vec3<T>,
72    rotation: Quaternion<T>,
73    scaling: Vec3<T>,
74) -> Mat4<T> {
75    // Start with rotation
76    let mut result: Mat4<T> = Mat3::from(rotation).into();
77
78    // Post-multiply scaling
79    for c in 0..3 {
80        for r in 0..3 {
81            result[(r, c)] *= scaling[c];
82        }
83    }
84
85    // Apply translation
86    result[(0, 3)] = translation[0];
87    result[(1, 3)] = translation[1];
88    result[(2, 3)] = translation[2];
89
90    result
91}
92
93/// Extracts the (x, y, z) translation component from a 4x4 TRS transformation matrix.
94///
95/// # Examples
96/// ```
97/// # use munum::{transform, vec3, Mat4};
98/// assert_eq!(*transform::translation_of(<Mat4>::from_slice(&[2., 2., -1., 0., -2., 4., 4., 0., 6., -3., 6., 0., 11., 12., 13., 1.])).as_ref(), [11., 12., 13.]);
99/// ```
100pub fn translation_of<T: Copy + NumAssign>(m: Mat4<T>) -> Vec3<T> {
101    Vec3::new([[m[(0, 3)], m[(1, 3)], m[(2, 3)]]])
102}
103
104/// Extracts the (x, y, z) scaling component from a 4x4 TRS transformation matrix.
105///
106/// # Examples
107/// ```
108/// # use munum::{transform, vec3, Mat4};
109/// assert_eq!(*transform::scaling_of(<Mat4>::from_slice(&[2., 2., -1., 0., -2., 4., 4., 0., 6., -3., 6., 0., 11., 12., 13., 1.])).as_ref(), [3., 6., 9.]);
110/// ```
111#[inline]
112pub fn scaling_of<T: Copy + FloatOps + NumAssign>(m: Mat4<T>) -> Vec3<T> {
113    Vec3::new([[
114        Vec3::new([[m[(0, 0)], m[(1, 0)], m[(2, 0)]]]).len(),
115        Vec3::new([[m[(0, 1)], m[(1, 1)], m[(2, 1)]]]).len(),
116        Vec3::new([[m[(0, 2)], m[(1, 2)], m[(2, 2)]]]).len(),
117    ]])
118}
119
120/// Extracts the rotation quaternion component from a 4x4 TRS transformation matrix.
121///
122/// # Examples
123/// ```
124/// # use munum::{transform, vec3, Mat4, assert_float_eq};
125/// assert_float_eq!(
126///     transform::rotation_of(<Mat4>::from_slice(&[2., 2., -1., 0., -2., 4., 4., 0., 6., -3., 6., 0., 11., 12., 13., 1.])).as_ref(),
127///     &[0.5/3_f32.sqrt(), 0.5/3_f32.sqrt(), 0.5/3_f32.sqrt(), 3_f32.sqrt()/2.]
128/// );
129/// ```
130pub fn rotation_of<T: Copy + FloatOps + NumAssign + PartialOrd>(m: Mat4<T>) -> Quaternion<T> {
131    let zero = T::zero();
132    let one = T::one();
133    let two = one + one;
134    let scaling = scaling_of(m);
135    let m00 = m[(0, 0)] / scaling[0];
136    let m11 = m[(1, 1)] / scaling[1];
137    let m22 = m[(2, 2)] / scaling[2];
138
139    Quaternion::from_slice(&[
140        scalar::copysign(
141            scalar::max(zero, one + m00 - m11 - m22).sqrt() / two,
142            m[(2, 1)] / scaling[1] - m[(1, 2)] / scaling[2],
143        ),
144        scalar::copysign(
145            scalar::max(zero, one - m00 + m11 - m22).sqrt() / two,
146            m[(0, 2)] / scaling[2] - m[(2, 0)] / scaling[0],
147        ),
148        scalar::copysign(
149            scalar::max(zero, one - m00 - m11 + m22).sqrt() / two,
150            m[(1, 0)] / scaling[0] - m[(0, 1)] / scaling[1],
151        ),
152        scalar::max(zero, one + m00 + m11 + m22).sqrt() / two,
153    ])
154}
155
156/// Inverts a `Mat4` that represents a valid transformation in TRS order (= translation * rotation * scale).
157/// This function is more efficient than `Mat4::invert` by leveraging the properties of a TRS matrix.
158///
159/// # Examples
160/// ```
161/// # use munum::{transform, Mat4, assert_float_eq};
162/// let mut m = <Mat4>::from_slice(&[2., 2., -1., 0., -2., 4., 4., 0., 6., -3., 6., 0., 1., 2., 3., 1.]);
163/// assert!(transform::invert_trs(&mut m));
164/// assert_float_eq!(m.as_ref(), &[2./9., -1./18., 2./27., 0., 2./9., 1./9., -1./27., 0., -1./9., 1./9., 2./27., 0., -1./3., -1./2., -2./9., 1.]);
165/// ```
166pub fn invert_trs<T: Copy + FloatOps + NumAssign>(m: &mut Mat4<T>) -> bool {
167    // Assume M is a TRS matrix:
168    // M = T * R * S = [RS  t]
169    //                 [0   1]
170    // Then the inverse of M is:
171    // M^-1 = [(RS)^-1  (RS)^-1 * -t]
172    //        [   0           1     ]
173    // Where: (RS)^-1 = S^-1 * R^-1 = S^-1 * RT = S^-1 * ((RS)(S^-1))T = S^-1 * (S^-1)T * (RS)T = S^-1 * S^-1 * (RS)T
174
175    let zero = T::zero();
176    let one = T::one();
177    let neg = scalar::neg();
178
179    if m[(3, 0)] != zero || m[(3, 1)] != zero || m[(3, 2)] != zero || m[(3, 3)] != one {
180        return false;
181    }
182
183    // Extract S and t
184    let scaling = scaling_of(*m);
185    let translation = translation_of(*m);
186
187    // Calculate m = (RS)T
188    m.transpose();
189    m[(3, 0)] = zero;
190    m[(3, 1)] = zero;
191    m[(3, 2)] = zero;
192
193    // Premultiply S^-2 = 1/(S*S) to m
194    for c in 0..3 {
195        for r in 0..3 {
196            let factor = scaling[r] * scaling[r];
197            if factor == zero {
198                return false;
199            }
200            m[(r, c)] *= one / factor;
201        }
202    }
203
204    // Now m = (RS)^-1
205    // Apply translation = (m * -t) to m
206    let mut t = Vec4::from_vec3(translation, zero);
207    t *= neg;
208    t.mul_assign(*m, t);
209
210    m[(0, 3)] = t[0];
211    m[(1, 3)] = t[1];
212    m[(2, 3)] = t[2];
213
214    true
215}
216
217// endregion: Affine transformations
218
219// region: Projection matrices
220
221/// Creates the {@link Mat4} orthographic projection matrix.
222/// To apply a glTF orthographic camera, use: left = -xmag, right = xmag, bottom = -ymag, top = ymag.
223/// See: <https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#projection-matrices>
224///
225/// # Examples
226/// ```
227/// # use munum::{transform, assert_float_eq};
228/// assert_float_eq!(
229///     transform::ortho(-1., 3., -7., 4., -2., 5.).as_ref(),
230///     &[1./2., 0., 0., 0., 0., 2./11., 0., 0., 0., 0., -2./7., 0., -1./2., 3./11., -3./7., 1.]
231/// );
232/// ```
233pub fn ortho<T: Copy + NumAssign>(
234    left: T,
235    right: T,
236    bottom: T,
237    top: T,
238    znear: T,
239    zfar: T,
240) -> Mat4<T> {
241    let neg = scalar::neg();
242    let one = T::one();
243    let two = one + one;
244    let x = one / (right - left);
245    let y = one / (top - bottom);
246    let z = one / (znear - zfar);
247    let mut result = Mat4::identity();
248    result[(0, 0)] = two * x;
249    result[(1, 1)] = two * y;
250    result[(2, 2)] = two * z;
251
252    result[(0, 3)] = (right + left) * x * neg;
253    result[(1, 3)] = (top + bottom) * y * neg;
254    result[(2, 3)] = (znear + zfar) * z;
255
256    result
257}
258
259/// Creates the 4x4 perspective projection using glTF's formula.
260/// Uses infinite projection if zfar = Infinity.
261/// See: <https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#projection-matrices>
262///
263/// # Examples
264/// ```
265/// # use core::f32::consts::PI;
266/// # use core::f32::INFINITY;
267/// # use munum::{transform, assert_float_eq};
268/// assert_float_eq!(
269///     transform::perspective(2., PI/2., 1., INFINITY).as_ref(),
270///     &[0.5, 0., 0., 0., 0., 1., 0., 0., 0., 0., -1., -1., 0., 0., -2., 0.]
271/// );
272/// assert_float_eq!(
273///     transform::perspective(2., PI/2., 1., 9.).as_ref(),
274///     &[0.5, 0., 0., 0., 0., 1., 0., 0., 0., 0., -1.25, -1., 0., 0., -2.25, 0.]
275/// );
276/// ```
277#[inline]
278pub fn perspective<T: Copy + FloatCore + FloatOps + NumAssign + Signed>(
279    aspect: T,
280    yfov: T,
281    znear: T,
282    zfar: T,
283) -> Mat4<T> {
284    let two = T::one() + T::one();
285    let top = znear * (yfov / two).tan();
286    let right = aspect * top;
287    perspective_viewport(-right, right, -top, top, znear, zfar)
288}
289
290/// Creates the 4x4 perspective projection from viewport and range.
291/// Uses infinite projection if zfar = Infinity.
292///
293/// # Examples
294/// ```
295/// # use core::f32::INFINITY;
296/// # use munum::{transform, assert_float_eq};
297/// assert_float_eq!(
298///     transform::perspective_viewport(-1.0, 3.0, -0.5, 1.5, 1., INFINITY).as_ref(),
299///     &[0.5, 0., 0., 0., 0., 1., 0., 0., 0.5, 0.5, -1., -1., 0., 0., -2., 0.]
300/// );
301/// assert_float_eq!(
302///     transform::perspective_viewport(-1.0, 3.0, -0.5, 1.5, 1., 9.).as_ref(),
303///     &[0.5, 0., 0., 0., 0., 1., 0., 0., 0.5, 0.5, -1.25, -1., 0., 0., -2.25, 0.]
304/// );
305/// ```
306pub fn perspective_viewport<T: Copy + FloatCore + NumAssign + Signed>(
307    left: T,
308    right: T,
309    bottom: T,
310    top: T,
311    znear: T,
312    zfar: T,
313) -> Mat4<T> {
314    let one = T::one();
315    let two = one + one;
316
317    let x = one / (right - left);
318    let y = one / (top - bottom);
319
320    let mut result = Mat4::identity();
321    result[(0, 0)] = two * znear * x;
322    result[(1, 1)] = two * znear * y;
323    result[(0, 2)] = (right + left) * x;
324    result[(1, 2)] = (top + bottom) * y;
325    result[(3, 2)] = -one;
326    result[(3, 3)] = T::zero();
327
328    if zfar.is_finite() {
329        let range_inv = one / (znear - zfar);
330        result[(2, 2)] = (znear + zfar) * range_inv;
331        result[(2, 3)] = two * znear * zfar * range_inv;
332    } else {
333        result[(2, 2)] = -one;
334        result[(2, 3)] = -two * znear;
335    }
336
337    result
338}
339
340// endregion: Projection matrices
341
342// region: Camera matrices
343
344/// Calculates the `Mat4` model matrix for a camera at eye position looking at the center position with a given up direction.
345///
346/// # Examples
347/// ```
348/// # use munum::{transform, vec3, vec4, assert_float_eq};
349/// let m = transform::target_to(vec3(0_f32, 2., 0.), vec3(0., 0.6, 0.), vec3(0., 0., -1.));
350/// assert_float_eq!((m * vec4(0_f32, 2., 0., 1.)).as_ref(), &[0., 2., -2., 1.]);
351/// assert_float_eq!((m * vec4(0_f32, 2., -1., 1.)).as_ref(), &[0., 1., -2., 1.]);
352/// assert_float_eq!((m * vec4(1_f32, 2., 0., 1.)).as_ref(), &[1., 2., -2., 1.]);
353/// assert_float_eq!((m * vec4(0_f32, 1., 0., 1.)).as_ref(), &[0., 2., -1., 1.]);
354/// ```
355pub fn target_to<T: Copy + FloatOps + NumAssign>(
356    eye: Vec3<T>,
357    center: Vec3<T>,
358    up: Vec3<T>,
359) -> Mat4<T> {
360    let mut v = eye - center; // front
361    v.normalize();
362    let mut n = up.cross(v); // right
363    n.normalize();
364    let mut u = v.cross(n); // up
365    u.normalize();
366
367    let mut result = Mat4::identity();
368    for i in 0..3 {
369        result[(i, 0)] = n[i];
370        result[(i, 1)] = u[i];
371        result[(i, 2)] = v[i];
372        result[(i, 3)] = eye[i];
373    }
374    result
375}
376
377/// Calculate the 4x4 view matrix for a camera at eye position looking at the center position with a given up direction.
378///
379/// # Examples
380/// ```
381/// # use munum::{transform, vec3, vec4, assert_float_eq};
382/// let m = transform::look_at(vec3(0_f32, 2., 0.), vec3(0., 0.6, 0.), vec3(0., 0., -1.));
383/// assert_float_eq!((m * vec4(0_f32, 2., 0., 1.)).as_ref(), &[0., 0., 0., 1.]);
384/// assert_float_eq!((m * vec4(0_f32, 2., -1., 1.)).as_ref(), &[0., 1., 0., 1.]);
385/// assert_float_eq!((m * vec4(1_f32, 2., 0., 1.)).as_ref(), &[1., 0., 0., 1.]);
386/// assert_float_eq!((m * vec4(0_f32, 1., 0., 1.)).as_ref(), &[0., 0., -1., 1.]);
387/// ```
388pub fn look_at<T: Copy + FloatOps + NumAssign + Signed>(
389    eye: Vec3<T>,
390    center: Vec3<T>,
391    up: Vec3<T>,
392) -> Mat4<T> {
393    let mut v = center - eye; // front
394    v.normalize();
395    let mut n = v.cross(up); // right
396    n.normalize();
397    let mut u = n.cross(v); // up
398    u.normalize();
399
400    let mut result = Mat4::identity();
401    for i in 0..3 {
402        result[(0, i)] = n[i];
403        result[(1, i)] = u[i];
404        result[(2, i)] = -v[i];
405    }
406    result[(0, 3)] = -n.dot(eye);
407    result[(1, 3)] = -u.dot(eye);
408    result[(2, 3)] = v.dot(eye);
409    result
410}
411
412/// Calculate the look-at direction {@link Vec3} vector from pitch (up/down) and yaw (left/right) angles in radians.
413/// It looks towards -Z axis when pitch = 0 and yaw = 0.
414/// This can be used with look_at method to build an FPS camera view matrix by:
415/// ```view_matrix = look_at(eye, eye + look_at_direction(yaw, pitch)), vec3(0, 1, 0))```
416///
417/// # Examples
418/// ```
419/// # use core::f64::consts::{FRAC_PI_4, PI};
420/// # use munum::{transform::look_at_direction, vec3, vec4, assert_float_eq};
421/// assert_float_eq!(look_at_direction(0., 0.).as_ref(), &[0., 0., -1.]);
422/// assert_float_eq!(look_at_direction(PI, 0.).as_ref(), &[0., 0., 1.]);
423/// assert_float_eq!(look_at_direction(0., PI).as_ref(), &[0., 0., 1.]);
424/// assert_float_eq!(look_at_direction(PI / 2., 0.).as_ref(), &[0., 1., 0.]);
425/// assert_float_eq!(look_at_direction(0., -PI / 2.).as_ref(), &[1., 0., 0.]);
426/// assert_float_eq!(look_at_direction(PI / 4., -PI / 2.).as_ref(), &[FRAC_PI_4.cos(), FRAC_PI_4.sin(), 0.]);
427/// ```
428pub fn look_at_direction<T: Copy + FloatOps + NumAssign + Signed>(pitch: T, yaw: T) -> Vec3<T> {
429    let neg_cos_pitch = -pitch.cos();
430    Vec3::new([[
431        neg_cos_pitch * yaw.sin(),
432        pitch.sin(),
433        neg_cos_pitch * yaw.cos(),
434    ]])
435}
436
437// endregion: Camera matrices