//! Affine [`Transform`]: translation, rotation, and (non-uniform) scale. //! //! A `Transform` is the canonical way to place an object in space. It composes //! as `parent * child`, matching the convention used by the scene graph in //! later stages. Internally it is stored in decomposed (TRS) form so that //! individual components stay editable without matrix round-trips. use glam::{Affine3A, Mat4, Quat, Vec3}; use serde::{Deserialize, Serialize}; /// A 3D affine transform stored as translation, rotation, and scale. /// /// The effective matrix is `T * R * S` (scale applied first, then rotation, /// then translation), which is the standard convention for scene hierarchies. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, crate::reflect::Reflect)] pub struct Transform { /// World/local-space position. pub translation: Vec3, /// Orientation as a unit quaternion. pub rotation: Quat, /// Per-axis scale. May be non-uniform; zero or negative components are /// permitted but make the transform non-invertible / mirror-inducing. pub scale: Vec3, } impl Default for Transform { /// The identity transform: no translation, no rotation, unit scale. fn default() -> Self { Self::IDENTITY } } impl Transform { /// The identity transform. pub const IDENTITY: Self = Self { translation: Vec3::ZERO, rotation: Quat::IDENTITY, scale: Vec3::ONE, }; /// Creates a transform from a translation only (identity rotation, unit scale). #[inline] pub const fn from_translation(translation: Vec3) -> Self { Self { translation, rotation: Quat::IDENTITY, scale: Vec3::ONE, } } /// Creates a transform from a rotation only. #[inline] pub const fn from_rotation(rotation: Quat) -> Self { Self { translation: Vec3::ZERO, rotation, scale: Vec3::ONE, } } /// Creates a transform from a uniform scale. #[inline] pub const fn from_scale(scale: Vec3) -> Self { Self { translation: Vec3::ZERO, rotation: Quat::IDENTITY, scale, } } /// Creates a transform from all three components. #[inline] pub const fn from_trs(translation: Vec3, rotation: Quat, scale: Vec3) -> Self { Self { translation, rotation, scale, } } /// Decomposes a 4x4 matrix back into a TRS transform. /// /// Negative determinants (mirrored matrices) are handled by `glam`'s /// decomposition, which folds the sign into the scale. #[inline] pub fn from_matrix(matrix: Mat4) -> Self { let (scale, rotation, translation) = matrix.to_scale_rotation_translation(); Self { translation, rotation, scale, } } /// Returns the equivalent 4x4 homogeneous matrix. #[inline] pub fn to_matrix(&self) -> Mat4 { Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation) } /// Returns the equivalent [`Affine3A`], which is cheaper to compose than a /// full `Mat4` and is what the renderer/scene graph use internally. #[inline] pub fn to_affine(&self) -> Affine3A { Affine3A::from_scale_rotation_translation(self.scale, self.rotation, self.translation) } /// Composes two transforms: `self * rhs` applies `rhs` first, then `self`. /// /// This is exact for the translation and rotation channels. When either /// operand carries non-uniform scale combined with rotation, the true /// product is no longer a pure TRS transform; in that case the result is /// re-decomposed from the composed matrix so the returned `Transform` /// remains the closest TRS approximation. For uniform scale (the common /// scene-graph case) the composition is exact. #[inline] pub fn mul_transform(&self, rhs: &Transform) -> Transform { // Fast path: uniform scale composes exactly in TRS form. if is_uniform(self.scale) { let scale = self.scale * rhs.scale; let rotation = self.rotation * rhs.rotation; let translation = self.translation + self.rotation * (self.scale * rhs.translation); Transform { translation, rotation, scale, } } else { Transform::from_matrix(self.to_matrix() * rhs.to_matrix()) } } /// Transforms a point (affected by translation, rotation, and scale). #[inline] pub fn transform_point(&self, point: Vec3) -> Vec3 { self.translation + self.rotation * (self.scale * point) } /// Transforms a direction vector (rotation and scale only, no translation). #[inline] pub fn transform_vector(&self, vector: Vec3) -> Vec3 { self.rotation * (self.scale * vector) } /// Returns the inverse transform, such that /// `t.mul_transform(&t.inverse())` is approximately the identity. /// /// # Panics /// Does not panic, but if any scale component is zero the inverse scale /// will contain infinities — the transform is not invertible in that case. #[inline] pub fn inverse(&self) -> Transform { let inv_scale = Vec3::ONE / self.scale; let inv_rotation = self.rotation.inverse(); let inv_translation = inv_rotation * (inv_scale * -self.translation); Transform { translation: inv_translation, rotation: inv_rotation, scale: inv_scale, } } /// The local forward direction (`-Z`) rotated into this transform's space. #[inline] pub fn forward(&self) -> Vec3 { self.rotation * Vec3::NEG_Z } /// The local up direction (`+Y`) rotated into this transform's space. #[inline] pub fn up(&self) -> Vec3 { self.rotation * Vec3::Y } /// The local right direction (`+X`) rotated into this transform's space. #[inline] pub fn right(&self) -> Vec3 { self.rotation * Vec3::X } /// Builds a transform positioned at `eye` looking toward `target`. /// /// `up` is the reference up vector. Returns the identity rotation if `eye` /// and `target` coincide. pub fn looking_at(eye: Vec3, target: Vec3, up: Vec3) -> Transform { let forward = target - eye; let rotation = if forward.length_squared() <= f32::EPSILON { Quat::IDENTITY } else { // glam's look_to is right-handed with -Z forward; invert the view // rotation to get an object-space orientation. Quat::from_mat4(&Mat4::look_to_rh(eye, forward.normalize(), up)).inverse() }; Transform { translation: eye, rotation, scale: Vec3::ONE, } } /// Returns `true` if every component is finite (no NaN/inf). #[inline] pub fn is_finite(&self) -> bool { self.translation.is_finite() && self.rotation.is_finite() && self.scale.is_finite() } } /// Returns `true` if all three components of `scale` are equal. #[inline] fn is_uniform(scale: Vec3) -> bool { (scale.x - scale.y).abs() <= f32::EPSILON && (scale.y - scale.z).abs() <= f32::EPSILON } #[cfg(test)] mod tests { use super::*; use std::f32::consts::{FRAC_PI_2, PI}; const EPS: f32 = 1e-4; fn approx_vec(a: Vec3, b: Vec3) -> bool { (a - b).length() <= EPS } #[test] fn identity_is_default() { assert_eq!(Transform::default(), Transform::IDENTITY); let p = Vec3::new(1.0, 2.0, 3.0); assert_eq!(Transform::IDENTITY.transform_point(p), p); } #[test] fn translation_moves_points() { let t = Transform::from_translation(Vec3::new(1.0, 2.0, 3.0)); assert!(approx_vec( t.transform_point(Vec3::ZERO), Vec3::new(1.0, 2.0, 3.0) )); // Vectors ignore translation. assert!(approx_vec(t.transform_vector(Vec3::X), Vec3::X)); } #[test] fn rotation_rotates_points() { let t = Transform::from_rotation(Quat::from_rotation_z(FRAC_PI_2)); assert!(approx_vec(t.transform_point(Vec3::X), Vec3::Y)); } #[test] fn scale_scales_points() { let t = Transform::from_scale(Vec3::new(2.0, 3.0, 4.0)); assert!(approx_vec( t.transform_point(Vec3::ONE), Vec3::new(2.0, 3.0, 4.0) )); } #[test] fn matrix_round_trip() { let t = Transform::from_trs( Vec3::new(5.0, -2.0, 1.0), Quat::from_euler(glam::EulerRot::XYZ, 0.3, -0.7, 1.1), Vec3::new(2.0, 2.0, 2.0), ); let back = Transform::from_matrix(t.to_matrix()); assert!(approx_vec(t.translation, back.translation)); assert!(approx_vec(t.scale, back.scale)); // Quaternions q and -q represent the same rotation. let dot = t.rotation.dot(back.rotation).abs(); assert!((dot - 1.0).abs() <= EPS, "rotation mismatch: dot={dot}"); } #[test] fn inverse_cancels() { let t = Transform::from_trs( Vec3::new(3.0, 4.0, 5.0), Quat::from_rotation_y(0.9), Vec3::splat(2.0), ); let id = t.mul_transform(&t.inverse()); assert!(approx_vec(id.translation, Vec3::ZERO)); assert!(approx_vec(id.scale, Vec3::ONE)); assert!(approx_vec( id.transform_point(Vec3::new(7.0, 8.0, 9.0)), Vec3::new(7.0, 8.0, 9.0) )); } #[test] fn composition_matches_matrix() { let a = Transform::from_trs( Vec3::new(1.0, 0.0, -2.0), Quat::from_rotation_x(0.4), Vec3::splat(1.5), ); let b = Transform::from_trs( Vec3::new(-3.0, 2.0, 1.0), Quat::from_rotation_z(-0.8), Vec3::splat(0.5), ); let composed = a.mul_transform(&b); let p = Vec3::new(2.0, -1.0, 3.0); let via_transform = composed.transform_point(p); let via_matrix = (a.to_matrix() * b.to_matrix()).transform_point3(p); assert!(approx_vec(via_transform, via_matrix)); } #[test] fn nonuniform_composition_falls_back_to_matrix() { let a = Transform::from_trs( Vec3::new(0.0, 1.0, 0.0), Quat::from_rotation_z(FRAC_PI_2), Vec3::new(2.0, 1.0, 1.0), ); let b = Transform::from_trs( Vec3::new(1.0, 0.0, 0.0), Quat::IDENTITY, Vec3::new(1.0, 3.0, 1.0), ); let composed = a.mul_transform(&b); let p = Vec3::new(1.0, 2.0, -1.0); let via_transform = composed.transform_point(p); let via_matrix = (a.to_matrix() * b.to_matrix()).transform_point3(p); // Re-decomposition keeps this close even with non-uniform scale. assert!( approx_vec(via_transform, via_matrix), "{via_transform} vs {via_matrix}" ); } #[test] fn zero_scale_is_non_invertible() { let t = Transform::from_scale(Vec3::new(0.0, 1.0, 1.0)); let inv = t.inverse(); assert!(!inv.scale.x.is_finite()); assert!(t.is_finite()); // the forward transform itself is still finite } #[test] fn gimbal_lock_path_stays_stable() { // Pitch to +90° (a classic gimbal-lock orientation) and confirm the // basis vectors remain orthonormal after round-tripping through a matrix. let t = Transform::from_rotation(Quat::from_euler(glam::EulerRot::YXZ, 0.0, FRAC_PI_2, 0.0)); let back = Transform::from_matrix(t.to_matrix()); assert!(approx_vec(back.forward(), t.forward())); assert!(approx_vec(back.up(), t.up())); // Orthonormality. assert!(t.forward().dot(t.up()).abs() <= EPS); assert!(t.right().dot(t.up()).abs() <= EPS); } #[test] fn looking_at_faces_target() { let t = Transform::looking_at(Vec3::new(0.0, 0.0, 5.0), Vec3::ZERO, Vec3::Y); // Forward should point toward the target (-Z world direction). assert!(approx_vec(t.forward(), Vec3::NEG_Z)); } #[test] fn looking_at_degenerate_is_identity_rotation() { let t = Transform::looking_at(Vec3::ONE, Vec3::ONE, Vec3::Y); assert_eq!(t.rotation, Quat::IDENTITY); } #[test] fn basis_vectors_for_half_turn() { let t = Transform::from_rotation(Quat::from_rotation_y(PI)); assert!(approx_vec(t.forward(), Vec3::Z)); assert!(approx_vec(t.right(), Vec3::NEG_X)); } }