ling/gfx/depth.rs
1// src/gfx/depth.rs — deferred depth-sorted draw queue (painter's algorithm).
2//
3// All 3-D draw calls (`วาดสามเหลี่ยม3มิติ`, `วาดเส้น3มิติ`) push a `DrawCall`
4// into this queue instead of rasterising immediately. When `แสดงผล` / `present`
5// is called, the queue is sorted back-to-front by the depth tag and then
6// flushed into the pixel buffer.
7//
8// Painter's algorithm is exact for convex non-intersecting geometry and
9// produces plausible results for the Sierpiński fractal + tesseract wireframe.
10//
11// Each call also captures the current blend `mode` (0 normal · 1 add · 2 mul ·
12// 3 screen · 4 subtract · 5 overlay) and pen `alpha` so translucent 3-D FX
13// (sword slashes, ring trails, liquid orbs) composite over the scene instead of
14// painting opaque black where they fade out.
15
16// `raster` is wasm-safe (pure CPU); the software-framebuffer flush runs on web too.
17use crate::gfx::raster;
18
19/// Tagged draw call stored in the queue.
20#[derive(Debug, Clone)]
21pub struct DrawCall {
22 /// Camera-space z of the face/edge centroid — larger = further away.
23 pub depth: f32,
24 /// Pre-lit 0x00RRGGBB colour.
25 pub color: u32,
26 /// Blend mode (0 normal · 1 add · 2 multiply · 3 screen · 4 subtract · 5 overlay).
27 pub mode: u8,
28 /// Pen opacity 0..1 (coverage for the composite).
29 pub alpha: f32,
30 pub kind: DrawKind,
31}
32
33#[derive(Debug, Clone)]
34pub enum DrawKind {
35 Triangle {
36 x0: f32,
37 y0: f32,
38 z0: f32,
39 x1: f32,
40 y1: f32,
41 z1: f32,
42 x2: f32,
43 y2: f32,
44 z2: f32,
45 },
46 /// Gouraud-interpolated + per-pixel posterised triangle (smooth cel).
47 TriangleG {
48 x0: f32,
49 y0: f32,
50 z0: f32,
51 c0: u32,
52 x1: f32,
53 y1: f32,
54 z1: f32,
55 c1: u32,
56 x2: f32,
57 y2: f32,
58 z2: f32,
59 c2: u32,
60 bands: u32,
61 },
62 Line {
63 x0: f32,
64 y0: f32,
65 z0: f32,
66 x1: f32,
67 y1: f32,
68 z1: f32,
69 },
70}
71
72/// Deferred depth-sorted draw queue.
73#[derive(Debug)]
74pub struct DepthQueue {
75 calls: Vec<DrawCall>,
76 /// Current blend mode applied to subsequent pushes (mirrors `gfx.blend`).
77 cur_mode: u8,
78 /// Current pen alpha applied to subsequent pushes (mirrors `gfx.alpha`).
79 cur_alpha: f32,
80}
81
82impl Default for DepthQueue {
83 fn default() -> Self {
84 Self { calls: Vec::new(), cur_mode: 0, cur_alpha: 1.0 }
85 }
86}
87
88impl DepthQueue {
89 /// Mirror the live pen blend mode + alpha so the next pushes capture them.
90 /// Call after `std::mem::take` so an active blend survives a mid-frame flush.
91 pub fn set_state(&mut self, mode: u8, alpha: f32) {
92 self.cur_mode = mode;
93 self.cur_alpha = alpha.clamp(0.0, 1.0);
94 }
95
96 /// Queue a filled triangle (flat per-vertex depth = the sort key).
97 pub fn push_triangle(
98 &mut self,
99 depth: f32,
100 color: u32,
101 x0: f32,
102 y0: f32,
103 x1: f32,
104 y1: f32,
105 x2: f32,
106 y2: f32,
107 ) {
108 self.calls.push(DrawCall {
109 depth,
110 color,
111 mode: self.cur_mode,
112 alpha: self.cur_alpha,
113 kind: DrawKind::Triangle { x0, y0, z0: depth, x1, y1, z1: depth, x2, y2, z2: depth },
114 });
115 }
116
117 /// Queue a filled triangle with true per-vertex camera-space depth, so the
118 /// per-pixel z-buffer can resolve interpenetration.
119 #[allow(clippy::too_many_arguments)]
120 pub fn push_triangle_zv(
121 &mut self,
122 color: u32,
123 x0: f32,
124 y0: f32,
125 z0: f32,
126 x1: f32,
127 y1: f32,
128 z1: f32,
129 x2: f32,
130 y2: f32,
131 z2: f32,
132 ) {
133 let depth = (z0 + z1 + z2) / 3.0;
134 self.calls.push(DrawCall {
135 depth,
136 color,
137 mode: self.cur_mode,
138 alpha: self.cur_alpha,
139 kind: DrawKind::Triangle { x0, y0, z0, x1, y1, z1, x2, y2, z2 },
140 });
141 }
142
143 /// Queue a Gouraud + posterised triangle (smooth cel), flat per-vertex depth.
144 #[allow(clippy::too_many_arguments)]
145 pub fn push_triangle_g(
146 &mut self,
147 depth: f32,
148 x0: f32,
149 y0: f32,
150 c0: u32,
151 x1: f32,
152 y1: f32,
153 c1: u32,
154 x2: f32,
155 y2: f32,
156 c2: u32,
157 bands: u32,
158 ) {
159 self.calls.push(DrawCall {
160 depth,
161 color: c0,
162 mode: self.cur_mode,
163 alpha: self.cur_alpha,
164 kind: DrawKind::TriangleG {
165 x0,
166 y0,
167 z0: depth,
168 c0,
169 x1,
170 y1,
171 z1: depth,
172 c1,
173 x2,
174 y2,
175 z2: depth,
176 c2,
177 bands,
178 },
179 });
180 }
181
182 /// Gouraud triangle with true per-vertex depth (for the z-buffer path).
183 #[allow(clippy::too_many_arguments)]
184 pub fn push_triangle_g_zv(
185 &mut self,
186 x0: f32,
187 y0: f32,
188 z0: f32,
189 c0: u32,
190 x1: f32,
191 y1: f32,
192 z1: f32,
193 c1: u32,
194 x2: f32,
195 y2: f32,
196 z2: f32,
197 c2: u32,
198 bands: u32,
199 ) {
200 let depth = (z0 + z1 + z2) / 3.0;
201 self.calls.push(DrawCall {
202 depth,
203 color: c0,
204 mode: self.cur_mode,
205 alpha: self.cur_alpha,
206 kind: DrawKind::TriangleG { x0, y0, z0, c0, x1, y1, z1, c1, x2, y2, z2, c2, bands },
207 });
208 }
209
210 /// Queue a line segment (flat per-vertex depth).
211 pub fn push_line(&mut self, depth: f32, color: u32, x0: f32, y0: f32, x1: f32, y1: f32) {
212 self.calls.push(DrawCall {
213 depth,
214 color,
215 mode: self.cur_mode,
216 alpha: self.cur_alpha,
217 kind: DrawKind::Line { x0, y0, z0: depth, x1, y1, z1: depth },
218 });
219 }
220
221 /// Sort back-to-front and rasterise everything into `buf`.
222 ///
223 /// `zbuf`: when `Some`, a per-pixel depth buffer (camera-space z, smaller =
224 /// nearer) is used so interpenetrating triangles resolve correctly — a true
225 /// z-buffer on top of the painter's sort. When `None`, pure painter's
226 /// algorithm (the default/legacy path).
227 ///
228 /// Opaque calls (mode 0, alpha ≈ 1) take the fast direct-write path; calls
229 /// with a blend mode or alpha < 1 composite via `composite_pixel`. In the
230 /// z-buffer path, translucent calls test depth but do not write it, so they
231 /// layer over the opaque scene (back-to-front sort handles their ordering).
232 ///
233 /// Consumes `self` — call site does `mem::take` to avoid borrow conflict.
234 pub fn flush(
235 mut self,
236 buf: &mut Vec<u32>,
237 zbuf: Option<&mut Vec<f32>>,
238 width: usize,
239 height: usize,
240 ) {
241 // Sort largest depth first (furthest → painted first, nearest on top).
242 // With a z-buffer the sort still helps transparency + reduces overdraw.
243 self.calls.sort_unstable_by(|a, b| {
244 b.depth
245 .partial_cmp(&a.depth)
246 .unwrap_or(std::cmp::Ordering::Equal)
247 });
248 match zbuf {
249 Some(z) => {
250 // Reset the depth buffer to "infinitely far" for this frame.
251 if z.len() != width * height {
252 z.clear();
253 z.resize(width * height, f32::INFINITY);
254 } else {
255 for v in z.iter_mut() {
256 *v = f32::INFINITY;
257 }
258 }
259 for call in &self.calls {
260 let blended = call.mode != 0 || call.alpha < 0.999;
261 match call.kind {
262 DrawKind::Triangle { x0, y0, z0, x1, y1, z1, x2, y2, z2 } => {
263 if blended {
264 raster::fill_triangle_z_blend(
265 buf, z, width, height, call.color, call.mode, call.alpha, x0,
266 y0, z0, x1, y1, z1, x2, y2, z2,
267 );
268 } else {
269 raster::fill_triangle_z(
270 buf, z, width, height, call.color, x0, y0, z0, x1, y1, z1, x2,
271 y2, z2,
272 );
273 }
274 },
275 DrawKind::TriangleG {
276 x0,
277 y0,
278 z0,
279 c0,
280 x1,
281 y1,
282 z1,
283 c1,
284 x2,
285 y2,
286 z2,
287 c2,
288 bands,
289 } => raster::fill_triangle_gouraud_z(
290 buf, z, width, height, x0, y0, z0, c0, x1, y1, z1, c1, x2, y2, z2, c2,
291 bands,
292 ),
293 DrawKind::Line { x0, y0, x1, y1, .. } => {
294 if blended {
295 raster::draw_line_blend(
296 buf, width, height, call.color, call.mode, call.alpha, x0, y0,
297 x1, y1,
298 );
299 } else {
300 raster::draw_line(buf, width, height, call.color, x0, y0, x1, y1);
301 }
302 },
303 }
304 }
305 },
306 None => {
307 for call in &self.calls {
308 let blended = call.mode != 0 || call.alpha < 0.999;
309 match call.kind {
310 DrawKind::Triangle { x0, y0, x1, y1, x2, y2, .. } => {
311 if blended {
312 raster::fill_triangle_blend(
313 buf, width, height, call.color, call.mode, call.alpha, x0, y0,
314 x1, y1, x2, y2,
315 );
316 } else {
317 raster::fill_triangle(
318 buf, width, height, call.color, x0, y0, x1, y1, x2, y2,
319 );
320 }
321 },
322 DrawKind::TriangleG {
323 x0, y0, c0, x1, y1, c1, x2, y2, c2, bands, ..
324 } => raster::fill_triangle_gouraud(
325 buf, width, height, x0, y0, c0, x1, y1, c1, x2, y2, c2, bands,
326 ),
327 DrawKind::Line { x0, y0, x1, y1, .. } => {
328 if blended {
329 raster::draw_line_blend(
330 buf, width, height, call.color, call.mode, call.alpha, x0, y0,
331 x1, y1,
332 );
333 } else {
334 raster::draw_line(buf, width, height, call.color, x0, y0, x1, y1);
335 }
336 },
337 }
338 }
339 },
340 }
341 // `self` dropped here — no need to clear explicitly
342 }
343
344 pub fn is_empty(&self) -> bool {
345 self.calls.is_empty()
346 }
347
348 /// Consume the queue and send all draw calls to the WebGL backend.
349 /// Only compiled for wasm32 targets.
350 #[cfg(target_arch = "wasm32")]
351 pub fn flush_to_webgl(
352 mut self,
353 fill_r: f32,
354 fill_g: f32,
355 fill_b: f32,
356 width: usize,
357 height: usize,
358 ) {
359 // Sort back-to-front (painter's algorithm) — same as the native path.
360 self.calls.sort_unstable_by(|a, b| {
361 b.depth
362 .partial_cmp(&a.depth)
363 .unwrap_or(std::cmp::Ordering::Equal)
364 });
365 for call in &self.calls {
366 match call.kind {
367 DrawKind::Triangle { x0, y0, x1, y1, x2, y2, .. } => {
368 crate::gfx::webgl::push_triangle(call.color, x0, y0, x1, y1, x2, y2, call.depth)
369 },
370 DrawKind::TriangleG { x0, y0, c0, x1, y1, c1, x2, y2, c2, .. } => {
371 // WebGL path: approximate with the averaged vertex colour.
372 let avg = {
373 let r = ((c0 >> 16 & 0xFF) + (c1 >> 16 & 0xFF) + (c2 >> 16 & 0xFF)) / 3;
374 let g = ((c0 >> 8 & 0xFF) + (c1 >> 8 & 0xFF) + (c2 >> 8 & 0xFF)) / 3;
375 let b = ((c0 & 0xFF) + (c1 & 0xFF) + (c2 & 0xFF)) / 3;
376 (r << 16) | (g << 8) | b
377 };
378 crate::gfx::webgl::push_triangle(avg, x0, y0, x1, y1, x2, y2, call.depth);
379 },
380 DrawKind::Line { x0, y0, x1, y1, .. } => {
381 crate::gfx::webgl::push_line(call.color, x0, y0, x1, y1, call.depth)
382 },
383 }
384 }
385 crate::gfx::webgl::flush(fill_r, fill_g, fill_b, width, height);
386 }
387}