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 std::vec::Vec;
26
27/// A triangle with its average depth for sorting
28#[derive(Debug, Clone)]
29pub struct DepthSortedTriangle {
30    pub primitive: DrawPrimitive,
31    pub avg_depth: f32,
32}
33
34impl DepthSortedTriangle {
35    /// Create a new depth-sorted triangle
36    pub fn new(primitive: DrawPrimitive, avg_depth: f32) -> Self {
37        Self {
38            primitive,
39            avg_depth,
40        }
41    }
42}
43
44impl K3dengine {
45    /// Render using Painter's Algorithm (back-to-front sorting, no Z-buffer)
46    ///
47    /// Collects all triangles, sorts them by depth, and renders back-to-front.
48    /// This eliminates the need for a Z-buffer, saving significant memory.
49    ///
50    /// # Arguments
51    /// * `meshes` - Iterator of meshes to render
52    /// * `triangles` - Buffer to store sorted triangles (must be large enough!)
53    /// * `callback` - Drawing callback for each primitive
54    ///
55    /// # Returns
56    /// Number of triangles rendered
57    pub fn render_painters_algorithm<'a, MS, F>(
58        &self,
59        meshes: MS,
60        triangles: &mut Vec<DepthSortedTriangle>,
61        mut callback: F,
62    ) -> usize
63    where
64        MS: IntoIterator<Item = &'a K3dMesh<'a>>,
65        F: FnMut(DrawPrimitive),
66    {
67        triangles.clear();
68
69        // Collect all triangles with their depths
70        for mesh in meshes {
71            if mesh.geometry.vertices.is_empty() {
72                continue;
73            }
74
75            // Frustum culling
76            if self.should_cull_mesh(mesh) {
77                continue;
78            }
79
80            // LOD Selection
81            let mesh_pos = mesh.get_position();
82            let distance = (mesh_pos - self.camera.position).norm();
83            let geometry = mesh.select_lod(distance);
84
85            let transform_matrix = self.camera.vp_matrix * mesh.model_matrix;
86
87            // Only collect renderable triangles (Solid modes)
88            match mesh.render_mode {
89                RenderMode::Solid
90                | RenderMode::SolidLightDir(_)
91                | RenderMode::BlinnPhong { .. } => {
92                    for face in geometry.faces {
93                        if let Some([p1, p2, p3]) =
94                            self.transform_points(face, geometry.vertices, transform_matrix)
95                        {
96                            // Backface culling
97                            let v1 = (p2.x - p1.x, p2.y - p1.y);
98                            let v2 = (p3.x - p1.x, p3.y - p1.y);
99                            let cross = v1.0 * v2.1 - v1.1 * v2.0;
100                            if cross <= 0 {
101                                continue;
102                            }
103
104                            // Calculate average depth for sorting
105                            let avg_depth = (p1.z + p2.z + p3.z) as f32 / 3.0;
106
107                            // Determine color based on render mode
108                            let color = match mesh.render_mode {
109                                RenderMode::SolidLightDir(light_dir) => self.calculate_lit_color(
110                                    face,
111                                    geometry.vertices,
112                                    geometry.normals,
113                                    mesh.color,
114                                    light_dir,
115                                ),
116                                RenderMode::BlinnPhong { .. } => {
117                                    // For Painter's Algorithm, fall back to simple lighting
118                                    // Full Blinn-Phong requires per-pixel depth
119                                    mesh.color
120                                }
121                                _ => mesh.color,
122                            };
123
124                            let primitive =
125                                DrawPrimitive::ColoredTriangle([p1.xy(), p2.xy(), p3.xy()], color);
126
127                            triangles.push(DepthSortedTriangle::new(primitive, avg_depth));
128                        }
129                    }
130                }
131                // Lines and Points don't need depth sorting
132                _ => {}
133            }
134        }
135
136        // Sort triangles by depth (back-to-front = largest depth first)
137        triangles.sort_by(|a, b| {
138            b.avg_depth
139                .partial_cmp(&a.avg_depth)
140                .unwrap_or(Ordering::Equal)
141        });
142
143        // Render sorted triangles
144        let count = triangles.len();
145        for triangle in triangles.iter() {
146            callback(triangle.primitive.clone());
147        }
148
149        count
150    }
151
152    /// Helper to calculate lit color for directional lighting
153    #[allow(dead_code)]
154    fn calculate_lit_color(
155        &self,
156        face: &[usize; 3],
157        vertices: &[[f32; 3]],
158        normals: &[[f32; 3]],
159        base_color: Rgb565,
160        light_dir: nalgebra::Vector3<f32>,
161    ) -> Rgb565 {
162        // Calculate face normal if not provided
163        let normal = if !normals.is_empty() && face[0] < normals.len() {
164            nalgebra::Vector3::new(
165                normals[face[0]][0],
166                normals[face[0]][1],
167                normals[face[0]][2],
168            )
169        } else {
170            // Compute face normal from vertices
171            let v0 = &vertices[face[0]];
172            let v1 = &vertices[face[1]];
173            let v2 = &vertices[face[2]];
174
175            let edge1 = nalgebra::Vector3::new(v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]);
176            let edge2 = nalgebra::Vector3::new(v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]);
177
178            let normal = edge1.cross(&edge2);
179            if normal.norm() > 0.0 {
180                normal.normalize()
181            } else {
182                nalgebra::Vector3::new(0.0, 1.0, 0.0)
183            }
184        };
185
186        // Simple diffuse lighting
187        let light_intensity = normal.dot(&light_dir.normalize()).max(0.0);
188        let ambient = 0.3;
189        let final_intensity = (ambient + (1.0 - ambient) * light_intensity).clamp(0.0, 1.0);
190
191        // Apply lighting to color
192        let r = (base_color.r() as f32 * final_intensity) as u8;
193        let g = (base_color.g() as f32 * final_intensity) as u8;
194        let b = (base_color.b() as f32 * final_intensity) as u8;
195
196        Rgb565::new(r, g, b)
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    extern crate std;
203    use super::*;
204    use embedded_graphics_core::pixelcolor::Rgb565;
205    use std::cmp::Ordering;
206
207    #[test]
208    fn test_sorting_by_depth() {
209        let mut triangles = std::vec![
210            DepthSortedTriangle {
211                primitive: DrawPrimitive::Line(
212                    [nalgebra::Point2::new(0, 0), nalgebra::Point2::new(1, 1)],
213                    Rgb565::new(31, 0, 0),
214                ),
215                avg_depth: 10.0,
216            },
217            DepthSortedTriangle {
218                primitive: DrawPrimitive::Line(
219                    [nalgebra::Point2::new(0, 0), nalgebra::Point2::new(1, 1)],
220                    Rgb565::new(0, 63, 0),
221                ),
222                avg_depth: 5.0,
223            },
224            DepthSortedTriangle {
225                primitive: DrawPrimitive::Line(
226                    [nalgebra::Point2::new(0, 0), nalgebra::Point2::new(1, 1)],
227                    Rgb565::new(0, 0, 31),
228                ),
229                avg_depth: 15.0,
230            },
231        ];
232
233        triangles.sort_by(|a, b| {
234            b.avg_depth
235                .partial_cmp(&a.avg_depth)
236                .unwrap_or(Ordering::Equal)
237        });
238
239        // Should be sorted furthest to nearest (15, 10, 5)
240        assert_eq!(triangles[0].avg_depth, 15.0);
241        assert_eq!(triangles[1].avg_depth, 10.0);
242        assert_eq!(triangles[2].avg_depth, 5.0);
243    }
244}