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