Skip to main content

nexcore_softrender/math/
transform.rs

1//! Transform factory functions
2//!
3//! Every visual operation is a matrix. Compose them via multiplication.
4//! M_final = M_last * ... * M_first (applied right to left).
5
6use super::mat::{Mat3, Mat4};
7
8// ============================================================================
9// 2D Transforms (Mat3)
10// ============================================================================
11
12/// 2D translation
13pub fn translate_2d(tx: f64, ty: f64) -> Mat3 {
14    Mat3::new([[1.0, 0.0, tx], [0.0, 1.0, ty], [0.0, 0.0, 1.0]])
15}
16
17/// 2D rotation (counter-clockwise, radians)
18pub fn rotate_2d(angle: f64) -> Mat3 {
19    let c = angle.cos();
20    let s = angle.sin();
21    Mat3::new([[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]])
22}
23
24/// 2D non-uniform scale
25pub fn scale_2d(sx: f64, sy: f64) -> Mat3 {
26    Mat3::new([[sx, 0.0, 0.0], [0.0, sy, 0.0], [0.0, 0.0, 1.0]])
27}
28
29/// 2D uniform scale
30pub fn scale_2d_uniform(s: f64) -> Mat3 {
31    scale_2d(s, s)
32}
33
34/// 2D shear
35pub fn shear_2d(shx: f64, shy: f64) -> Mat3 {
36    Mat3::new([[1.0, shx, 0.0], [shy, 1.0, 0.0], [0.0, 0.0, 1.0]])
37}
38
39/// Compose two 2D transforms: result = a * b (b applied first)
40pub fn compose_2d(a: &Mat3, b: &Mat3) -> Mat3 {
41    a.mul(b)
42}
43
44// ============================================================================
45// 3D Transforms (Mat4)
46// ============================================================================
47
48/// 3D translation
49pub fn translate_3d(tx: f64, ty: f64, tz: f64) -> Mat4 {
50    Mat4::new([
51        [1.0, 0.0, 0.0, tx],
52        [0.0, 1.0, 0.0, ty],
53        [0.0, 0.0, 1.0, tz],
54        [0.0, 0.0, 0.0, 1.0],
55    ])
56}
57
58/// 3D non-uniform scale
59pub fn scale_3d(sx: f64, sy: f64, sz: f64) -> Mat4 {
60    Mat4::new([
61        [sx, 0.0, 0.0, 0.0],
62        [0.0, sy, 0.0, 0.0],
63        [0.0, 0.0, sz, 0.0],
64        [0.0, 0.0, 0.0, 1.0],
65    ])
66}
67
68/// Rotation around X axis
69pub fn rotate_x(angle: f64) -> Mat4 {
70    let c = angle.cos();
71    let s = angle.sin();
72    Mat4::new([
73        [1.0, 0.0, 0.0, 0.0],
74        [0.0, c, -s, 0.0],
75        [0.0, s, c, 0.0],
76        [0.0, 0.0, 0.0, 1.0],
77    ])
78}
79
80/// Rotation around Y axis
81pub fn rotate_y(angle: f64) -> Mat4 {
82    let c = angle.cos();
83    let s = angle.sin();
84    Mat4::new([
85        [c, 0.0, s, 0.0],
86        [0.0, 1.0, 0.0, 0.0],
87        [-s, 0.0, c, 0.0],
88        [0.0, 0.0, 0.0, 1.0],
89    ])
90}
91
92/// Rotation around Z axis
93pub fn rotate_z(angle: f64) -> Mat4 {
94    let c = angle.cos();
95    let s = angle.sin();
96    Mat4::new([
97        [c, -s, 0.0, 0.0],
98        [s, c, 0.0, 0.0],
99        [0.0, 0.0, 1.0, 0.0],
100        [0.0, 0.0, 0.0, 1.0],
101    ])
102}
103
104/// Orthographic projection: maps [l,r]×[b,t]×[n,f] → [-1,1]³
105pub fn ortho(left: f64, right: f64, bottom: f64, top: f64, near: f64, far: f64) -> Mat4 {
106    let rl = right - left;
107    let tb = top - bottom;
108    let fn_ = far - near;
109    Mat4::new([
110        [2.0 / rl, 0.0, 0.0, -(right + left) / rl],
111        [0.0, 2.0 / tb, 0.0, -(top + bottom) / tb],
112        [0.0, 0.0, -2.0 / fn_, -(far + near) / fn_],
113        [0.0, 0.0, 0.0, 1.0],
114    ])
115}
116
117/// Perspective projection (fov_y in radians, aspect = width/height)
118pub fn perspective(fov_y: f64, aspect: f64, near: f64, far: f64) -> Mat4 {
119    let f = 1.0 / (fov_y / 2.0).tan();
120    let nf = near - far;
121    Mat4::new([
122        [f / aspect, 0.0, 0.0, 0.0],
123        [0.0, f, 0.0, 0.0],
124        [0.0, 0.0, (far + near) / nf, 2.0 * far * near / nf],
125        [0.0, 0.0, -1.0, 0.0],
126    ])
127}
128
129/// Screen-space transform: NDC [-1,1]² → pixel [0,w)×[0,h)
130pub fn viewport(width: f64, height: f64) -> Mat3 {
131    let hw = width / 2.0;
132    let hh = height / 2.0;
133    Mat3::new([
134        [hw, 0.0, hw],
135        [0.0, -hh, hh], // flip Y: screen Y goes down
136        [0.0, 0.0, 1.0],
137    ])
138}
139
140/// Compose two 3D transforms: result = a * b (b applied first)
141pub fn compose_3d(a: &Mat4, b: &Mat4) -> Mat4 {
142    a.mul(b)
143}
144
145// ============================================================================
146// Tests
147// ============================================================================
148
149#[cfg(test)]
150mod tests {
151    use super::super::vec::Vec2;
152    use super::*;
153
154    #[test]
155    fn translate_2d_moves_point() {
156        let m = translate_2d(10.0, 20.0);
157        let p = m.transform_point(Vec2::new(1.0, 2.0));
158        assert!((p.x - 11.0).abs() < 1e-10);
159        assert!((p.y - 22.0).abs() < 1e-10);
160    }
161
162    #[test]
163    fn rotate_2d_90_degrees() {
164        let m = rotate_2d(core::f64::consts::FRAC_PI_2);
165        let p = m.transform_point(Vec2::new(1.0, 0.0));
166        assert!((p.x - 0.0).abs() < 1e-10);
167        assert!((p.y - 1.0).abs() < 1e-10);
168    }
169
170    #[test]
171    fn scale_2d_doubles() {
172        let m = scale_2d(2.0, 3.0);
173        let p = m.transform_point(Vec2::new(5.0, 7.0));
174        assert!((p.x - 10.0).abs() < 1e-10);
175        assert!((p.y - 21.0).abs() < 1e-10);
176    }
177
178    #[test]
179    fn compose_translate_then_rotate() {
180        // First translate (1,0), then rotate 90°
181        let t = translate_2d(1.0, 0.0);
182        let r = rotate_2d(core::f64::consts::FRAC_PI_2);
183        let m = compose_2d(&r, &t); // r(t(v))
184        let p = m.transform_point(Vec2::ZERO);
185        // (0,0) → translate → (1,0) → rotate 90° → (0,1)
186        assert!((p.x - 0.0).abs() < 1e-10);
187        assert!((p.y - 1.0).abs() < 1e-10);
188    }
189
190    #[test]
191    fn viewport_maps_ndc_to_pixels() {
192        let v = viewport(800.0, 600.0);
193        // NDC (0,0) → center of screen
194        let center = v.transform_point(Vec2::ZERO);
195        assert!((center.x - 400.0).abs() < 1e-10);
196        assert!((center.y - 300.0).abs() < 1e-10);
197        // NDC (-1, 1) → top-left (0, 0)
198        let tl = v.transform_point(Vec2::new(-1.0, 1.0));
199        assert!((tl.x - 0.0).abs() < 1e-10);
200        assert!((tl.y - 0.0).abs() < 1e-10);
201    }
202
203    #[test]
204    fn translate_3d_moves_point() {
205        let m = translate_3d(1.0, 2.0, 3.0);
206        let p = super::super::vec::Vec3::new(10.0, 20.0, 30.0);
207        let result = m.transform_point(p);
208        assert!((result.x - 11.0).abs() < 1e-10);
209        assert!((result.y - 22.0).abs() < 1e-10);
210        assert!((result.z - 33.0).abs() < 1e-10);
211    }
212
213    #[test]
214    fn rotate_z_90_degrees() {
215        let m = rotate_z(core::f64::consts::FRAC_PI_2);
216        let p = super::super::vec::Vec3::new(1.0, 0.0, 0.0);
217        let result = m.transform_point(p);
218        assert!((result.x - 0.0).abs() < 1e-10);
219        assert!((result.y - 1.0).abs() < 1e-10);
220    }
221}