Skip to main content

uika_runtime/
ue_math.rs

1// UE math types without direct glam equivalents.
2// These are simple Rust structs with conversions to/from glam types where applicable.
3
4use glam::{DQuat, DVec2, DVec3, Vec4};
5
6// ---------------------------------------------------------------------------
7// Rotator (FRotator equivalent — pitch/yaw/roll in degrees)
8// ---------------------------------------------------------------------------
9
10#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct Rotator {
12    pub pitch: f64,
13    pub yaw: f64,
14    pub roll: f64,
15}
16
17impl Rotator {
18    pub const ZERO: Rotator = Rotator { pitch: 0.0, yaw: 0.0, roll: 0.0 };
19
20    pub fn new(pitch: f64, yaw: f64, roll: f64) -> Self {
21        Rotator { pitch, yaw, roll }
22    }
23}
24
25// UE uses intrinsic ZYX rotation order (Yaw → Pitch → Roll), angles in degrees.
26// UE axes: Pitch=Y, Yaw=Z, Roll=X.
27impl From<Rotator> for DQuat {
28    fn from(r: Rotator) -> DQuat {
29        let deg2rad = std::f64::consts::PI / 180.0;
30        let (sp, cp) = (r.pitch * 0.5 * deg2rad).sin_cos();
31        let (sy, cy) = (r.yaw * 0.5 * deg2rad).sin_cos();
32        let (sr, cr) = (r.roll * 0.5 * deg2rad).sin_cos();
33
34        // Standard ZYX: Quat = Qz(yaw) * Qy(pitch) * Qx(roll)
35        DQuat::from_xyzw(
36            cy * cp * sr - sy * sp * cr,
37            cy * sp * cr + sy * cp * sr,
38            sy * cp * cr - cy * sp * sr,
39            cy * cp * cr + sy * sp * sr,
40        )
41    }
42}
43
44impl From<DQuat> for Rotator {
45    fn from(q: DQuat) -> Rotator {
46        let rad2deg = 180.0 / std::f64::consts::PI;
47
48        // Extract Euler angles (UE convention: intrinsic ZYX → extrinsic XYZ)
49        let sinr_cosp = 2.0 * (q.w * q.x + q.y * q.z);
50        let cosr_cosp = 1.0 - 2.0 * (q.x * q.x + q.y * q.y);
51        let roll = sinr_cosp.atan2(cosr_cosp);
52
53        let sinp = 2.0 * (q.w * q.y - q.z * q.x);
54        let pitch = if sinp.abs() >= 1.0 {
55            std::f64::consts::FRAC_PI_2.copysign(sinp)
56        } else {
57            sinp.asin()
58        };
59
60        let siny_cosp = 2.0 * (q.w * q.z + q.x * q.y);
61        let cosy_cosp = 1.0 - 2.0 * (q.y * q.y + q.z * q.z);
62        let yaw = siny_cosp.atan2(cosy_cosp);
63
64        Rotator {
65            pitch: pitch * rad2deg,
66            yaw: yaw * rad2deg,
67            roll: roll * rad2deg,
68        }
69    }
70}
71
72// ---------------------------------------------------------------------------
73// Transform (FTransform equivalent)
74// ---------------------------------------------------------------------------
75
76#[derive(Clone, Copy, Debug, PartialEq)]
77pub struct Transform {
78    pub rotation: DQuat,
79    pub translation: DVec3,
80    pub scale: DVec3,
81}
82
83impl Transform {
84    pub const IDENTITY: Transform = Transform {
85        rotation: DQuat::IDENTITY,
86        translation: DVec3::ZERO,
87        scale: DVec3::ONE,
88    };
89
90    pub fn new(rotation: DQuat, translation: DVec3, scale: DVec3) -> Self {
91        Transform { rotation, translation, scale }
92    }
93
94    pub fn from_translation(translation: DVec3) -> Self {
95        Transform { translation, ..Self::IDENTITY }
96    }
97
98    pub fn from_rotation(rotation: DQuat) -> Self {
99        Transform { rotation, ..Self::IDENTITY }
100    }
101}
102
103// ---------------------------------------------------------------------------
104// Color types
105// ---------------------------------------------------------------------------
106
107/// Linear color (float RGBA, 0.0–1.0 range). Maps to FLinearColor.
108#[derive(Clone, Copy, Debug, PartialEq)]
109pub struct LinearColor {
110    pub r: f32,
111    pub g: f32,
112    pub b: f32,
113    pub a: f32,
114}
115
116impl LinearColor {
117    pub const BLACK: LinearColor = LinearColor { r: 0.0, g: 0.0, b: 0.0, a: 1.0 };
118    pub const WHITE: LinearColor = LinearColor { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
119    pub const RED: LinearColor = LinearColor { r: 1.0, g: 0.0, b: 0.0, a: 1.0 };
120    pub const GREEN: LinearColor = LinearColor { r: 0.0, g: 1.0, b: 0.0, a: 1.0 };
121    pub const BLUE: LinearColor = LinearColor { r: 0.0, g: 0.0, b: 1.0, a: 1.0 };
122
123    pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
124        LinearColor { r, g, b, a }
125    }
126}
127
128impl From<LinearColor> for Vec4 {
129    fn from(c: LinearColor) -> Vec4 {
130        Vec4::new(c.r, c.g, c.b, c.a)
131    }
132}
133
134impl From<Vec4> for LinearColor {
135    fn from(v: Vec4) -> LinearColor {
136        LinearColor { r: v.x, g: v.y, b: v.z, a: v.w }
137    }
138}
139
140/// 8-bit RGBA color. Maps to FColor (note: UE stores BGRA internally,
141/// conversions handle the reorder).
142#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
143pub struct Color {
144    pub r: u8,
145    pub g: u8,
146    pub b: u8,
147    pub a: u8,
148}
149
150impl Color {
151    pub const BLACK: Color = Color { r: 0, g: 0, b: 0, a: 255 };
152    pub const WHITE: Color = Color { r: 255, g: 255, b: 255, a: 255 };
153    pub const RED: Color = Color { r: 255, g: 0, b: 0, a: 255 };
154    pub const GREEN: Color = Color { r: 0, g: 255, b: 0, a: 255 };
155    pub const BLUE: Color = Color { r: 0, g: 0, b: 255, a: 255 };
156
157    pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
158        Color { r, g, b, a }
159    }
160}
161
162// ---------------------------------------------------------------------------
163// Geometry primitives
164// ---------------------------------------------------------------------------
165
166/// A plane defined by normal + distance from origin. Maps to FPlane.
167#[derive(Clone, Copy, Debug, PartialEq)]
168pub struct Plane {
169    pub normal: DVec3,
170    pub d: f64,
171}
172
173impl Plane {
174    pub fn new(normal: DVec3, d: f64) -> Self {
175        Plane { normal, d }
176    }
177}
178
179/// A ray defined by origin + direction. Maps to FRay.
180#[derive(Clone, Copy, Debug, PartialEq)]
181pub struct Ray {
182    pub origin: DVec3,
183    pub direction: DVec3,
184}
185
186impl Ray {
187    pub fn new(origin: DVec3, direction: DVec3) -> Self {
188        Ray { origin, direction }
189    }
190}
191
192/// A sphere defined by center + radius. Maps to FSphere.
193#[derive(Clone, Copy, Debug, PartialEq)]
194pub struct Sphere {
195    pub center: DVec3,
196    pub radius: f64,
197}
198
199impl Sphere {
200    pub fn new(center: DVec3, radius: f64) -> Self {
201        Sphere { center, radius }
202    }
203}
204
205// ---------------------------------------------------------------------------
206// Bounding volumes
207// ---------------------------------------------------------------------------
208
209/// Axis-aligned bounding box. Named `UeBox` to avoid conflict with Rust's `Box`.
210/// Maps to FBox.
211#[derive(Clone, Copy, Debug, PartialEq)]
212pub struct UeBox {
213    pub min: DVec3,
214    pub max: DVec3,
215}
216
217impl UeBox {
218    pub fn new(min: DVec3, max: DVec3) -> Self {
219        UeBox { min, max }
220    }
221}
222
223/// 2D axis-aligned bounding box. Maps to FBox2D.
224#[derive(Clone, Copy, Debug, PartialEq)]
225pub struct UeBox2d {
226    pub min: DVec2,
227    pub max: DVec2,
228}
229
230impl UeBox2d {
231    pub fn new(min: DVec2, max: DVec2) -> Self {
232        UeBox2d { min, max }
233    }
234}
235
236/// Combined box + sphere bounds. Maps to FBoxSphereBounds.
237#[derive(Clone, Copy, Debug, PartialEq)]
238pub struct BoxSphereBounds {
239    pub origin: DVec3,
240    pub box_extent: DVec3,
241    pub sphere_radius: f64,
242}
243
244impl BoxSphereBounds {
245    pub fn new(origin: DVec3, box_extent: DVec3, sphere_radius: f64) -> Self {
246        BoxSphereBounds { origin, box_extent, sphere_radius }
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn rotator_zero_to_quat_is_identity() {
256        let q: DQuat = Rotator::ZERO.into();
257        let diff = (q - DQuat::IDENTITY).length();
258        assert!(diff < 1e-10, "Expected identity quat, got {q:?}");
259    }
260
261    #[test]
262    fn rotator_roundtrip() {
263        let r = Rotator::new(30.0, 45.0, 60.0);
264        let q: DQuat = r.into();
265        let r2: Rotator = q.into();
266        assert!((r.pitch - r2.pitch).abs() < 1e-10);
267        assert!((r.yaw - r2.yaw).abs() < 1e-10);
268        assert!((r.roll - r2.roll).abs() < 1e-10);
269    }
270
271    #[test]
272    fn linear_color_vec4_roundtrip() {
273        let c = LinearColor::new(0.5, 0.3, 0.8, 1.0);
274        let v: Vec4 = c.into();
275        let c2: LinearColor = v.into();
276        assert_eq!(c, c2);
277    }
278}