Skip to main content

rs_math3d/
transforms.rs

1// Copyright 2020-Present (c) Raja Lehtihet & Wael El Oraiby
2//
3// Redistribution and use in source and binary forms, with or without
4// modification, are permitted provided that the following conditions are met:
5//
6// 1. Redistributions of source code must retain the above copyright notice,
7// this list of conditions and the following disclaimer.
8//
9// 2. Redistributions in binary form must reproduce the above copyright notice,
10// this list of conditions and the following disclaimer in the documentation
11// and/or other materials provided with the distribution.
12//
13// 3. Neither the name of the copyright holder nor the names of its contributors
14// may be used to endorse or promote products derived from this software without
15// specific prior written permission.
16//
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27// POSSIBILITY OF SUCH DAMAGE.
28//! 3D transformation functions for computer graphics.
29//!
30//! This module provides functions to create and manipulate transformation
31//! matrices commonly used in 3D graphics, including translation, rotation,
32//! scaling, and projection matrices.
33//!
34//! # Examples
35//!
36//! ```
37//! use rs_math3d::transforms;
38//! use rs_math3d::vector::Vector3;
39//!
40//! // Create a translation matrix
41//! let translation = transforms::translate(Vector3::new(10.0, 5.0, 0.0));
42//!
43//! // Create a perspective projection matrix
44//! let projection = transforms::perspective(
45//!     45.0f32.to_radians(),  // Field of view
46//!     16.0 / 9.0,            // Aspect ratio
47//!     0.1,                   // Near plane
48//!     100.0                  // Far plane
49//! );
50//! ```
51
52use crate::matrix::*;
53use crate::quaternion::*;
54use crate::scalar::*;
55use crate::vector::*;
56use num_traits::{One, Zero};
57
58/// Creates a 4x4 translation matrix.
59///
60/// The translation matrix has the form:
61/// ```text
62/// [1  0  0  tx]
63/// [0  1  0  ty]
64/// [0  0  1  tz]
65/// [0  0  0  1 ]
66/// ```
67///
68/// # Parameters
69/// - `trans`: Translation vector (tx, ty, tz)
70pub fn translate<T: Scalar>(trans: Vector3<T>) -> Matrix4<T> {
71    Matrix4::new(
72        <T as One>::one(),
73        <T as Zero>::zero(),
74        <T as Zero>::zero(),
75        <T as Zero>::zero(),
76        <T as Zero>::zero(),
77        <T as One>::one(),
78        <T as Zero>::zero(),
79        <T as Zero>::zero(),
80        <T as Zero>::zero(),
81        <T as Zero>::zero(),
82        <T as One>::one(),
83        <T as Zero>::zero(),
84        trans.x,
85        trans.y,
86        trans.z,
87        <T as One>::one(),
88    )
89}
90
91/// Creates a 4x4 scaling matrix.
92///
93/// The scaling matrix has the form:
94/// ```text
95/// [sx 0  0  0]
96/// [0  sy 0  0]
97/// [0  0  sz 0]
98/// [0  0  0  1]
99/// ```
100///
101/// # Parameters
102/// - `scale`: Scale factors for each axis
103pub fn scale<T: Scalar>(scale: Vector3<T>) -> Matrix4<T> {
104    Matrix4::new(
105        scale.x,
106        <T as Zero>::zero(),
107        <T as Zero>::zero(),
108        <T as Zero>::zero(),
109        <T as Zero>::zero(),
110        scale.y,
111        <T as Zero>::zero(),
112        <T as Zero>::zero(),
113        <T as Zero>::zero(),
114        <T as Zero>::zero(),
115        scale.z,
116        <T as Zero>::zero(),
117        <T as Zero>::zero(),
118        <T as Zero>::zero(),
119        <T as Zero>::zero(),
120        <T as One>::one(),
121    )
122}
123
124/// Creates a 4x4 rotation matrix from a quaternion.
125///
126/// Converts a quaternion to its equivalent rotation matrix.
127///
128/// The quaternion is normalized before conversion so the result is always a
129/// pure rotation matrix.
130pub fn rotation_from_quat<T: FloatScalar>(q: &Quat<T>) -> Matrix4<T> {
131    Quat::mat4(q)
132}
133
134/// Creates a 4x4 rotation matrix from an axis and angle.
135///
136/// # Parameters
137/// - `axis`: The rotation axis (will be normalized)
138/// - `angle`: The rotation angle in radians
139/// - `epsilon`: Minimum axis length to treat as valid
140///
141/// # Returns
142/// - `Some(matrix)` for a valid axis
143/// - `None` if the axis length is too small
144pub fn rotation_from_axis_angle<T: FloatScalar>(
145    axis: &Vector3<T>,
146    angle: T,
147    epsilon: T,
148) -> Option<Matrix4<T>> {
149    Quat::of_axis_angle(axis, angle, epsilon).map(|q| q.mat4())
150}
151
152/// Transforms a 3D vector by a 4x4 matrix with perspective division.
153///
154/// The vector is treated as a point (w=1) and the result is divided by w.
155pub fn transform_vec3<T: FloatScalar>(m: &Matrix4<T>, v: &Vector3<T>) -> Vector3<T> {
156    let v4 = Vector4::new(v.x, v.y, v.z, <T as One>::one());
157    let vout = *m * v4;
158    Vector3::new(vout.x / vout.w, vout.y / vout.w, vout.z / vout.w)
159}
160
161/// Projects a 3D point to screen coordinates.
162///
163/// # Parameters
164/// - `world`: World transformation matrix
165/// - `persp`: Perspective projection matrix
166/// - `lb`: Screen left-bottom corner
167/// - `rt`: Screen right-top corner
168/// - `pt`: Point to project
169///
170/// # Returns
171/// Screen coordinates with z in \[0,1\] (0=near, 1=far)
172pub fn project3<T: FloatScalar>(
173    world: &Matrix4<T>,
174    persp: &Matrix4<T>,
175    lb: &Vector2<T>,
176    rt: &Vector2<T>,
177    pt: &Vector3<T>,
178) -> Vector3<T> {
179    let inp = Vector4::new(pt.x, pt.y, pt.z, <T as One>::one());
180    let pw = *persp * *world;
181    let mut out = pw * inp;
182
183    out.x /= out.w;
184    out.y /= out.w;
185    out.z /= out.w;
186
187    let out_x = lb.x + ((rt.x - lb.x) * (out.x + <T as One>::one()) * T::half());
188    let out_y = lb.y + ((rt.y - lb.y) * (out.y + <T as One>::one()) * T::half());
189    let out_z = (out.z + <T as One>::one()) * T::half();
190    Vector3::new(out_x, out_y, out_z)
191}
192
193/// Unprojects screen coordinates back to 3D world space.
194///
195/// # Parameters
196/// - `world`: World transformation matrix
197/// - `persp`: Perspective projection matrix
198/// - `lb`: Screen left-bottom corner
199/// - `rt`: Screen right-top corner
200/// - `pt`: Screen point with z-depth
201///
202/// # Returns
203/// The corresponding 3D world point
204pub fn unproject3<T: FloatScalar>(
205    world: &Matrix4<T>,
206    persp: &Matrix4<T>,
207    lb: &Vector2<T>,
208    rt: &Vector2<T>,
209    pt: &Vector3<T>,
210) -> Vector3<T> {
211    let pw = *persp * *world;
212    let inv = if pw.is_affine(T::epsilon()) {
213        pw.inverse_affine()
214    } else {
215        pw.inverse()
216    };
217    let in_x = (T::two() * (pt.x - lb.x) / (rt.x - lb.x)) - <T as One>::one();
218    let in_y = (T::two() * (pt.y - lb.y) / (rt.y - lb.y)) - <T as One>::one();
219    let in_z = (T::two() * pt.z) - <T as One>::one();
220    let in_w = <T as One>::one();
221    let inp = Vector4::new(in_x, in_y, in_z, in_w);
222    let out = inv * inp;
223    let out4 = out / out.w;
224    Vector3::new(out4.x, out4.y, out4.z)
225}
226
227/// Creates a perspective projection matrix from frustum bounds.
228///
229/// # Parameters
230/// - `lbn`: Left-bottom-near corner (x, y, z)
231/// - `rtf`: Right-top-far corner (x, y, z)
232///
233/// The frustum is defined by the near and far clipping planes.
234pub fn frustum<T: FloatScalar>(lbn: &Vector3<T>, rtf: &Vector3<T>) -> Matrix4<T> {
235    let width = rtf.x - lbn.x;
236    let height = rtf.y - lbn.y;
237    let depth = rtf.z - lbn.z;
238    let a = (rtf.x + lbn.x) / width;
239    let b = (rtf.y + lbn.y) / height;
240    let c = -(rtf.z + lbn.z) / depth;
241    let d = -(T::two() * rtf.z * lbn.z) / depth;
242
243    Matrix4::new(
244        T::two() * lbn.z / width,
245        <T as Zero>::zero(),
246        <T as Zero>::zero(),
247        <T as Zero>::zero(),
248        <T as Zero>::zero(),
249        T::two() * lbn.z / height,
250        <T as Zero>::zero(),
251        <T as Zero>::zero(),
252        a,
253        b,
254        c,
255        -<T as One>::one(),
256        <T as Zero>::zero(),
257        <T as Zero>::zero(),
258        d,
259        <T as Zero>::zero(),
260    )
261}
262
263/// Creates an orthographic projection matrix.
264///
265/// # Parameters
266/// - `left`, `right`: X-axis bounds
267/// - `bottom`, `top`: Y-axis bounds
268/// - `near`, `far`: Z-axis bounds (depth)
269///
270/// Objects maintain their size regardless of depth in orthographic projection.
271pub fn ortho4<T: FloatScalar>(left: T, right: T, bottom: T, top: T, near: T, far: T) -> Matrix4<T> {
272    let width = right - left;
273    let height = top - bottom;
274    let depth = far - near;
275    let r00 = T::two() / width;
276    let r11 = T::two() / height;
277    let r22 = -T::two() / depth;
278    let r03 = -(right + left) / width;
279    let r13 = -(top + bottom) / height;
280    let r23 = -(far + near) / depth;
281    Matrix4::new(
282        r00,
283        <T as Zero>::zero(),
284        <T as Zero>::zero(),
285        <T as Zero>::zero(),
286        <T as Zero>::zero(),
287        r11,
288        <T as Zero>::zero(),
289        <T as Zero>::zero(),
290        <T as Zero>::zero(),
291        <T as Zero>::zero(),
292        r22,
293        <T as Zero>::zero(),
294        r03,
295        r13,
296        r23,
297        <T as One>::one(),
298    )
299}
300
301/// Creates a perspective projection matrix.
302///
303/// # Parameters
304/// - `fovy`: Field of view angle in radians (vertical)
305/// - `aspect`: Aspect ratio (width / height)
306/// - `near`: Near clipping plane distance
307/// - `far`: Far clipping plane distance
308///
309/// Uses the standard OpenGL perspective projection formula.
310pub fn perspective<T: FloatScalar>(fovy: T, aspect: T, near: T, far: T) -> Matrix4<T> {
311    let f = <T as One>::one() / T::ttan(fovy * T::half());
312    let denom = near - far;
313    let a = (far + near) / denom;
314    let b = (T::two() * far * near) / denom;
315
316    Matrix4::new(
317        f / aspect,
318        <T as Zero>::zero(),
319        <T as Zero>::zero(),
320        <T as Zero>::zero(),
321        <T as Zero>::zero(),
322        f,
323        <T as Zero>::zero(),
324        <T as Zero>::zero(),
325        <T as Zero>::zero(),
326        <T as Zero>::zero(),
327        a,
328        -<T as One>::one(),
329        <T as Zero>::zero(),
330        <T as Zero>::zero(),
331        b,
332        <T as Zero>::zero(),
333    )
334}
335
336/// Creates a view matrix looking from eye position to target.
337///
338/// # Parameters
339/// - `eye`: Camera position
340/// - `dest`: Target position to look at
341/// - `up`: Up vector (typically (0, 1, 0))
342///
343/// The resulting matrix transforms from world space to view space.
344///
345/// # Preconditions
346/// - `eye` and `dest` must not be the same point
347/// - `up` must not be parallel, or nearly parallel, to the viewing direction
348///
349/// This routine performs unchecked normalization. Violating the preconditions
350/// yields non-finite matrix components.
351pub fn lookat<T: FloatScalar>(eye: &Vector3<T>, dest: &Vector3<T>, up: &Vector3<T>) -> Matrix4<T> {
352    let f = Vector3::normalize(&(*dest - *eye));
353    let s = Vector3::normalize(&Vector3::cross(&f, up));
354    let u = Vector3::normalize(&Vector3::cross(&s, &f));
355
356    let trans = translate(-*eye);
357
358    let m = Matrix4::new(
359        s.x,
360        u.x,
361        -f.x,
362        <T as Zero>::zero(),
363        s.y,
364        u.y,
365        -f.y,
366        <T as Zero>::zero(),
367        s.z,
368        u.z,
369        -f.z,
370        <T as Zero>::zero(),
371        <T as Zero>::zero(),
372        <T as Zero>::zero(),
373        <T as Zero>::zero(),
374        <T as One>::one(),
375    );
376    m * trans
377}
378
379/// Decomposes a transformation matrix into scale, rotation, and translation.
380///
381/// # Returns
382/// - `Some((scale, rotation, translation))` if successful
383/// - `None` if the matrix is singular or has zero scale
384///
385/// # Note
386/// This assumes the matrix represents a valid affine transformation.
387pub fn decompose<T: FloatScalar>(m: &Matrix4<T>) -> Option<(Vector3<T>, Quat<T>, Vector3<T>)> {
388    let mut col0 = Vector3::new(m.col[0].x, m.col[0].y, m.col[0].z);
389    let mut col1 = Vector3::new(m.col[1].x, m.col[1].y, m.col[1].z);
390    let mut col2 = Vector3::new(m.col[2].x, m.col[2].y, m.col[2].z);
391    let det = m.determinant();
392
393    // the scale needs to be tested
394    let mut scale = Vector3::new(
395        Vector3::length(&col0),
396        Vector3::length(&col1),
397        Vector3::length(&col2),
398    );
399    let trans = Vector3::new(m.col[3].x, m.col[3].y, m.col[3].z);
400
401    if det < <T as Zero>::zero() {
402        scale = -scale;
403    }
404
405    if scale.x != <T as Zero>::zero() {
406        col0 = col0 / scale.x;
407    } else {
408        return Option::None;
409    }
410
411    if scale.y != <T as Zero>::zero() {
412        col1 = col1 / scale.y;
413    } else {
414        return Option::None;
415    }
416
417    if scale.z != <T as Zero>::zero() {
418        col2 = col2 / scale.z;
419    } else {
420        return Option::None;
421    }
422
423    let rot_matrix = Matrix3::new(
424        col0.x, col0.y, col0.z, col1.x, col1.y, col1.z, col2.x, col2.y, col2.z,
425    );
426
427    let rot = Quat::of_matrix3(&rot_matrix);
428
429    Some((scale, rot, trans))
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    #[test]
436    pub fn test_decompose() {
437        let ms = scale(Vector3::<f32>::new(4.0, 5.0, 6.0));
438        let mt = translate(Vector3::<f32>::new(1.0, 2.0, 3.0));
439        let q = Quat::<f32>::of_axis_angle(&Vector3::new(1.0, 1.0, 1.0), 1.0, EPS_F32)
440            .expect("axis length too small");
441        let mr = rotation_from_quat(&q);
442
443        let m = mt * mr * ms;
444
445        let v = decompose(&m);
446        match v {
447            None => assert_eq!(1, 2),
448            Some((s, r, t)) => {
449                assert!((s.x - 4.0) < f32::epsilon());
450                assert!((s.y - 5.0) < f32::epsilon());
451                assert!((s.z - 6.0) < f32::epsilon());
452
453                assert!((q.x - r.x) < f32::epsilon());
454                assert!((q.y - r.y) < f32::epsilon());
455                assert!((q.z - r.z) < f32::epsilon());
456                assert!((q.w - r.w) < f32::epsilon());
457
458                assert!((t.x - 1.0) < f32::epsilon());
459                assert!((t.y - 2.0) < f32::epsilon());
460                assert!((t.z - 3.0) < f32::epsilon());
461            }
462        }
463    }
464
465    #[test]
466    fn test_rotation_from_axis_angle_zero_axis() {
467        let axis = Vector3::<f32>::new(0.0, 0.0, 0.0);
468        assert!(rotation_from_axis_angle(&axis, 1.0, EPS_F32).is_none());
469    }
470
471    #[test]
472    fn test_project_unproject_roundtrip() {
473        let world = Matrix4::<f32>::identity();
474        let proj = ortho4(-1.0, 1.0, -1.0, 1.0, 0.1, 10.0);
475        let lb = Vector2::new(-1.0, -1.0);
476        let rt = Vector2::new(1.0, 1.0);
477        let pt = Vector3::new(0.2, -0.4, 1.0);
478
479        let screen = project3(&world, &proj, &lb, &rt, &pt);
480        let out = unproject3(&world, &proj, &lb, &rt, &screen);
481
482        assert!((out.x - pt.x).abs() < 0.001);
483        assert!((out.y - pt.y).abs() < 0.001);
484        assert!((out.z - pt.z).abs() < 0.001);
485    }
486
487    #[test]
488    fn test_lookat_identity() {
489        let eye = Vector3::new(0.0f32, 0.0, 0.0);
490        let dest = Vector3::new(0.0f32, 0.0, -1.0);
491        let up = Vector3::new(0.0f32, 1.0, 0.0);
492        let view = lookat(&eye, &dest, &up);
493        let v = Vector4::new(1.0f32, 2.0, 3.0, 1.0);
494        let out = view * v;
495
496        assert!((out.x - v.x).abs() < 0.001);
497        assert!((out.y - v.y).abs() < 0.001);
498        assert!((out.z - v.z).abs() < 0.001);
499        assert!((out.w - v.w).abs() < 0.001);
500    }
501}