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}
9
10#[derive(Debug, Clone, Copy)]
11struct Vec3 {
12 x: f32,
13 y: f32,
14 z: f32,
15}
16
17impl Vec3 {
18 fn new(x: f32, y: f32, z: f32) -> Self {
19 Self { x, y, z }
20 }
21
22 fn sub(self, rhs: Self) -> Self {
23 Self::new(self.x - rhs.x, self.y - rhs.y, self.z - rhs.z)
24 }
25
26 fn add(self, rhs: Self) -> Self {
27 Self::new(self.x + rhs.x, self.y + rhs.y, self.z + rhs.z)
28 }
29
30 fn dot(self, rhs: Self) -> f32 {
31 self.x * rhs.x + self.y * rhs.y + self.z * rhs.z
32 }
33
34 fn cross(self, rhs: Self) -> Self {
35 Self::new(
36 self.y * rhs.z - self.z * rhs.y,
37 self.z * rhs.x - self.x * rhs.z,
38 self.x * rhs.y - self.y * rhs.x,
39 )
40 }
41
42 fn normalize(self) -> Self {
43 let len2 = self.dot(self);
44 if len2 <= f32::EPSILON {
45 return Self::new(0.0, 0.0, 0.0);
46 }
47 let inv = len2.sqrt().recip();
48 Self::new(self.x * inv, self.y * inv, self.z * inv)
49 }
50}
51
52fn rotate_x(v: Vec3, angle: f32) -> Vec3 {
53 let (s, c) = angle.sin_cos();
54 Vec3::new(v.x, v.y * c - v.z * s, v.y * s + v.z * c)
55}
56
57fn rotate_y(v: Vec3, angle: f32) -> Vec3 {
58 let (s, c) = angle.sin_cos();
59 Vec3::new(v.x * c + v.z * s, v.y, -v.x * s + v.z * c)
60}
61
62fn rotate_z(v: Vec3, angle: f32) -> Vec3 {
63 let (s, c) = angle.sin_cos();
64 Vec3::new(v.x * c - v.y * s, v.x * s + v.y * c, v.z)
65}
66
67fn uses_3d(sprite: &Sprite) -> bool {
68 sprite.billboard
69 || sprite.camera_enabled
70 || sprite.z.abs() > f32::EPSILON
71 || sprite.pivot_z.abs() > f32::EPSILON
72 || (sprite.scale_z - 1.0).abs() > 1e-6
73 || sprite.rotate_x.abs() > f32::EPSILON
74 || sprite.rotate_y.abs() > f32::EPSILON
75}
76
77fn transform_local_point(sprite: &Sprite, px: f32, py: f32, dst_x: f32, dst_y: f32) -> Vec3 {
78 let (anchor_x, anchor_y, mut p) = if sprite.object_anchor {
79 (
80 dst_x,
81 dst_y,
82 Vec3::new(
83 px - sprite.texture_center_x - sprite.pivot_x,
84 py - sprite.texture_center_y - sprite.pivot_y,
85 -sprite.pivot_z,
86 ),
87 )
88 } else {
89 (
90 dst_x + sprite.pivot_x,
91 dst_y + sprite.pivot_y,
92 Vec3::new(px - sprite.pivot_x, py - sprite.pivot_y, -sprite.pivot_z),
93 )
94 };
95 p.x *= sprite.scale_x;
96 p.y *= sprite.scale_y;
97 p.z *= sprite.scale_z;
98 p = rotate_x(p, sprite.rotate_x);
99 p = rotate_y(p, sprite.rotate_y);
100 p = rotate_z(p, sprite.rotate);
101 p.add(Vec3::new(anchor_x, anchor_y, sprite.z + sprite.pivot_z))
102}
103
104fn camera_basis(sprite: &Sprite) -> (Vec3, Vec3, Vec3, Vec3) {
105 let eye = Vec3::new(
106 sprite.camera_eye[0],
107 sprite.camera_eye[1],
108 sprite.camera_eye[2],
109 );
110 let target = Vec3::new(
111 sprite.camera_target[0],
112 sprite.camera_target[1],
113 sprite.camera_target[2],
114 );
115 let up = Vec3::new(
116 sprite.camera_up[0],
117 sprite.camera_up[1],
118 sprite.camera_up[2],
119 );
120 let forward = target.sub(eye).normalize();
121 let right = up.cross(forward).normalize();
122 let up2 = forward.cross(right).normalize();
123 (eye, forward, right, up2)
124}
125
126fn transform_billboard_point(sprite: &Sprite, px: f32, py: f32, dst_x: f32, dst_y: f32) -> Vec3 {
127 let (_, _, right, up) = camera_basis(sprite);
128 let lx = (px - sprite.pivot_x) * sprite.scale_x;
129 let ly = (py - sprite.pivot_y) * sprite.scale_y;
130 let (s, c) = sprite.rotate.sin_cos();
131 let rx = lx * c - ly * s;
132 let ry = lx * s + ly * c;
133 let anchor = Vec3::new(
134 dst_x + sprite.pivot_x,
135 dst_y + sprite.pivot_y,
136 sprite.z + sprite.pivot_z,
137 );
138 anchor.add(Vec3::new(
139 right.x * rx + up.x * ry,
140 right.y * rx + up.y * ry,
141 right.z * rx + up.z * ry,
142 ))
143}
144
145fn project_point(sprite: &Sprite, p: Vec3, win_w: f32, win_h: f32) -> Option<ProjectedPoint> {
146 if !sprite.camera_enabled {
147 let depth = (0.5 - p.z / 100000.0).clamp(0.0, 1.0);
148 return Some(ProjectedPoint {
149 x: p.x,
150 y: p.y,
151 depth,
152 });
153 }
154
155 let (eye, zaxis, xaxis, yaxis) = camera_basis(sprite);
156
157 let rel = p.sub(eye);
158 let cx = rel.dot(xaxis);
159 let cy = rel.dot(yaxis);
160 let cz = rel.dot(zaxis);
161 if cz <= 1e-3 {
162 return None;
163 }
164
165 let aspect = if win_h.abs() > f32::EPSILON {
166 win_w / win_h
167 } else {
168 1.0
169 };
170 let hfov = sprite
171 .camera_view_angle_deg
172 .to_radians()
173 .clamp(1e-3, std::f32::consts::PI - 1e-3);
174 let tan_half_h = (hfov * 0.5).tan().max(1e-3);
175 let tan_half_v = (tan_half_h / aspect.max(1e-3)).max(1e-3);
176
177 let x_ndc = cx / (cz * tan_half_h);
178 let y_ndc = cy / (cz * tan_half_v);
179
180 let sx = (x_ndc + 1.0) * 0.5 * win_w;
181 let sy = (1.0 - y_ndc) * 0.5 * win_h;
182 let depth = ((cz - 1.0) / 20000.0).clamp(0.0, 1.0);
183 Some(ProjectedPoint {
184 x: sx,
185 y: sy,
186 depth,
187 })
188}
189
190fn signed_area(a: (f32, f32), b: (f32, f32), c: (f32, f32)) -> f32 {
191 (b.0 - a.0) * (c.1 - a.1) - (b.1 - a.1) * (c.0 - a.0)
192}
193
194pub fn sprite_quad_points(
195 sprite: &Sprite,
196 dst_x: f32,
197 dst_y: f32,
198 dst_w: f32,
199 dst_h: f32,
200 win_w: f32,
201 win_h: f32,
202) -> Option<[ProjectedPoint; 4]> {
203 if !uses_3d(sprite) {
204 let p0 = transform_local_point(sprite, 0.0, 0.0, dst_x, dst_y);
205 let p1 = transform_local_point(sprite, dst_w, 0.0, dst_x, dst_y);
206 let p2 = transform_local_point(sprite, dst_w, dst_h, dst_x, dst_y);
207 let p3 = transform_local_point(sprite, 0.0, dst_h, dst_x, dst_y);
208 return Some([
209 ProjectedPoint {
210 x: p0.x,
211 y: p0.y,
212 depth: 0.0,
213 },
214 ProjectedPoint {
215 x: p1.x,
216 y: p1.y,
217 depth: 0.0,
218 },
219 ProjectedPoint {
220 x: p2.x,
221 y: p2.y,
222 depth: 0.0,
223 },
224 ProjectedPoint {
225 x: p3.x,
226 y: p3.y,
227 depth: 0.0,
228 },
229 ]);
230 }
231
232 let xf = if sprite.billboard {
233 transform_billboard_point as fn(&Sprite, f32, f32, f32, f32) -> Vec3
234 } else {
235 transform_local_point as fn(&Sprite, f32, f32, f32, f32) -> Vec3
236 };
237
238 let p0 = project_point(sprite, xf(sprite, 0.0, 0.0, dst_x, dst_y), win_w, win_h)?;
239 let p1 = project_point(sprite, xf(sprite, dst_w, 0.0, dst_x, dst_y), win_w, win_h)?;
240 let p2 = project_point(sprite, xf(sprite, dst_w, dst_h, dst_x, dst_y), win_w, win_h)?;
241 let p3 = project_point(sprite, xf(sprite, 0.0, dst_h, dst_x, dst_y), win_w, win_h)?;
242
243 if sprite.culling && signed_area((p0.x, p0.y), (p1.x, p1.y), (p2.x, p2.y)) <= 0.0 {
244 return None;
245 }
246
247 Some([p0, p1, p2, p3])
248}
249
250pub fn project_model_point(
251 sprite: &Sprite,
252 local: [f32; 3],
253 anchor_x: f32,
254 anchor_y: f32,
255 win_w: f32,
256 win_h: f32,
257) -> Option<ProjectedPoint> {
258 let mut p = Vec3::new(
259 local[0] - sprite.pivot_x,
260 local[1] - sprite.pivot_y,
261 local[2] - sprite.pivot_z,
262 );
263 p.x *= sprite.scale_x;
264 p.y *= sprite.scale_y;
265 p.z *= sprite.scale_z;
266 p = rotate_x(p, sprite.rotate_x);
267 p = rotate_y(p, sprite.rotate_y);
268 p = rotate_z(p, sprite.rotate);
269 p = p.add(Vec3::new(
270 anchor_x + sprite.pivot_x,
271 anchor_y + sprite.pivot_y,
272 sprite.z + sprite.pivot_z,
273 ));
274 project_point(sprite, p, win_w, win_h)
275}