Skip to main content

embedded_3dgfx/
mesh.rs

1use embedded_graphics_core::pixelcolor::{Rgb565, WebColors};
2use heapless::Vec;
3use heapless::index_set::FnvIndexSet;
4use log::error;
5use nalgebra::{Point3, Similarity3, UnitQuaternion, Vector3};
6
7#[cfg(not(feature = "std"))]
8use micromath::F32Ext;
9
10#[derive(Debug, PartialEq, Clone)]
11pub enum RenderMode {
12    Points,
13    Lines,
14    Solid,
15    SolidLightDir(Vector3<f32>),
16    BlinnPhong {
17        light_dir: Vector3<f32>,
18        specular_intensity: f32,
19        shininess: f32,
20    },
21    GouraudLightDir(Vector3<f32>),
22    /// Flat-shaded with a uniform brightness level (0=black, 255=full color).
23    /// Used for Doom-style sector-based lighting.
24    SectorBright(u8),
25}
26#[derive(Debug, Default, Copy, Clone)]
27pub struct Geometry<'a> {
28    pub vertices: &'a [[f32; 3]],
29    pub faces: &'a [[usize; 3]],
30    pub colors: &'a [Rgb565],
31    pub lines: &'a [[usize; 2]],
32    pub normals: &'a [[f32; 3]],
33    /// Per-vertex normals for smooth (Gouraud) shading.
34    /// If non-empty, must have the same length as `vertices`.
35    pub vertex_normals: &'a [[f32; 3]],
36    /// UV texture coordinates (one per vertex)
37    pub uvs: &'a [[f32; 2]],
38    /// Optional texture ID for this geometry
39    pub texture_id: Option<u32>,
40}
41
42impl Geometry<'_> {
43    fn check_validity(&self) -> bool {
44        if self.vertices.is_empty() {
45            error!("Vertices are empty");
46            return false;
47        }
48
49        for face in self.faces {
50            if face[0] >= self.vertices.len()
51                || face[1] >= self.vertices.len()
52                || face[2] >= self.vertices.len()
53            {
54                error!("Face vertices are out of bounds");
55                return false;
56            }
57        }
58
59        for line in self.lines {
60            if line[0] >= self.vertices.len() || line[1] >= self.vertices.len() {
61                error!("Line vertices are out of bounds");
62                return false;
63            }
64        }
65
66        if !self.colors.is_empty() && self.colors.len() != self.vertices.len() {
67            error!("Colors are not the same length as vertices");
68            return false;
69        }
70
71        if !self.uvs.is_empty() && self.uvs.len() != self.vertices.len() {
72            error!("UVs are not the same length as vertices");
73            return false;
74        }
75
76        if !self.vertex_normals.is_empty() && self.vertex_normals.len() != self.vertices.len() {
77            error!("Vertex normals are not the same length as vertices");
78            return false;
79        }
80
81        true
82    }
83
84    /// Converts faces to unique edge pairs for line rendering.
85    ///
86    /// # Type Parameters
87    /// * `N` - Maximum capacity for the edges buffer. For a closed mesh, a good estimate is
88    ///   `faces.len() * 3 / 2` since each edge is typically shared by 2 faces.
89    ///
90    /// # Returns
91    /// A heapless Vec containing unique edge pairs. If capacity is exceeded, returns
92    /// partial results with an error logged.
93    pub fn lines_from_faces<const N: usize>(faces: &[[usize; 3]]) -> Vec<(usize, usize), N> {
94        let mut set: FnvIndexSet<(usize, usize), N> = FnvIndexSet::new();
95        for face in faces {
96            for &(i1, i2) in &[(face[0], face[1]), (face[1], face[2]), (face[2], face[0])] {
97                let edge = if i1 < i2 { (i1, i2) } else { (i2, i1) };
98                if set.insert(edge).is_err() {
99                    error!(
100                        "lines_from_faces: heapless Vec capacity exceeded (max {}). Some edges will not be rendered.",
101                        N
102                    );
103                    break;
104                }
105            }
106        }
107        set.iter().copied().collect()
108    }
109}
110
111/// Level of Detail configuration for a mesh
112///
113/// Defines distance thresholds for switching between LOD levels:
114/// - 0 to high_distance: Use high detail geometry
115/// - high_distance to medium_distance: Use medium detail geometry
116/// - Beyond medium_distance: Use low detail geometry
117#[derive(Debug, Clone, Copy)]
118pub struct LODLevels {
119    /// Distance threshold for high detail (0 to this distance)
120    pub high_distance: f32,
121    /// Distance threshold for medium detail (high_distance to this distance)
122    pub medium_distance: f32,
123}
124
125impl Default for LODLevels {
126    fn default() -> Self {
127        Self {
128            high_distance: 50.0,
129            medium_distance: 100.0,
130        }
131    }
132}
133
134/// A mesh with optional Level of Detail (LOD) support
135pub struct K3dMesh<'a> {
136    pub similarity: Similarity3<f32>,
137    pub model_matrix: nalgebra::Matrix4<f32>,
138
139    pub color: Rgb565,
140    pub render_mode: RenderMode,
141    pub geometry: Geometry<'a>,
142
143    /// Optional LOD geometries (medium detail, low detail)
144    /// If None, only the main geometry is used
145    pub lod_medium: Option<Geometry<'a>>,
146    pub lod_low: Option<Geometry<'a>>,
147    pub lod_levels: LODLevels,
148    pub priority: u8,
149}
150
151impl<'a> K3dMesh<'a> {
152    pub fn new(geometry: Geometry) -> K3dMesh {
153        assert!(geometry.check_validity());
154        let sim = Similarity3::new(Vector3::new(0.0, 0.0, 0.0), nalgebra::zero(), 1.0);
155        K3dMesh {
156            model_matrix: sim.to_homogeneous(),
157            similarity: sim,
158            color: Rgb565::CSS_WHITE,
159            render_mode: RenderMode::Points,
160            geometry,
161            lod_medium: None,
162            lod_low: None,
163            lod_levels: LODLevels::default(),
164            priority: 128,
165        }
166    }
167
168    /// Set LOD geometries for this mesh
169    ///
170    /// # Arguments
171    /// * `medium` - Medium detail geometry (optional)
172    /// * `low` - Low detail geometry (optional)
173    /// * `levels` - Distance thresholds for switching LOD levels
174    pub fn set_lod<'b>(
175        &mut self,
176        medium: Option<Geometry<'b>>,
177        low: Option<Geometry<'b>>,
178        levels: LODLevels,
179    ) where
180        'b: 'a,
181    {
182        if let Some(ref geom) = medium {
183            assert!(geom.check_validity());
184        }
185        if let Some(ref geom) = low {
186            assert!(geom.check_validity());
187        }
188        self.lod_medium = medium;
189        self.lod_low = low;
190        self.lod_levels = levels;
191    }
192
193    /// Select the appropriate geometry based on distance from camera
194    ///
195    /// Returns a reference to the geometry that should be used for rendering
196    #[inline]
197    pub fn select_lod(&self, distance: f32) -> &Geometry<'_> {
198        if distance < self.lod_levels.high_distance {
199            // High detail
200            &self.geometry
201        } else if distance < self.lod_levels.medium_distance {
202            // Medium detail
203            self.lod_medium.as_ref().unwrap_or(&self.geometry)
204        } else {
205            // Low detail
206            self.lod_low
207                .as_ref()
208                .unwrap_or(self.lod_medium.as_ref().unwrap_or(&self.geometry))
209        }
210    }
211
212    pub fn set_color(&mut self, color: Rgb565) {
213        self.color = color;
214    }
215
216    pub fn set_render_mode(&mut self, mode: RenderMode) {
217        self.render_mode = mode;
218    }
219
220    pub fn set_priority(&mut self, priority: u8) {
221        self.priority = priority;
222    }
223
224    pub fn set_position(&mut self, x: f32, y: f32, z: f32) {
225        self.similarity.isometry.translation.x = x;
226        self.similarity.isometry.translation.y = y;
227        self.similarity.isometry.translation.z = z;
228        self.update_model_matrix();
229    }
230
231    pub fn get_position(&self) -> Point3<f32> {
232        self.similarity.isometry.translation.vector.into()
233    }
234
235    pub fn set_attitude(&mut self, roll: f32, pitch: f32, yaw: f32) {
236        self.similarity.isometry.rotation = UnitQuaternion::from_euler_angles(roll, pitch, yaw);
237        self.update_model_matrix();
238    }
239
240    /// Set orientation directly from a unit quaternion.
241    pub fn set_rotation(&mut self, rotation: UnitQuaternion<f32>) {
242        self.similarity.isometry.rotation = rotation;
243        self.update_model_matrix();
244    }
245
246    pub fn set_target(&mut self, target: Point3<f32>) {
247        let view = Similarity3::look_at_rh(
248            &self.similarity.isometry.translation.vector.into(),
249            &target,
250            &Vector3::y(),
251            1.0,
252        );
253
254        self.similarity = view;
255        self.model_matrix = self.similarity.to_homogeneous();
256    }
257
258    pub fn set_scale(&mut self, s: f32) {
259        if s == 0.0 {
260            return;
261        }
262        self.similarity.set_scaling(s);
263        self.update_model_matrix();
264    }
265
266    fn update_model_matrix(&mut self) {
267        self.model_matrix = self.similarity.to_homogeneous();
268    }
269
270    /// Compute the squared bounding sphere radius of the mesh in model space.
271    /// Returns squared radius to avoid expensive sqrt operation.
272    /// This is used for frustum culling.
273    #[inline]
274    pub fn compute_bounding_radius_sq(&self) -> f32 {
275        let mut max_dist_sq = 0.0f32;
276        for vertex in self.geometry.vertices {
277            let dist_sq = vertex[0] * vertex[0] + vertex[1] * vertex[1] + vertex[2] * vertex[2];
278            if dist_sq > max_dist_sq {
279                max_dist_sq = dist_sq;
280            }
281        }
282        let scale = self.similarity.scaling();
283        max_dist_sq * scale * scale
284    }
285}
286
287/// Compute per-vertex normals by averaging the face normals of all faces
288/// that share each vertex, then normalizing.
289///
290/// # Type Parameters
291/// * `V` - Maximum number of vertices (capacity of the returned Vec)
292///
293/// # Returns
294/// A heapless Vec with one normal per vertex. Vertices not referenced by any
295/// face get a zero normal.
296pub fn compute_vertex_normals<const V: usize>(
297    vertices: &[[f32; 3]],
298    faces: &[[usize; 3]],
299    face_normals: &[[f32; 3]],
300) -> Vec<[f32; 3], V> {
301    let mut normals = Vec::<[f32; 3], V>::new();
302    for _ in 0..vertices.len() {
303        if normals.push([0.0, 0.0, 0.0]).is_err() {
304            break;
305        }
306    }
307
308    for (face, fn_arr) in faces.iter().zip(face_normals.iter()) {
309        for &vi in face {
310            if vi < normals.len() {
311                normals[vi][0] += fn_arr[0];
312                normals[vi][1] += fn_arr[1];
313                normals[vi][2] += fn_arr[2];
314            }
315        }
316    }
317
318    for n in normals.iter_mut() {
319        let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
320        if len > 1e-10 {
321            n[0] /= len;
322            n[1] /= len;
323            n[2] /= len;
324        }
325    }
326
327    normals
328}
329
330#[cfg(test)]
331mod tests {
332    extern crate std;
333    use super::*;
334
335    #[test]
336    fn test_geometry_validation_valid() {
337        let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
338        let faces = [[0, 1, 2]];
339
340        let geometry = Geometry {
341            vertices: &vertices,
342            faces: &faces,
343            colors: &[],
344            lines: &[],
345            normals: &[],
346            vertex_normals: &[],
347            uvs: &[],
348            texture_id: None,
349        };
350
351        assert!(geometry.check_validity());
352    }
353
354    #[test]
355    #[should_panic]
356    fn test_geometry_validation_invalid_face_index() {
357        let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
358        let faces = [[0, 1, 5]]; // Index 5 is out of bounds
359
360        let geometry = Geometry {
361            vertices: &vertices,
362            faces: &faces,
363            colors: &[],
364            lines: &[],
365            normals: &[],
366            vertex_normals: &[],
367            uvs: &[],
368            texture_id: None,
369        };
370
371        // This should panic because we call assert! in K3dMesh::new
372        K3dMesh::new(geometry);
373    }
374
375    #[test]
376    #[should_panic]
377    fn test_geometry_validation_invalid_line_index() {
378        let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
379        let lines = [[0, 10]]; // Index 10 is out of bounds
380
381        let geometry = Geometry {
382            vertices: &vertices,
383            faces: &[],
384            colors: &[],
385            lines: &lines,
386            normals: &[],
387            vertex_normals: &[],
388            uvs: &[],
389            texture_id: None,
390        };
391
392        K3dMesh::new(geometry);
393    }
394
395    #[test]
396    #[should_panic]
397    fn test_geometry_validation_color_length_mismatch() {
398        let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
399        let colors = [Rgb565::CSS_RED]; // Only 1 color for 2 vertices
400
401        let geometry = Geometry {
402            vertices: &vertices,
403            faces: &[],
404            colors: &colors,
405            lines: &[],
406            normals: &[],
407            vertex_normals: &[],
408            uvs: &[],
409            texture_id: None,
410        };
411
412        K3dMesh::new(geometry);
413    }
414
415    #[test]
416    fn test_lines_from_faces_basic() {
417        let faces = [[0, 1, 2]];
418        let lines = Geometry::lines_from_faces::<16>(&faces);
419
420        // Triangle should produce 3 unique edges
421        assert_eq!(lines.len(), 3);
422
423        // Check that edges are unique and normalized (smaller index first)
424        let expected_edges = [(0, 1), (0, 2), (1, 2)];
425        for edge in expected_edges.iter() {
426            assert!(lines.contains(edge));
427        }
428    }
429
430    #[test]
431    fn test_lines_from_faces_shared_edges() {
432        let faces = [[0, 1, 2], [0, 2, 3]];
433        let lines = Geometry::lines_from_faces::<16>(&faces);
434
435        // Two triangles sharing edge (0,2) should produce 5 unique edges
436        assert_eq!(lines.len(), 5);
437    }
438
439    #[test]
440    fn test_lines_from_faces_capacity_limit() {
441        let faces = [[0, 1, 2], [3, 4, 5]];
442        // Small capacity that can't hold all 6 edges (needs at least 16 for IndexSet)
443        let lines = Geometry::lines_from_faces::<16>(&faces);
444
445        // Should contain all 6 edges since capacity is sufficient
446        assert_eq!(lines.len(), 6);
447    }
448
449    #[test]
450    fn test_mesh_creation() {
451        let vertices = [[0.0, 0.0, 0.0]];
452        let geometry = Geometry {
453            vertices: &vertices,
454            faces: &[],
455            colors: &[],
456            lines: &[],
457            normals: &[],
458            vertex_normals: &[],
459            uvs: &[],
460            texture_id: None,
461        };
462
463        let mesh = K3dMesh::new(geometry);
464        assert_eq!(mesh.color, Rgb565::CSS_WHITE);
465        assert_eq!(mesh.render_mode, RenderMode::Points);
466        assert_eq!(mesh.get_position(), Point3::new(0.0, 0.0, 0.0));
467    }
468
469    #[test]
470    fn test_mesh_set_color() {
471        let vertices = [[0.0, 0.0, 0.0]];
472        let geometry = Geometry {
473            vertices: &vertices,
474            faces: &[],
475            colors: &[],
476            lines: &[],
477            normals: &[],
478            vertex_normals: &[],
479            uvs: &[],
480            texture_id: None,
481        };
482
483        let mut mesh = K3dMesh::new(geometry);
484        mesh.set_color(Rgb565::CSS_RED);
485        assert_eq!(mesh.color, Rgb565::CSS_RED);
486    }
487
488    #[test]
489    fn test_mesh_set_position() {
490        let vertices = [[0.0, 0.0, 0.0]];
491        let geometry = Geometry {
492            vertices: &vertices,
493            faces: &[],
494            colors: &[],
495            lines: &[],
496            normals: &[],
497            vertex_normals: &[],
498            uvs: &[],
499            texture_id: None,
500        };
501
502        let mut mesh = K3dMesh::new(geometry);
503        mesh.set_position(5.0, 10.0, 15.0);
504        assert_eq!(mesh.get_position(), Point3::new(5.0, 10.0, 15.0));
505    }
506
507    #[test]
508    fn test_mesh_set_scale() {
509        let vertices = [[0.0, 0.0, 0.0]];
510        let geometry = Geometry {
511            vertices: &vertices,
512            faces: &[],
513            colors: &[],
514            lines: &[],
515            normals: &[],
516            vertex_normals: &[],
517            uvs: &[],
518            texture_id: None,
519        };
520
521        let mut mesh = K3dMesh::new(geometry);
522        mesh.set_scale(2.0);
523        assert!((mesh.similarity.scaling() - 2.0).abs() < 0.001);
524    }
525
526    #[test]
527    fn test_mesh_set_scale_zero_ignored() {
528        let vertices = [[0.0, 0.0, 0.0]];
529        let geometry = Geometry {
530            vertices: &vertices,
531            faces: &[],
532            colors: &[],
533            lines: &[],
534            normals: &[],
535            vertex_normals: &[],
536            uvs: &[],
537            texture_id: None,
538        };
539
540        let mut mesh = K3dMesh::new(geometry);
541        let original_scale = mesh.similarity.scaling();
542        mesh.set_scale(0.0);
543        // Scale should remain unchanged
544        assert_eq!(mesh.similarity.scaling(), original_scale);
545    }
546
547    #[test]
548    fn test_mesh_set_attitude() {
549        let vertices = [[0.0, 0.0, 0.0]];
550        let geometry = Geometry {
551            vertices: &vertices,
552            faces: &[],
553            colors: &[],
554            lines: &[],
555            normals: &[],
556            vertex_normals: &[],
557            uvs: &[],
558            texture_id: None,
559        };
560
561        let mut mesh = K3dMesh::new(geometry);
562        mesh.set_attitude(0.1, 0.2, 0.3);
563        // Just verify it doesn't panic and updates the matrix
564        assert_ne!(mesh.model_matrix, nalgebra::Matrix4::identity());
565    }
566
567    #[test]
568    fn test_mesh_set_target() {
569        let vertices = [[0.0, 0.0, 0.0]];
570        let geometry = Geometry {
571            vertices: &vertices,
572            faces: &[],
573            colors: &[],
574            lines: &[],
575            normals: &[],
576            vertex_normals: &[],
577            uvs: &[],
578            texture_id: None,
579        };
580
581        let mut mesh = K3dMesh::new(geometry);
582        mesh.set_position(5.0, 5.0, 5.0);
583        mesh.set_target(Point3::new(0.0, 0.0, 0.0));
584        // Mesh should now be oriented toward origin
585        // Just verify it doesn't panic
586        assert_ne!(mesh.model_matrix, nalgebra::Matrix4::identity());
587    }
588
589    #[test]
590    fn test_mesh_render_mode_changes() {
591        let vertices = [[0.0, 0.0, 0.0]];
592        let geometry = Geometry {
593            vertices: &vertices,
594            faces: &[],
595            colors: &[],
596            lines: &[],
597            normals: &[],
598            vertex_normals: &[],
599            uvs: &[],
600            texture_id: None,
601        };
602
603        let mut mesh = K3dMesh::new(geometry);
604
605        mesh.set_render_mode(RenderMode::Lines);
606        assert_eq!(mesh.render_mode, RenderMode::Lines);
607
608        mesh.set_render_mode(RenderMode::Solid);
609        assert_eq!(mesh.render_mode, RenderMode::Solid);
610
611        mesh.set_render_mode(RenderMode::SolidLightDir(Vector3::new(0.0, 1.0, 0.0)));
612        assert!(matches!(mesh.render_mode, RenderMode::SolidLightDir(_)));
613    }
614
615    #[test]
616    fn test_compute_vertex_normals_single_triangle() {
617        let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
618        let faces = [[0, 1, 2]];
619        let face_normals = [[0.0, 0.0, 1.0]];
620
621        let vn = compute_vertex_normals::<8>(&vertices, &faces, &face_normals);
622        assert_eq!(vn.len(), 3);
623        for n in vn.iter() {
624            assert!((n[0] - 0.0).abs() < 1e-5);
625            assert!((n[1] - 0.0).abs() < 1e-5);
626            assert!((n[2] - 1.0).abs() < 1e-5);
627        }
628    }
629
630    #[test]
631    fn test_compute_vertex_normals_shared_edge() {
632        // Two triangles sharing edge (0,1), with normals pointing in +Z and +Y
633        let vertices = [
634            [0.0, 0.0, 0.0],
635            [1.0, 0.0, 0.0],
636            [0.5, 0.0, 1.0],
637            [0.5, 1.0, 0.0],
638        ];
639        let faces = [[0, 1, 2], [0, 1, 3]];
640        let face_normals = [[0.0, 0.0, 1.0], [0.0, 1.0, 0.0]];
641
642        let vn = compute_vertex_normals::<8>(&vertices, &faces, &face_normals);
643        assert_eq!(vn.len(), 4);
644
645        // Shared vertices 0 and 1 should have averaged normals
646        let _expected_len = (0.5f32 * 0.5 + 0.5 * 0.5).sqrt(); // ~0.707
647        for i in 0..2 {
648            let n = &vn[i];
649            let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
650            assert!((len - 1.0).abs() < 1e-5, "Normal should be unit length");
651            assert!(
652                (n[1] - n[2]).abs() < 1e-5,
653                "Y and Z components should be equal for shared verts"
654            );
655        }
656
657        // Vertex 2: only in face 0, should be [0,0,1]
658        assert!((vn[2][2] - 1.0).abs() < 1e-5);
659        // Vertex 3: only in face 1, should be [0,1,0]
660        assert!((vn[3][1] - 1.0).abs() < 1e-5);
661    }
662
663    #[test]
664    fn test_geometry_validation_vertex_normals_length_mismatch() {
665        let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
666        let vn = [[0.0, 0.0, 1.0]]; // Only 1 normal for 2 vertices
667
668        let geometry = Geometry {
669            vertices: &vertices,
670            faces: &[],
671            colors: &[],
672            lines: &[],
673            normals: &[],
674            vertex_normals: &vn,
675            uvs: &[],
676            texture_id: None,
677        };
678
679        assert!(!geometry.check_validity());
680    }
681}