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