1use crate::layer::Sprite;
2
3#[derive(Debug, Clone, Copy)]
4pub struct ProjectedPoint {
5 pub x: f32,
6 pub y: f32,
7 pub depth: f32,
8 pub clip_w: f32,
13}
14
15#[derive(Debug, Clone, Copy)]
16pub struct SpriteQuadGeometry {
17 pub projected: [ProjectedPoint; 4],
18 pub world: [[f32; 3]; 4],
19}
20
21#[derive(Debug, Clone, Copy)]
22struct Vec3 {
23 x: f32,
24 y: f32,
25 z: f32,
26}
27
28impl Vec3 {
29 fn new(x: f32, y: f32, z: f32) -> Self {
30 Self { x, y, z }
31 }
32
33 fn sub(self, rhs: Self) -> Self {
34 Self::new(self.x - rhs.x, self.y - rhs.y, self.z - rhs.z)
35 }
36
37 fn add(self, rhs: Self) -> Self {
38 Self::new(self.x + rhs.x, self.y + rhs.y, self.z + rhs.z)
39 }
40
41 fn dot(self, rhs: Self) -> f32 {
42 self.x * rhs.x + self.y * rhs.y + self.z * rhs.z
43 }
44
45 fn cross(self, rhs: Self) -> Self {
46 Self::new(
47 self.y * rhs.z - self.z * rhs.y,
48 self.z * rhs.x - self.x * rhs.z,
49 self.x * rhs.y - self.y * rhs.x,
50 )
51 }
52
53 fn normalize(self) -> Self {
54 let len2 = self.dot(self);
55 if len2 <= f32::EPSILON {
56 return Self::new(0.0, 0.0, 0.0);
57 }
58 let inv = len2.sqrt().recip();
59 Self::new(self.x * inv, self.y * inv, self.z * inv)
60 }
61}
62
63fn rotate_x(v: Vec3, angle: f32) -> Vec3 {
64 let (s, c) = angle.sin_cos();
65 Vec3::new(v.x, v.y * c - v.z * s, v.y * s + v.z * c)
66}
67
68fn rotate_y(v: Vec3, angle: f32) -> Vec3 {
69 let (s, c) = angle.sin_cos();
70 Vec3::new(v.x * c + v.z * s, v.y, -v.x * s + v.z * c)
71}
72
73fn rotate_z(v: Vec3, angle: f32) -> Vec3 {
74 let (s, c) = angle.sin_cos();
75 Vec3::new(v.x * c - v.y * s, v.x * s + v.y * c, v.z)
76}
77
78fn uses_3d(sprite: &Sprite) -> bool {
79 sprite.billboard
80 || sprite.camera_enabled
81 || sprite.z.abs() > f32::EPSILON
82 || sprite.pivot_z.abs() > f32::EPSILON
83 || (sprite.scale_z - 1.0).abs() > 1e-6
84 || sprite.rotate_x.abs() > f32::EPSILON
85 || sprite.rotate_y.abs() > f32::EPSILON
86}
87
88fn transform_local_point(sprite: &Sprite, px: f32, py: f32, dst_x: f32, dst_y: f32) -> Vec3 {
89 let (anchor_x, anchor_y, mut p) = if sprite.object_anchor {
90 (
91 dst_x,
92 dst_y,
93 Vec3::new(
94 px - sprite.texture_center_x - sprite.pivot_x,
95 py - sprite.texture_center_y - sprite.pivot_y,
96 -sprite.pivot_z,
97 ),
98 )
99 } else {
100 (
101 dst_x + sprite.pivot_x,
102 dst_y + sprite.pivot_y,
103 Vec3::new(px - sprite.pivot_x, py - sprite.pivot_y, -sprite.pivot_z),
104 )
105 };
106 p.x *= sprite.scale_x;
107 p.y *= sprite.scale_y;
108 p.z *= sprite.scale_z;
109 p = rotate_z(p, sprite.rotate);
112 p = rotate_x(p, sprite.rotate_x);
113 p = rotate_y(p, sprite.rotate_y);
114 p.add(Vec3::new(anchor_x, anchor_y, sprite.z + sprite.pivot_z))
115}
116
117fn camera_basis(sprite: &Sprite) -> (Vec3, Vec3, Vec3, Vec3) {
118 let eye = Vec3::new(
119 sprite.camera_eye[0],
120 sprite.camera_eye[1],
121 sprite.camera_eye[2],
122 );
123 let target = Vec3::new(
124 sprite.camera_target[0],
125 sprite.camera_target[1],
126 sprite.camera_target[2],
127 );
128 let up = Vec3::new(
129 sprite.camera_up[0],
130 sprite.camera_up[1],
131 sprite.camera_up[2],
132 );
133 let forward = target.sub(eye).normalize();
134 let right = up.cross(forward).normalize();
135 let up2 = forward.cross(right).normalize();
136 (eye, forward, right, up2)
137}
138
139fn transform_billboard_point(sprite: &Sprite, px: f32, py: f32, dst_x: f32, dst_y: f32) -> Vec3 {
140 let (_, _, right, up) = camera_basis(sprite);
141 let lx = (px - sprite.pivot_x) * sprite.scale_x;
142 let ly = -(py - sprite.pivot_y) * sprite.scale_y;
144 let (s, c) = sprite.rotate.sin_cos();
145 let rx = lx * c - ly * s;
146 let ry = lx * s + ly * c;
147 let anchor = Vec3::new(
148 dst_x + sprite.pivot_x,
149 dst_y + sprite.pivot_y,
150 sprite.z + sprite.pivot_z,
151 );
152 anchor.add(Vec3::new(
153 right.x * rx + up.x * ry,
154 right.y * rx + up.y * ry,
155 right.z * rx + up.z * ry,
156 ))
157}
158
159fn project_point(sprite: &Sprite, p: Vec3, win_w: f32, win_h: f32) -> Option<ProjectedPoint> {
160 if !sprite.camera_enabled {
161 let depth = (0.5 - p.z / 100000.0).clamp(0.0, 1.0);
162 return Some(ProjectedPoint {
163 x: p.x,
164 y: p.y,
165 depth,
166 clip_w: 1.0,
167 });
168 }
169
170 let (eye, zaxis, xaxis, yaxis) = camera_basis(sprite);
171
172 let rel = p.sub(eye);
173 let cx = rel.dot(xaxis);
174 let cy = rel.dot(yaxis);
175 let cz = rel.dot(zaxis);
176 if cz <= 1e-3 {
177 return None;
178 }
179
180 let aspect = if win_h.abs() > f32::EPSILON {
181 win_w / win_h
182 } else {
183 1.0
184 };
185 let vfov = sprite
186 .camera_view_angle_deg
187 .to_radians()
188 .clamp(1e-3, std::f32::consts::PI - 1e-3);
189 let tan_half_v = (vfov * 0.5).tan().max(1e-3);
190 let tan_half_h = (tan_half_v * aspect.max(1e-3)).max(1e-3);
191
192 let x_ndc = cx / (cz * tan_half_h);
193 let y_ndc = cy / (cz * tan_half_v);
194
195 let sx = (x_ndc + 1.0) * 0.5 * win_w;
196 let sy = (1.0 - y_ndc) * 0.5 * win_h;
197 let near = 1.0;
198 let far = 10000.0;
199 let depth = (far / (far - near) - near * far / ((far - near) * cz)).clamp(0.0, 1.0);
200 Some(ProjectedPoint {
201 x: sx,
202 y: sy,
203 depth,
204 clip_w: cz,
205 })
206}
207
208fn signed_area(a: (f32, f32), b: (f32, f32), c: (f32, f32)) -> f32 {
209 (b.0 - a.0) * (c.1 - a.1) - (b.1 - a.1) * (c.0 - a.0)
210}
211
212pub fn sprite_quad_geometry_rect(
213 sprite: &Sprite,
214 dst_x: f32,
215 dst_y: f32,
216 local_left: f32,
217 local_top: f32,
218 local_right: f32,
219 local_bottom: f32,
220 win_w: f32,
221 win_h: f32,
222) -> Option<SpriteQuadGeometry> {
223 let xf = if sprite.billboard {
224 transform_billboard_point as fn(&Sprite, f32, f32, f32, f32) -> Vec3
225 } else {
226 transform_local_point as fn(&Sprite, f32, f32, f32, f32) -> Vec3
227 };
228
229 let w0 = xf(sprite, local_left, local_top, dst_x, dst_y);
230 let w1 = xf(sprite, local_right, local_top, dst_x, dst_y);
231 let w2 = xf(sprite, local_right, local_bottom, dst_x, dst_y);
232 let w3 = xf(sprite, local_left, local_bottom, dst_x, dst_y);
233 let world = [
234 [w0.x, w0.y, w0.z],
235 [w1.x, w1.y, w1.z],
236 [w2.x, w2.y, w2.z],
237 [w3.x, w3.y, w3.z],
238 ];
239
240 if !uses_3d(sprite) {
241 return Some(SpriteQuadGeometry {
242 projected: [
243 ProjectedPoint {
244 x: w0.x,
245 y: w0.y,
246 depth: 0.0,
247 clip_w: 1.0,
248 },
249 ProjectedPoint {
250 x: w1.x,
251 y: w1.y,
252 depth: 0.0,
253 clip_w: 1.0,
254 },
255 ProjectedPoint {
256 x: w2.x,
257 y: w2.y,
258 depth: 0.0,
259 clip_w: 1.0,
260 },
261 ProjectedPoint {
262 x: w3.x,
263 y: w3.y,
264 depth: 0.0,
265 clip_w: 1.0,
266 },
267 ],
268 world,
269 });
270 }
271
272 let p0 = project_point(sprite, w0, win_w, win_h)?;
273 let p1 = project_point(sprite, w1, win_w, win_h)?;
274 let p2 = project_point(sprite, w2, win_w, win_h)?;
275 let p3 = project_point(sprite, w3, win_w, win_h)?;
276
277 if sprite.culling && signed_area((p0.x, p0.y), (p1.x, p1.y), (p2.x, p2.y)) <= 0.0 {
278 return None;
279 }
280
281 Some(SpriteQuadGeometry {
282 projected: [p0, p1, p2, p3],
283 world,
284 })
285}
286
287pub fn sprite_quad_points_rect(
288 sprite: &Sprite,
289 dst_x: f32,
290 dst_y: f32,
291 local_left: f32,
292 local_top: f32,
293 local_right: f32,
294 local_bottom: f32,
295 win_w: f32,
296 win_h: f32,
297) -> Option<[ProjectedPoint; 4]> {
298 sprite_quad_geometry_rect(
299 sprite,
300 dst_x,
301 dst_y,
302 local_left,
303 local_top,
304 local_right,
305 local_bottom,
306 win_w,
307 win_h,
308 )
309 .map(|quad| quad.projected)
310}
311
312pub fn sprite_quad_points(
313 sprite: &Sprite,
314 dst_x: f32,
315 dst_y: f32,
316 dst_w: f32,
317 dst_h: f32,
318 win_w: f32,
319 win_h: f32,
320) -> Option<[ProjectedPoint; 4]> {
321 sprite_quad_points_rect(sprite, dst_x, dst_y, 0.0, 0.0, dst_w, dst_h, win_w, win_h)
322}
323
324pub fn project_model_point(
325 sprite: &Sprite,
326 local: [f32; 3],
327 anchor_x: f32,
328 anchor_y: f32,
329 win_w: f32,
330 win_h: f32,
331) -> Option<ProjectedPoint> {
332 let mut p = Vec3::new(
333 local[0] - sprite.pivot_x,
334 local[1] - sprite.pivot_y,
335 local[2] - sprite.pivot_z,
336 );
337 p.x *= sprite.scale_x;
338 p.y *= sprite.scale_y;
339 p.z *= sprite.scale_z;
340 p = rotate_z(p, sprite.rotate);
341 p = rotate_x(p, sprite.rotate_x);
342 p = rotate_y(p, sprite.rotate_y);
343 p = p.add(Vec3::new(
344 anchor_x + sprite.pivot_x,
345 anchor_y + sprite.pivot_y,
346 sprite.z + sprite.pivot_z,
347 ));
348 project_point(sprite, p, win_w, win_h)
349}
350
351#[cfg(test)]
352mod tests {
353 use super::{Vec3, project_point, rotate_x, rotate_y, rotate_z, sprite_quad_points};
354 use crate::layer::Sprite;
355
356 fn camera_sprite() -> Sprite {
357 Sprite {
358 camera_enabled: true,
359 camera_eye: [0.0, 0.0, 0.0],
360 camera_target: [0.0, 0.0, 1.0],
361 camera_up: [0.0, 1.0, 0.0],
362 camera_view_angle_deg: 60.0,
363 ..Default::default()
364 }
365 }
366
367 #[test]
368 fn yaw_pitch_roll_matches_d3dx_application_order() {
369 let quarter = std::f32::consts::FRAC_PI_2;
370 let mut point = Vec3::new(1.0, 2.0, 3.0);
371 point = rotate_z(point, quarter);
372 point = rotate_x(point, quarter);
373 point = rotate_y(point, quarter);
374 assert!((point.x - 1.0).abs() < 1e-5);
375 assert!((point.y + 3.0).abs() < 1e-5);
376 assert!((point.z - 2.0).abs() < 1e-5);
377 }
378
379 #[test]
380 fn billboard_texture_top_projects_above_its_bottom() {
381 let mut sprite = camera_sprite();
382 sprite.billboard = true;
383 sprite.z = 100.0;
384 let quad = sprite_quad_points(&sprite, 0.0, 0.0, 32.0, 64.0, 640.0, 480.0).unwrap();
385 assert!(quad[0].y < quad[3].y);
386 }
387
388 #[test]
389 fn world_view_angle_is_vertical_and_does_not_change_with_window_width() {
390 let sprite = camera_sprite();
391 let point = Vec3::new(0.0, 1.0, 10.0);
392 let narrow = project_point(&sprite, point, 640.0, 480.0).unwrap();
393 let wide = project_point(&sprite, point, 1280.0, 480.0).unwrap();
394 assert!((narrow.y - wide.y).abs() < 1e-4);
395 }
396
397 #[test]
398 fn world_depth_matches_d3d_lh_near_and_far_planes() {
399 let sprite = camera_sprite();
400 let near = project_point(&sprite, Vec3::new(0.0, 0.0, 1.0), 640.0, 480.0).unwrap();
401 let far = project_point(&sprite, Vec3::new(0.0, 0.0, 10000.0), 640.0, 480.0).unwrap();
402 assert!(near.depth.abs() < 1e-6);
403 assert!((far.depth - 1.0).abs() < 1e-6);
404 }
405}