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