ferravex_graphics/
mesh.rs

1use ferravex_math::{Color, Vec3, vec3};
2
3#[derive(Clone, Copy, Debug, PartialEq)]
4pub struct Vertex {
5    pub position: Vec3,
6    pub normal: Vec3,
7    pub color: Color,
8}
9impl Vertex {
10    pub const fn new(position: Vec3, normal: Vec3, color: Color) -> Self {
11        Self {
12            position,
13            normal,
14            color,
15        }
16    }
17}
18
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct Bounds {
21    pub min: Vec3,
22    pub max: Vec3,
23}
24impl Bounds {
25    pub fn center(self) -> Vec3 {
26        (self.min + self.max) * 0.5
27    }
28    pub fn half_extents(self) -> Vec3 {
29        (self.max - self.min) * 0.5
30    }
31}
32
33#[derive(Clone, Debug, PartialEq)]
34pub struct Mesh {
35    pub vertices: Vec<Vertex>,
36    pub indices: Vec<u32>,
37}
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum MeshError {
40    Empty,
41    InvalidTriangleCount,
42    IndexOutOfBounds,
43    NonFiniteVertex,
44    TooLarge,
45}
46impl std::fmt::Display for MeshError {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        write!(f, "invalid mesh: {self:?}")
49    }
50}
51impl std::error::Error for MeshError {}
52impl Mesh {
53    pub fn validate(&self) -> Result<Bounds, MeshError> {
54        if self.vertices.is_empty() || self.indices.is_empty() {
55            return Err(MeshError::Empty);
56        }
57        if self.vertices.len() > 1_000_000 || self.indices.len() > 6_000_000 {
58            return Err(MeshError::TooLarge);
59        }
60        if !self.indices.len().is_multiple_of(3) {
61            return Err(MeshError::InvalidTriangleCount);
62        }
63        let mut bounds = Bounds {
64            min: Vec3::INFINITY,
65            max: Vec3::NEG_INFINITY,
66        };
67        for v in &self.vertices {
68            if !v.position.is_finite() || !v.normal.is_finite() || !v.color.to_vec().is_finite() {
69                return Err(MeshError::NonFiniteVertex);
70            }
71            bounds.min = bounds.min.min(v.position);
72            bounds.max = bounds.max.max(v.position);
73        }
74        if self
75            .indices
76            .iter()
77            .any(|&i| i as usize >= self.vertices.len())
78        {
79            return Err(MeshError::IndexOutOfBounds);
80        }
81        Ok(bounds)
82    }
83    pub fn triangle_count(&self) -> usize {
84        self.indices.len() / 3
85    }
86    /// Each face has its own vertices, preserving hard low-poly normals.
87    pub fn cuboid(size: Vec3, color: Color) -> Self {
88        assert!(size.is_finite() && size.cmpgt(Vec3::ZERO).all());
89        let half = size * 0.5;
90        let mut mesh = Self {
91            vertices: Vec::with_capacity(24),
92            indices: Vec::with_capacity(36),
93        };
94        for (n, u, v) in [
95            (Vec3::X, Vec3::Y, Vec3::Z),
96            (Vec3::NEG_X, Vec3::Y, Vec3::NEG_Z),
97            (Vec3::Y, Vec3::Z, Vec3::X),
98            (Vec3::NEG_Y, Vec3::Z, Vec3::NEG_X),
99            (Vec3::Z, Vec3::X, Vec3::Y),
100            (Vec3::NEG_Z, Vec3::NEG_X, Vec3::Y),
101        ] {
102            let start = mesh.vertices.len() as u32;
103            for (a, b) in [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] {
104                mesh.vertices
105                    .push(Vertex::new((n + u * a + v * b) * half, n, color));
106            }
107            mesh.indices
108                .extend([start, start + 1, start + 2, start, start + 2, start + 3]);
109        }
110        mesh
111    }
112    /// A subdivided XZ floor with normal +Y.
113    pub fn plane(width: f32, depth: f32, subdivisions: u16, color: Color) -> Self {
114        assert!(width.is_finite() && depth.is_finite() && width > 0.0 && depth > 0.0);
115        let n = usize::from(subdivisions.clamp(1, 128));
116        let mut mesh = Self {
117            vertices: Vec::with_capacity((n + 1) * (n + 1)),
118            indices: Vec::with_capacity(n * n * 6),
119        };
120        for z in 0..=n {
121            for x in 0..=n {
122                mesh.vertices.push(Vertex::new(
123                    vec3(
124                        (x as f32 / n as f32 - 0.5) * width,
125                        0.0,
126                        (z as f32 / n as f32 - 0.5) * depth,
127                    ),
128                    Vec3::Y,
129                    color,
130                ));
131            }
132        }
133        for z in 0..n {
134            for x in 0..n {
135                let a = (z * (n + 1) + x) as u32;
136                let b = a + (n + 1) as u32;
137                mesh.indices.extend([a, b, b + 1, a, b + 1, a + 1]);
138            }
139        }
140        mesh
141    }
142    /// Faceted sphere. Duplicate per-triangle vertices keep normals flat.
143    pub fn sphere(radius: f32, rings: u16, sectors: u16, color: Color) -> Self {
144        assert!(radius.is_finite() && radius > 0.0);
145        let rings = rings.clamp(2, 64);
146        let sectors = sectors.clamp(3, 128);
147        let mut mesh = Self {
148            vertices: Vec::new(),
149            indices: Vec::new(),
150        };
151        let point = |ring: u16, sector: u16| {
152            let phi = std::f32::consts::PI * ring as f32 / rings as f32;
153            let theta = std::f32::consts::TAU * sector as f32 / sectors as f32;
154            vec3(phi.sin() * theta.cos(), phi.cos(), phi.sin() * theta.sin()) * radius
155        };
156        for ring in 0..rings {
157            for sector in 0..sectors {
158                let a = point(ring, sector);
159                let b = point(ring + 1, sector);
160                let c = point(ring + 1, sector + 1);
161                let d = point(ring, sector + 1);
162                if ring > 0 {
163                    mesh.push_outward_triangle(a, c, d, color);
164                }
165                if ring + 1 < rings {
166                    mesh.push_outward_triangle(a, b, c, color);
167                }
168            }
169        }
170        mesh
171    }
172    /// Faceted Y-axis cylinder, including closed caps.
173    pub fn cylinder(radius: f32, height: f32, sides: u16, color: Color) -> Self {
174        assert!(radius.is_finite() && height.is_finite() && radius > 0.0 && height > 0.0);
175        let sides = sides.clamp(3, 128);
176        let half = height * 0.5;
177        let mut mesh = Self {
178            vertices: Vec::new(),
179            indices: Vec::new(),
180        };
181        for i in 0..sides {
182            let a = std::f32::consts::TAU * i as f32 / sides as f32;
183            let b = std::f32::consts::TAU * (i + 1) as f32 / sides as f32;
184            let p = vec3(a.cos() * radius, -half, a.sin() * radius);
185            let q = vec3(b.cos() * radius, -half, b.sin() * radius);
186            let r = q + Vec3::Y * height;
187            let s = p + Vec3::Y * height;
188            mesh.push_outward_triangle(p, q, r, color);
189            mesh.push_outward_triangle(p, r, s, color);
190            mesh.push_outward_triangle(vec3(0.0, -half, 0.0), q, p, color);
191            mesh.push_outward_triangle(vec3(0.0, half, 0.0), s, r, color);
192        }
193        mesh
194    }
195    fn push_outward_triangle(&mut self, a: Vec3, mut b: Vec3, mut c: Vec3, color: Color) {
196        if (b - a).cross(c - a).dot(a + b + c) < 0.0 {
197            std::mem::swap(&mut b, &mut c);
198        }
199        let normal = (b - a).cross(c - a).normalize_or_zero();
200        let index = self.vertices.len() as u32;
201        for point in [a, b, c] {
202            self.vertices.push(Vertex::new(point, normal, color));
203        }
204        self.indices.extend([index, index + 1, index + 2]);
205    }
206    /// Bakes a caller-computed AO factor into RGB once. Alpha is preserved.
207    pub fn bake_vertex_ao(&mut self, mut occlusion: impl FnMut(Vec3, Vec3) -> f32) {
208        for vertex in &mut self.vertices {
209            let factor = occlusion(vertex.position, vertex.normal).clamp(0.0, 1.0);
210            let factor = if factor.is_finite() { factor } else { 1.0 };
211            vertex.color.r *= factor;
212            vertex.color.g *= factor;
213            vertex.color.b *= factor;
214        }
215    }
216}