Skip to main content

scenix_math/
transform.rs

1use crate::{Mat4, Quat, Vec3};
2
3/// A translation, rotation, and scale transform.
4#[derive(Clone, Copy, Debug, PartialEq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub struct Transform {
7    /// Local translation.
8    pub translation: Vec3,
9    /// Local rotation.
10    pub rotation: Quat,
11    /// Local scale.
12    pub scale: Vec3,
13}
14
15impl Transform {
16    /// Identity transform.
17    pub const IDENTITY: Self = Self::new(Vec3::ZERO, Quat::IDENTITY, Vec3::ONE);
18
19    /// Creates a transform from translation, rotation, and scale.
20    #[inline]
21    pub const fn new(translation: Vec3, rotation: Quat, scale: Vec3) -> Self {
22        Self {
23            translation,
24            rotation,
25            scale,
26        }
27    }
28
29    /// Creates a translation transform.
30    #[inline]
31    pub const fn from_translation(value: Vec3) -> Self {
32        Self::new(value, Quat::IDENTITY, Vec3::ONE)
33    }
34
35    /// Creates a rotation transform.
36    #[inline]
37    pub const fn from_rotation(value: Quat) -> Self {
38        Self::new(Vec3::ZERO, value, Vec3::ONE)
39    }
40
41    /// Creates a scale transform.
42    #[inline]
43    pub const fn from_scale(value: Vec3) -> Self {
44        Self::new(Vec3::ZERO, Quat::IDENTITY, value)
45    }
46
47    /// Creates a transform that looks from `eye` toward `target`.
48    pub fn looking_at(eye: Vec3, target: Vec3, up: Vec3) -> Self {
49        let view = Mat4::look_at(eye, target, up);
50        let world = view.inverse().unwrap_or(Mat4::from_translation(eye));
51        world.decompose().unwrap_or(Self::from_translation(eye))
52    }
53
54    /// Converts the transform to a column-major matrix.
55    #[inline]
56    pub fn to_mat4(self) -> Mat4 {
57        Mat4::from_trs(self.translation, self.rotation, self.scale)
58    }
59
60    /// Creates a transform from a TRS matrix.
61    #[inline]
62    pub fn from_mat4(matrix: Mat4) -> Option<Self> {
63        matrix.decompose()
64    }
65
66    /// Composes two transforms.
67    #[inline]
68    pub fn mul_transform(self, rhs: Self) -> Self {
69        Self::from_mat4(self.to_mat4() * rhs.to_mat4()).unwrap_or(Self::IDENTITY)
70    }
71
72    /// Returns the inverse transform.
73    #[inline]
74    pub fn inverse(self) -> Self {
75        self.to_mat4()
76            .inverse()
77            .and_then(Self::from_mat4)
78            .unwrap_or(Self::IDENTITY)
79    }
80
81    /// Returns the transformed forward direction (`-Z`).
82    #[inline]
83    pub fn forward(self) -> Vec3 {
84        self.rotation.mul_vec3(Vec3::NEG_Z).normalize()
85    }
86
87    /// Returns the transformed right direction (`+X`).
88    #[inline]
89    pub fn right(self) -> Vec3 {
90        self.rotation.mul_vec3(Vec3::X).normalize()
91    }
92
93    /// Returns the transformed up direction (`+Y`).
94    #[inline]
95    pub fn up(self) -> Vec3 {
96        self.rotation.mul_vec3(Vec3::Y).normalize()
97    }
98
99    /// Returns this transform translated by `delta`.
100    #[inline]
101    pub fn translate_by(mut self, delta: Vec3) -> Self {
102        self.translation += delta;
103        self
104    }
105
106    /// Returns this transform rotated by `rotation`.
107    #[inline]
108    pub fn rotate_by(mut self, rotation: Quat) -> Self {
109        self.rotation = (self.rotation * rotation).normalize();
110        self
111    }
112
113    /// Returns this transform scaled component-wise by `scale`.
114    #[inline]
115    pub fn scale_by(mut self, scale: Vec3) -> Self {
116        self.scale = self.scale.mul_elements(scale);
117        self
118    }
119}
120
121impl Default for Transform {
122    #[inline]
123    fn default() -> Self {
124        Self::IDENTITY
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::assert_close;
132
133    #[test]
134    fn transform_matrix_decomposes_back_to_trs() {
135        let transform = Transform::new(
136            Vec3::new(1.0, 2.0, 3.0),
137            Quat::from_axis_angle(Vec3::Y, 0.8),
138            Vec3::new(2.0, 2.0, 2.0),
139        );
140        let out = Transform::from_mat4(transform.to_mat4()).unwrap();
141        assert_close(out.translation.x, transform.translation.x);
142        assert_close(out.translation.y, transform.translation.y);
143        assert_close(out.translation.z, transform.translation.z);
144        assert_close(out.scale.x, transform.scale.x);
145        assert_close(out.rotation.angle_between(transform.rotation), 0.0);
146    }
147
148    #[test]
149    fn inverse_undoes_transform() {
150        let transform = Transform::new(
151            Vec3::new(1.0, 2.0, 3.0),
152            Quat::from_axis_angle(Vec3::Y, 0.6),
153            Vec3::new(2.0, 2.0, 2.0),
154        );
155        let point = Vec3::new(4.0, 5.0, 6.0);
156        let moved = transform.to_mat4().mul_vec3(point);
157        let restored = transform.inverse().to_mat4().mul_vec3(moved);
158        assert_close(restored.x, point.x);
159        assert_close(restored.y, point.y);
160        assert_close(restored.z, point.z);
161    }
162
163    #[test]
164    fn direction_vectors_follow_rotation() {
165        let transform =
166            Transform::from_rotation(Quat::from_axis_angle(Vec3::Y, core::f32::consts::FRAC_PI_2));
167        assert_close(transform.forward().x, -1.0);
168        assert_close(transform.right().z, -1.0);
169        assert_close(transform.up().y, 1.0);
170    }
171}