Skip to main content

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}