Skip to main content

embedded_3dgfx/
painters.rs

1//! Painter's Algorithm Implementation
2//!
3//! Renders triangles sorted by depth (back-to-front) without a Z-buffer.
4//! This trades sorting overhead for significant memory savings (no Z-buffer needed).
5//!
6//! Benefits:
7//! - Saves ~1.92MB RAM for 800x600 resolution (u32 Z-buffer)
8//! - Good for simple scenes with few overlapping triangles
9//! - O(n log n) sorting cost, acceptable when n is small
10//!
11//! Limitations:
12//! - Doesn't handle cyclic overlaps perfectly
13//! - Sorting cost increases with triangle count
14//! - Best for scenes with ~1000-5000 triangles
15//!
16//! Note: This module is only available when building with std (examples, tests)
17//! as it requires Vec for dynamic triangle collection.
18
19use crate::mesh::{K3dMesh, RenderMode};
20use crate::{DrawPrimitive, K3dengine};
21use core::cmp::Ordering;
22use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
23use nalgebra::{Vector3, Vector4};
24
25/// A triangle with its average depth for sorting
26#[derive(Debug, Clone)]
27pub struct DepthSortedTriangle {
28    pub primitive: DrawPrimitive,
29    pub avg_depth: f32,
30}
31
32impl DepthSortedTriangle {
33    pub const DUMMY: Self = Self {
34        primitive: DrawPrimitive::ColoredPoint(nalgebra::Point2::new(0, 0), Rgb565::BLACK),
35        avg_depth: 0.0,
36    };
37
38    /// Create a new depth-sorted triangle
39    pub fn new(primitive: DrawPrimitive, avg_depth: f32) -> Self {
40        Self {
41            primitive,
42            avg_depth,
43        }
44    }
45}
46
47impl Default for DepthSortedTriangle {
48    fn default() -> Self {
49        Self::DUMMY
50    }
51}
52
53impl K3dengine {
54    /// Render using Painter's Algorithm (back-to-front sorting, no Z-buffer)
55    ///
56    /// Collects all triangles, sorts them by depth, and renders back-to-front.
57    /// This eliminates the need for a Z-buffer, saving significant memory.
58    ///
59    /// # Arguments
60    /// * `meshes` - Iterator of meshes to render
61    /// * `triangles` - Buffer to store sorted triangles (must be large enough!)
62    /// * `callback` - Drawing callback for each primitive
63    ///
64    /// # Returns
65    /// Number of triangles rendered
66    pub fn render_painters_algorithm<'a, MS, F>(
67        &self,
68        meshes: MS,
69        triangles: &mut [DepthSortedTriangle],
70        mut callback: F,
71    ) -> usize
72    where
73        MS: IntoIterator<Item = &'a K3dMesh<'a>>,
74        F: FnMut(DrawPrimitive),
75    {
76        let mut count = 0;
77
78        // Collect all triangles with their depths
79        for mesh in meshes {
80            if mesh.geometry.vertices.is_empty() {
81                continue;
82            }
83
84            // Frustum culling
85            if self.should_cull_mesh(mesh) {
86                continue;
87            }
88
89            // LOD Selection
90            let mesh_pos = mesh.get_position();
91            let distance = (mesh_pos - self.camera.position).norm();
92            let geometry = mesh.select_lod(distance);
93
94            let transform_matrix = self.camera.vp_matrix * mesh.model_matrix;
95            let render_mode = self.resolve_render_mode(&mesh.render_mode);
96
97            // Only collect renderable triangles (solid-style modes)
98            match render_mode {
99                RenderMode::Solid
100                | RenderMode::SolidLightDir(_)
101                | RenderMode::BlinnPhong { .. }
102                | RenderMode::SectorBright(_) => {
103                    for (face_idx, face) in geometry.faces.iter().enumerate() {
104                        let v = geometry.vertices;
105                        if face[0] >= v.len() || face[1] >= v.len() || face[2] >= v.len() {
106                            continue;
107                        }
108
109                        let v0_world = mesh.model_matrix.transform_point(&nalgebra::Point3::new(
110                            v[face[0]][0],
111                            v[face[0]][1],
112                            v[face[0]][2],
113                        ));
114
115                        // Determine a usable world-space face normal.
116                        //
117                        // If explicit per-face normals are available, trust those and use
118                        // position-based backface culling.
119                        //
120                        // If normals are absent, keep faces (no backface cull). Many existing
121                        // demo meshes use mixed winding; forcing cull from inferred winding
122                        // makes Painter mode appear "corrupted" because visible faces are
123                        // incorrectly discarded.
124                        let mut normal_world_opt: Option<Vector3<f32>> = None;
125                        if !geometry.normals.is_empty() && face_idx < geometry.normals.len() {
126                            let n = geometry.normals[face_idx];
127                            let n_world = mesh
128                                .model_matrix
129                                .transform_vector(&Vector3::new(n[0], n[1], n[2]));
130                            if n_world.norm_squared() > 1e-8 {
131                                let n_world = n_world.normalize();
132                                if self.is_backface(
133                                    face,
134                                    geometry.vertices,
135                                    mesh.model_matrix,
136                                    &n_world,
137                                ) {
138                                    continue;
139                                }
140                                normal_world_opt = Some(n_world);
141                            }
142                        }
143
144                        // Fallback normal for flat lighting when explicit normals are absent.
145                        let v0 = Vector3::new(v[face[0]][0], v[face[0]][1], v[face[0]][2]);
146                        let v1 = Vector3::new(v[face[1]][0], v[face[1]][1], v[face[1]][2]);
147                        let v2 = Vector3::new(v[face[2]][0], v[face[2]][1], v[face[2]][2]);
148                        let normal_world = if let Some(n) = normal_world_opt {
149                            n
150                        } else {
151                            let normal_model = (v1 - v0).cross(&(v2 - v0));
152                            if normal_model.norm_squared() <= 1e-8 {
153                                continue;
154                            }
155                            let mut n = mesh
156                                .model_matrix
157                                .transform_vector(&normal_model)
158                                .normalize();
159                            // Flip inferred normal toward camera so flat directional lighting
160                            // remains visually stable for mixed-winding assets.
161                            if (self.camera.position - v0_world).dot(&n) < 0.0 {
162                                n = -n;
163                            }
164                            n
165                        };
166
167                        // Determine flat color based on render mode.
168                        let mut color = match render_mode {
169                            RenderMode::SolidLightDir(light_dir) => {
170                                let adjusted_dir =
171                                    Vector3::new(light_dir.x, light_dir.y, -light_dir.z)
172                                        .normalize();
173                                let intensity = normal_world.dot(&adjusted_dir).max(0.0);
174                                let ambient = 0.1;
175                                let final_intensity =
176                                    (ambient + (1.0 - ambient) * intensity).clamp(0.0, 1.0);
177                                Rgb565::new(
178                                    (mesh.color.r() as f32 * final_intensity) as u8,
179                                    (mesh.color.g() as f32 * final_intensity) as u8,
180                                    (mesh.color.b() as f32 * final_intensity) as u8,
181                                )
182                            }
183                            RenderMode::SectorBright(brightness) => {
184                                let wc = Self::face_world_center(
185                                    face,
186                                    geometry.vertices,
187                                    mesh.model_matrix,
188                                );
189                                self.sector_shaded_color(mesh.color, brightness, wc)
190                            }
191                            // Full per-pixel Blinn-Phong is not supported in painter mode.
192                            RenderMode::BlinnPhong { .. } | RenderMode::Solid => mesh.color,
193                            _ => mesh.color,
194                        };
195
196                        if !self.point_lights.is_empty() {
197                            let wc =
198                                Self::face_world_center(face, geometry.vertices, mesh.model_matrix);
199                            color = Self::add_tint(color, self.light_tint_at(wc));
200                        }
201
202                        let clip = [
203                            transform_matrix
204                                * Vector4::new(v[face[0]][0], v[face[0]][1], v[face[0]][2], 1.0),
205                            transform_matrix
206                                * Vector4::new(v[face[1]][0], v[face[1]][1], v[face[1]][2], 1.0),
207                            transform_matrix
208                                * Vector4::new(v[face[2]][0], v[face[2]][1], v[face[2]][2], 1.0),
209                        ];
210                        // Use a face-coherent sort depth so all clipped fan pieces of the same
211                        // source polygon remain locked together during rotation.
212                        let face_sort_depth = {
213                            let d = (clip[0].w + clip[1].w + clip[2].w) / 3.0;
214                            if d.is_finite() { d } else { 0.0 }
215                        };
216
217                        self.emit_clipped(clip, color, &mut |prim| {
218                            if let DrawPrimitive::ColoredTriangleWithDepth {
219                                points,
220                                depths: _,
221                                color,
222                            } = prim
223                            {
224                                if count < triangles.len() {
225                                    triangles[count] = DepthSortedTriangle::new(
226                                        DrawPrimitive::ColoredTriangle(points, color),
227                                        face_sort_depth,
228                                    );
229                                    count += 1;
230                                }
231                            }
232                        });
233                    }
234                }
235                // Lines and Points don't need depth sorting
236                _ => {}
237            }
238        }
239
240        // Sort triangles by depth (back-to-front = largest depth first)
241        triangles[0..count].sort_unstable_by(|a, b| {
242            b.avg_depth
243                .partial_cmp(&a.avg_depth)
244                .unwrap_or(Ordering::Equal)
245        });
246
247        // Render sorted triangles
248        for triangle in triangles[0..count].iter() {
249            callback(triangle.primitive.clone());
250        }
251
252        count
253    }
254
255    /// Helper to calculate lit color for directional lighting
256    #[allow(dead_code)]
257    fn calculate_lit_color(
258        &self,
259        face: &[usize; 3],
260        vertices: &[[f32; 3]],
261        normals: &[[f32; 3]],
262        base_color: Rgb565,
263        light_dir: nalgebra::Vector3<f32>,
264    ) -> Rgb565 {
265        // Calculate face normal if not provided
266        let normal = if !normals.is_empty() && face[0] < normals.len() {
267            nalgebra::Vector3::new(
268                normals[face[0]][0],
269                normals[face[0]][1],
270                normals[face[0]][2],
271            )
272        } else {
273            // Compute face normal from vertices
274            let v0 = &vertices[face[0]];
275            let v1 = &vertices[face[1]];
276            let v2 = &vertices[face[2]];
277
278            let edge1 = nalgebra::Vector3::new(v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]);
279            let edge2 = nalgebra::Vector3::new(v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]);
280
281            let normal = edge1.cross(&edge2);
282            if normal.norm() > 0.0 {
283                normal.normalize()
284            } else {
285                nalgebra::Vector3::new(0.0, 1.0, 0.0)
286            }
287        };
288
289        // Simple diffuse lighting
290        let light_intensity = normal.dot(&light_dir.normalize()).max(0.0);
291        let ambient = 0.3;
292        let final_intensity = (ambient + (1.0 - ambient) * light_intensity).clamp(0.0, 1.0);
293
294        // Apply lighting to color
295        let r = (base_color.r() as f32 * final_intensity) as u8;
296        let g = (base_color.g() as f32 * final_intensity) as u8;
297        let b = (base_color.b() as f32 * final_intensity) as u8;
298
299        Rgb565::new(r, g, b)
300    }
301}
302
303#[cfg(test)]
304mod tests {
305
306    use super::*;
307    use embedded_graphics_core::pixelcolor::Rgb565;
308    use nalgebra::Point3;
309    use std::cmp::Ordering;
310
311    #[test]
312    fn test_sorting_by_depth() {
313        let mut triangles = std::vec![
314            DepthSortedTriangle {
315                primitive: DrawPrimitive::Line(
316                    [nalgebra::Point2::new(0, 0), nalgebra::Point2::new(1, 1)],
317                    Rgb565::new(31, 0, 0),
318                ),
319                avg_depth: 10.0,
320            },
321            DepthSortedTriangle {
322                primitive: DrawPrimitive::Line(
323                    [nalgebra::Point2::new(0, 0), nalgebra::Point2::new(1, 1)],
324                    Rgb565::new(0, 63, 0),
325                ),
326                avg_depth: 5.0,
327            },
328            DepthSortedTriangle {
329                primitive: DrawPrimitive::Line(
330                    [nalgebra::Point2::new(0, 0), nalgebra::Point2::new(1, 1)],
331                    Rgb565::new(0, 0, 31),
332                ),
333                avg_depth: 15.0,
334            },
335        ];
336
337        triangles.sort_by(|a, b| {
338            b.avg_depth
339                .partial_cmp(&a.avg_depth)
340                .unwrap_or(Ordering::Equal)
341        });
342
343        // Should be sorted furthest to nearest (15, 10, 5)
344        assert_eq!(triangles[0].avg_depth, 15.0);
345        assert_eq!(triangles[1].avg_depth, 10.0);
346        assert_eq!(triangles[2].avg_depth, 5.0);
347    }
348
349    #[test]
350    fn painters_algorithm_clips_partially_offscreen_triangle() {
351        let mut engine = K3dengine::new(320, 240);
352        engine.camera.set_position(Point3::new(0.0, 0.0, 5.0));
353        engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
354
355        let vertices = [
356            [-1.0f32, -1.0, 0.0],
357            [1.0f32, -1.0, 0.0],
358            [20.0f32, 2.0, 0.0], // intentionally far outside horizontal frustum
359        ];
360        let faces = [[0usize, 1usize, 2usize]];
361        let geometry = crate::mesh::Geometry {
362            vertices: &vertices,
363            faces: &faces,
364            colors: &[],
365            lines: &[],
366            normals: &[],
367            vertex_normals: &[],
368            uvs: &[],
369            texture_id: None,
370        };
371        let mut mesh = crate::mesh::K3dMesh::new(geometry);
372        mesh.set_render_mode(crate::mesh::RenderMode::Solid);
373        mesh.set_color(Rgb565::new(31, 0, 0));
374
375        let mut triangles = std::vec![DepthSortedTriangle::DUMMY; 256];
376        let count =
377            engine.render_painters_algorithm(std::iter::once(&mesh), &mut triangles, |_| {});
378
379        // Regression guard: painter mode must clip partially off-screen faces
380        // instead of dropping them entirely.
381        assert!(count > 0);
382    }
383
384    #[test]
385    fn painters_clipped_fan_pieces_share_sort_depth() {
386        let mut engine = K3dengine::new(320, 240);
387        engine.camera.set_position(Point3::new(0.0, 0.0, 5.0));
388        engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
389
390        let vertices = [
391            [-1.0f32, -1.0, 0.0],
392            [1.0f32, -1.0, 0.0],
393            [30.0f32, 2.0, 0.0], // heavily clipped, typically produces a fan
394        ];
395        let faces = [[0usize, 1usize, 2usize]];
396        let geometry = crate::mesh::Geometry {
397            vertices: &vertices,
398            faces: &faces,
399            colors: &[],
400            lines: &[],
401            normals: &[],
402            vertex_normals: &[],
403            uvs: &[],
404            texture_id: None,
405        };
406        let mut mesh = crate::mesh::K3dMesh::new(geometry);
407        mesh.set_render_mode(crate::mesh::RenderMode::Solid);
408        mesh.set_color(Rgb565::new(0, 63, 0));
409
410        let mut triangles = std::vec![DepthSortedTriangle::DUMMY; 256];
411        let count =
412            engine.render_painters_algorithm(std::iter::once(&mesh), &mut triangles, |_| {});
413        assert!(count > 0);
414        let first = triangles[0].avg_depth;
415        assert!(
416            triangles[0..count]
417                .iter()
418                .all(|t| (t.avg_depth - first).abs() < 1e-5)
419        );
420    }
421}