Skip to main content

myth_core/
transform.rs

1use glam::{Affine3A, EulerRot, Mat3, Mat4, Quat, Vec3};
2
3/// Transform component for scene nodes.
4///
5/// Encapsulates position, rotation, and scale (TRS) along with
6/// cached matrices and dirty-checking logic for efficient updates.
7///
8/// # Coordinate System
9///
10/// Uses a right-handed coordinate system:
11/// - +X: Right
12/// - +Y: Up
13/// - +Z: Forward (out of screen)
14///
15/// # Matrix Updates
16///
17/// The transform maintains two cached matrices:
18/// - `local_matrix`: Transform relative to parent
19/// - `world_matrix`: Transform relative to world origin
20///
21/// These are updated lazily when TRS values change, tracked by
22/// dirty checking against previous frame values.
23///
24/// # Example
25///
26/// ```rust,ignore
27/// let mut transform = Transform::new();
28/// transform.position = Vec3::new(1.0, 2.0, 3.0);
29/// transform.rotation = Quat::from_rotation_y(std::f32::consts::FRAC_PI_2);
30/// transform.scale = Vec3::splat(2.0);
31/// ```
32#[derive(Debug, Copy, Clone)]
33pub struct Transform {
34    /// Local position relative to parent.
35    pub position: Vec3,
36    /// Local rotation as quaternion.
37    pub rotation: Quat,
38    /// Local scale factor.
39    pub scale: Vec3,
40
41    /// Cached local-space matrix. Updated by [`update_local_matrix`](Self::update_local_matrix).
42    #[doc(hidden)]
43    pub local_matrix: Affine3A,
44    /// Cached world-space matrix. Updated by the transform system.
45    #[doc(hidden)]
46    pub world_matrix: Affine3A,
47    /// World matrix from the previous frame (used for motion vectors / TAA).
48    #[doc(hidden)]
49    pub previous_world_matrix: Affine3A,
50
51    // Dirty checking state
52    last_position: Vec3,
53    last_rotation: Quat,
54    last_scale: Vec3,
55    force_update: bool,
56}
57
58impl Transform {
59    /// Creates a new transform with identity values.
60    #[must_use]
61    pub fn new() -> Self {
62        Self {
63            position: Vec3::ZERO,
64            rotation: Quat::IDENTITY,
65            scale: Vec3::ONE,
66
67            local_matrix: Affine3A::IDENTITY,
68            world_matrix: Affine3A::IDENTITY,
69
70            previous_world_matrix: Affine3A::IDENTITY,
71
72            last_position: Vec3::ZERO,
73            last_rotation: Quat::IDENTITY,
74            last_scale: Vec3::ONE,
75            force_update: true,
76        }
77    }
78
79    /// Checks and updates the local matrix if TRS values changed.
80    ///
81    /// Returns `true` if the matrix was updated, `false` otherwise.
82    /// This method is called by the transform system during hierarchy traversal.
83    pub fn update_local_matrix(&mut self) -> bool {
84        let changed = self.position != self.last_position
85            || self.rotation != self.last_rotation
86            || self.scale != self.last_scale
87            || self.force_update;
88
89        if changed {
90            self.local_matrix =
91                Affine3A::from_scale_rotation_translation(self.scale, self.rotation, self.position);
92
93            self.last_position = self.position;
94            self.last_rotation = self.rotation;
95            self.last_scale = self.scale;
96            self.force_update = false;
97        }
98
99        changed
100    }
101
102    /// Sets rotation from Euler angles (XYZ order).
103    ///
104    /// # Arguments
105    ///
106    /// * `x` - Rotation around X axis in radians
107    /// * `y` - Rotation around Y axis in radians
108    /// * `z` - Rotation around Z axis in radians
109    pub fn set_rotation_euler(&mut self, x: f32, y: f32, z: f32) {
110        self.rotation = Quat::from_euler(EulerRot::XYZ, x, y, z);
111    }
112
113    /// Returns the rotation as Euler angles (XYZ order) in radians.
114    #[must_use]
115    pub fn rotation_euler(&self) -> Vec3 {
116        let (x, y, z) = self.rotation.to_euler(EulerRot::XYZ);
117        Vec3::new(x, y, z)
118    }
119
120    /// Sets rotation from Euler angles with a custom rotation order.
121    pub fn set_rotation_euler_with_order(&mut self, x: f32, y: f32, z: f32, order: EulerRot) {
122        self.rotation = Quat::from_euler(order, x, y, z);
123    }
124
125    /// Returns a reference to the cached local matrix.
126    #[inline]
127    #[must_use]
128    pub fn local_matrix(&self) -> &Affine3A {
129        &self.local_matrix
130    }
131
132    /// Returns a reference to the cached world matrix.
133    #[inline]
134    #[must_use]
135    pub fn world_matrix(&self) -> &Affine3A {
136        &self.world_matrix
137    }
138
139    /// Returns the world matrix converted to [`Mat4`].
140    #[inline]
141    #[must_use]
142    pub fn world_matrix_as_mat4(&self) -> Mat4 {
143        Mat4::from(self.world_matrix)
144    }
145
146    /// Sets the world matrix directly.
147    pub fn set_world_matrix(&mut self, mat: Affine3A) {
148        self.world_matrix = mat;
149    }
150
151    /// Sets position and marks the transform dirty.
152    pub fn set_position(&mut self, pos: Vec3) {
153        self.position = pos;
154        self.mark_dirty();
155    }
156
157    /// Directly sets the local matrix (e.g. from glTF or a physics engine).
158    /// Decomposes it into TRS and synchronizes state.
159    pub fn apply_local_matrix(&mut self, mat: Affine3A) {
160        self.local_matrix = mat;
161        let (scale, rotation, translation) = mat.to_scale_rotation_translation();
162        self.scale = scale;
163        self.rotation = rotation;
164        self.position = translation;
165        self.last_scale = scale;
166        self.last_rotation = rotation;
167        self.last_position = translation;
168        self.force_update = false;
169    }
170
171    /// Applies a local matrix from [`Mat4`].
172    pub fn apply_local_matrix_from_mat4(&mut self, mat: Mat4) {
173        let affine = Affine3A::from_mat4(mat);
174        self.apply_local_matrix(affine);
175    }
176
177    /// Orients the transform to face a target point (in parent space).
178    pub fn look_at(&mut self, target: Vec3, up: Vec3) {
179        let forward = (target - self.position).normalize();
180        if forward.cross(up).length_squared() < 1e-4 {
181            return;
182        }
183        let right = forward.cross(up).normalize();
184        let new_up = right.cross(forward).normalize();
185        let rot_mat = Mat3::from_cols(right, new_up, -forward);
186        self.rotation = Quat::from_mat3(&rot_mat);
187    }
188
189    /// Marks the transform as dirty, forcing a matrix recompute next frame.
190    #[inline]
191    pub fn mark_dirty(&mut self) {
192        self.force_update = true;
193    }
194
195    /// Returns the local coordinate axes based on current rotation.
196    ///
197    /// Returns (right, up, forward) vectors in world space.
198    #[must_use]
199    pub fn rotation_basis(&self) -> (Vec3, Vec3, Vec3) {
200        let right = self.rotation * Vec3::X;
201        let up = self.rotation * Vec3::Y;
202        let forward = self.rotation * Vec3::Z;
203        (right, up, forward)
204    }
205}
206
207impl Default for Transform {
208    fn default() -> Self {
209        Self::new()
210    }
211}