Skip to main content

cranpose_ui_graphics/space/
mesh.rs

1use std::{
2    f32::consts::PI,
3    fmt,
4    sync::{OnceLock, atomic::Ordering},
5};
6
7use portable_atomic::AtomicU64;
8
9use super::math::{Mat4, Vec3};
10
11static NEXT_MESH_ID: AtomicU64 = AtomicU64::new(1);
12
13/// Why the parts given to [`Mesh::new`] do not make a mesh.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum MeshError {
16    /// The texture coordinates or the normals do not have one entry for
17    /// each position.
18    AttributeCountMismatch,
19    /// The number of indices is not a multiple of three.
20    IndicesNotTriangles,
21    /// An index names a vertex past the last one.
22    IndexOutOfRange,
23    /// The mesh has no triangle.
24    Empty,
25}
26
27impl fmt::Display for MeshError {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        f.write_str(match self {
30            Self::AttributeCountMismatch => {
31                "texture coordinates and normals need one entry for each position"
32            }
33            Self::IndicesNotTriangles => "the index count is not a multiple of three",
34            Self::IndexOutOfRange => "an index names a vertex past the last one",
35            Self::Empty => "the mesh has no triangle",
36        })
37    }
38}
39
40impl std::error::Error for MeshError {}
41
42/// Where a ray meets a mesh.
43#[derive(Clone, Copy, Debug, PartialEq)]
44pub struct MeshHit {
45    /// How far along the ray the hit is, in units of the ray's direction.
46    pub distance: f32,
47    /// The index of the triangle that the ray meets.
48    pub triangle: u32,
49    /// The texture coordinate at the hit.
50    pub uv: [f32; 2],
51}
52
53/// The point of a placed mesh nearest some point.
54#[derive(Clone, Copy, Debug, PartialEq)]
55pub struct MeshContact {
56    /// The nearest point.
57    pub point: Vec3,
58    /// How far it lies from the point asked about.
59    pub distance: f32,
60    /// The texture coordinate there.
61    pub uv: [f32; 2],
62}
63
64/// Triangles in 3D that a composable can be drawn on.
65///
66/// Each vertex has a position, a normal and a texture coordinate. A
67/// composable drawn on the mesh shows its content point `(x, y)` where the
68/// texture coordinate is `(x / width, y / height)`: `(0, 0)` is the content's
69/// top left corner and `(1, 1)` its bottom right.
70///
71/// ```
72/// use cranpose_ui_graphics::space::{Mesh, Vec3};
73///
74/// let sphere = Mesh::sphere(32, 16);
75/// // The middle of the texture faces the viewer.
76/// let front = sphere
77///     .point_at([0.5, 0.5])
78///     .expect("the sphere covers its texture");
79/// assert!((front - Vec3::new(0.0, 0.0, -1.0)).length() < 1e-3);
80/// ```
81///
82/// Two meshes are equal when they are the same mesh: a mesh does not change
83/// once made.
84pub struct Mesh {
85    id: u64,
86    positions: Vec<[f32; 3]>,
87    normals: Vec<[f32; 3]>,
88    uvs: Vec<[f32; 2]>,
89    indices: Vec<u32>,
90    texture_plane: OnceLock<Option<TexturePlane>>,
91    bounds: OnceLock<(Vec3, Vec3)>,
92}
93
94impl PartialEq for Mesh {
95    fn eq(&self, other: &Self) -> bool {
96        self.id == other.id
97    }
98}
99
100impl fmt::Debug for Mesh {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.debug_struct("Mesh")
103            .field("id", &self.id)
104            .field("vertices", &self.positions.len())
105            .field("triangles", &self.triangle_count())
106            .finish()
107    }
108}
109
110/// The plane that carries texture coordinates onto a mesh: `(u, v)` lies at
111/// `origin + u_axis * u + v_axis * v`. A flat mesh lies in it; a curved one
112/// touches it where the texture's middle lies.
113#[derive(Clone, Copy, Debug, PartialEq)]
114pub struct TexturePlane {
115    /// Where `(0, 0)` lies.
116    pub origin: Vec3,
117    /// The step from `(0, 0)` to `(1, 0)`.
118    pub u_axis: Vec3,
119    /// The step from `(0, 0)` to `(0, 1)`.
120    pub v_axis: Vec3,
121}
122
123impl Mesh {
124    /// A mesh from its vertices and the indices of its triangles' corners,
125    /// three to a triangle. Without `normals`, each vertex takes the mean
126    /// direction of the triangles around it.
127    pub fn new(
128        positions: Vec<[f32; 3]>,
129        uvs: Vec<[f32; 2]>,
130        normals: Option<Vec<[f32; 3]>>,
131        indices: Vec<u32>,
132    ) -> Result<Self, MeshError> {
133        if uvs.len() != positions.len()
134            || normals
135                .as_ref()
136                .is_some_and(|normals| normals.len() != positions.len())
137        {
138            return Err(MeshError::AttributeCountMismatch);
139        }
140        if !indices.len().is_multiple_of(3) {
141            return Err(MeshError::IndicesNotTriangles);
142        }
143        if indices.is_empty() {
144            return Err(MeshError::Empty);
145        }
146        if indices
147            .iter()
148            .any(|&index| index as usize >= positions.len())
149        {
150            return Err(MeshError::IndexOutOfRange);
151        }
152        let normals = normals.unwrap_or_else(|| smooth_normals(&positions, &indices));
153        Ok(Self::assembled(positions, uvs, normals, indices))
154    }
155
156    /// The unit square from `(0, 0, 0)` to `(1, 1, 0)`, facing the viewer,
157    /// with the texture coordinate equal to the position. Scaled by a
158    /// composable's size it shows the composable as it is laid out.
159    pub fn plane() -> Self {
160        let mut parts = MeshParts::default();
161        parts.quad(
162            Vec3::ZERO,
163            Vec3::new(1.0, 0.0, 0.0),
164            Vec3::new(0.0, 1.0, 0.0),
165        );
166        parts.into_mesh()
167    }
168
169    /// The unit cube from `(-0.5, -0.5, -0.5)` to `(0.5, 0.5, 0.5)`. Each
170    /// face shows the whole texture, upright when seen from outside; the top
171    /// and bottom faces have the texture's top toward the back.
172    pub fn cuboid() -> Self {
173        let mut parts = MeshParts::default();
174        let (x, y, z) = (
175            Vec3::new(1.0, 0.0, 0.0),
176            Vec3::new(0.0, 1.0, 0.0),
177            Vec3::new(0.0, 0.0, 1.0),
178        );
179        let h = 0.5;
180        parts.quad(Vec3::new(-h, -h, -h), x, y);
181        parts.quad(Vec3::new(h, -h, h), -x, y);
182        parts.quad(Vec3::new(-h, -h, h), -z, y);
183        parts.quad(Vec3::new(h, -h, -h), z, y);
184        parts.quad(Vec3::new(-h, -h, h), x, -z);
185        parts.quad(Vec3::new(-h, h, -h), x, z);
186        parts.into_mesh()
187    }
188
189    /// The sphere of radius one about the origin, with `segments` slices
190    /// around and `rings` from top to bottom. The texture wraps around it
191    /// once: its left and right edges meet at the back, its top and bottom
192    /// edges are the poles, and its middle faces the viewer.
193    pub fn sphere(segments: u32, rings: u32) -> Self {
194        let (segments, rings) = (segments.max(3), rings.max(2));
195        let columns = segments + 1;
196        let mut positions = Vec::with_capacity((columns * (rings + 1)) as usize);
197        let mut uvs = Vec::with_capacity(positions.capacity());
198        for ring in 0..=rings {
199            let v = ring as f32 / rings as f32;
200            let (sin_polar, cos_polar) = (v * PI).sin_cos();
201            for segment in 0..=segments {
202                let u = segment as f32 / segments as f32;
203                let (sin_around, cos_around) = ((u - 0.5) * 2.0 * PI).sin_cos();
204                positions.push([sin_polar * sin_around, -cos_polar, -sin_polar * cos_around]);
205                uvs.push([u, v]);
206            }
207        }
208        let mut indices = Vec::with_capacity((segments * rings * 6) as usize);
209        for ring in 0..rings {
210            for segment in 0..segments {
211                let top_left = ring * columns + segment;
212                let bottom_left = top_left + columns;
213                indices.extend([
214                    top_left,
215                    top_left + 1,
216                    bottom_left,
217                    bottom_left,
218                    top_left + 1,
219                    bottom_left + 1,
220                ]);
221            }
222        }
223        let normals = positions.clone();
224        Self::assembled(positions, uvs, normals, indices)
225    }
226
227    /// A number that no other mesh made in this process has. Renderers keep
228    /// a mesh's buffers under it.
229    pub fn id(&self) -> u64 {
230        self.id
231    }
232
233    /// The vertex positions.
234    pub fn positions(&self) -> &[[f32; 3]] {
235        &self.positions
236    }
237
238    /// The vertex normals, one for each position, each of length one.
239    pub fn normals(&self) -> &[[f32; 3]] {
240        &self.normals
241    }
242
243    /// The vertex texture coordinates, one for each position.
244    pub fn uvs(&self) -> &[[f32; 2]] {
245        &self.uvs
246    }
247
248    /// The vertex indices of the triangles' corners, three to a triangle.
249    pub fn indices(&self) -> &[u32] {
250        &self.indices
251    }
252
253    /// The nearest place in front of `origin` where the ray along
254    /// `direction` meets a triangle, from either side.
255    pub fn cast(&self, origin: Vec3, direction: Vec3) -> Option<MeshHit> {
256        let mut nearest: Option<MeshHit> = None;
257        for triangle in 0..self.triangle_count() {
258            let [a, b, c] = self.corners(triangle);
259            let Some((distance, weights)) = ray_triangle(origin, direction, [a, b, c]) else {
260                continue;
261            };
262            if nearest.is_some_and(|hit| hit.distance <= distance) {
263                continue;
264            }
265            nearest = Some(MeshHit {
266                distance,
267                triangle,
268                uv: self.uv_at(triangle, weights),
269            });
270        }
271        nearest
272    }
273
274    /// Where the ray meets the plane of `triangle`, as a texture coordinate
275    /// carried on from the triangle's corners: it lies outside the triangle
276    /// when the ray misses it. `None` when the ray runs along the plane or
277    /// meets it behind `origin`, or for a triangle that does not exist.
278    pub fn cast_plane(&self, origin: Vec3, direction: Vec3, triangle: u32) -> Option<[f32; 2]> {
279        if triangle >= self.triangle_count() {
280            return None;
281        }
282        let [a, b, c] = self.corners(triangle);
283        let normal = (b - a).cross(c - a);
284        let along = normal.dot(direction);
285        if along.abs() <= f32::EPSILON * normal.length() * direction.length() {
286            return None;
287        }
288        let distance = normal.dot(a - origin) / along;
289        if distance <= 0.0 {
290            return None;
291        }
292        let point = origin + direction * distance;
293        let weights = barycentric(point, [a, b, c], normal)?;
294        Some(self.uv_at(triangle, weights))
295    }
296
297    /// The position where the texture coordinate `uv` lies on the mesh, from
298    /// the first triangle that covers it; `None` when none does.
299    pub fn point_at(&self, uv: [f32; 2]) -> Option<Vec3> {
300        const EDGE: f32 = 1.0e-4;
301        let point = Vec3::new(uv[0], uv[1], 0.0);
302        (0..self.triangle_count()).find_map(|triangle| {
303            let [ia, ib, ic] = self.triangle_indices(triangle);
304            let corner = |index: usize| {
305                let [u, v] = self.uvs[index];
306                Vec3::new(u, v, 0.0)
307            };
308            let flat = [corner(ia), corner(ib), corner(ic)];
309            let normal = (flat[1] - flat[0]).cross(flat[2] - flat[0]);
310            let [wa, wb, wc] = barycentric(point, flat, normal)?;
311            (wa >= -EDGE && wb >= -EDGE && wc >= -EDGE).then(|| {
312                let position = |index: usize| Vec3::from_array(self.positions[index]);
313                position(ia) * wa + position(ib) * wb + position(ic) * wc
314            })
315        })
316    }
317
318    /// The point of the mesh, placed by `transform`, nearest `point`, with
319    /// the texture coordinate there; `None` for a mesh without triangles.
320    pub fn nearest(&self, point: Vec3, transform: &Mat4) -> Option<MeshContact> {
321        let mut nearest: Option<MeshContact> = None;
322        for triangle in 0..self.triangle_count() {
323            let corners = self
324                .corners(triangle)
325                .map(|corner| transform.transform_point(corner));
326            let (at, weights) = nearest_on_triangle(point, corners);
327            let distance = (point - at).length();
328            if nearest.is_some_and(|known| known.distance <= distance) {
329                continue;
330            }
331            nearest = Some(MeshContact {
332                point: at,
333                distance,
334                uv: self.uv_at(triangle, weights),
335            });
336        }
337        nearest
338    }
339
340    /// The smallest box along the axes that holds every vertex: its lowest
341    /// and highest corners.
342    pub fn bounds(&self) -> (Vec3, Vec3) {
343        *self.bounds.get_or_init(|| {
344            let mut low = Vec3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY);
345            let mut high = -low;
346            for &position in &self.positions {
347                let point = Vec3::from_array(position);
348                (low, high) = (low.min(point), high.max(point));
349            }
350            (low, high)
351        })
352    }
353
354    /// The smallest box along the axes that holds the corners of
355    /// [`Self::bounds`] that `transform` maps: its lowest and highest
356    /// corners.
357    pub fn bounds_in(&self, transform: &Mat4) -> (Vec3, Vec3) {
358        let (mesh_low, mesh_high) = self.bounds();
359        let mut low = Vec3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY);
360        let mut high = -low;
361        for corner in 0..8 {
362            let pick = |bit: u32, low: f32, high: f32| if corner & bit == 0 { low } else { high };
363            let point = transform.transform_point(Vec3::new(
364                pick(1, mesh_low.x, mesh_high.x),
365                pick(2, mesh_low.y, mesh_high.y),
366                pick(4, mesh_low.z, mesh_high.z),
367            ));
368            (low, high) = (low.min(point), high.max(point));
369        }
370        (low, high)
371    }
372
373    /// The plane that carries texture coordinates onto the mesh, worked out
374    /// once: the plane of a flat mesh, or the one that touches a curved mesh
375    /// at the texture's middle. `None` when the texture's middle does not lie
376    /// on the mesh, or the mesh stretches it to a line there.
377    pub fn texture_plane(&self) -> Option<TexturePlane> {
378        *self.texture_plane.get_or_init(|| {
379            self.flat_texture_plane()
380                .or_else(|| self.tangent_texture_plane())
381        })
382    }
383
384    fn flat_texture_plane(&self) -> Option<TexturePlane> {
385        let origin = self.point_at([0.0, 0.0])?;
386        let u_axis = self.point_at([1.0, 0.0])? - origin;
387        let v_axis = self.point_at([0.0, 1.0])? - origin;
388        let plane = TexturePlane {
389            origin,
390            u_axis,
391            v_axis,
392        };
393        let tolerance = 1.0e-3 * (u_axis.length() + v_axis.length());
394        let lies_flat = [[1.0, 1.0], [0.5, 0.5], [0.25, 0.75]].iter().all(|&uv| {
395            self.point_at(uv)
396                .is_some_and(|point| (point - plane.at(uv)).length() <= tolerance)
397        });
398        (lies_flat && u_axis.cross(v_axis).length() > tolerance * tolerance).then_some(plane)
399    }
400
401    fn tangent_texture_plane(&self) -> Option<TexturePlane> {
402        const STEP: f32 = 1.0 / 64.0;
403        let middle = self.point_at([0.5, 0.5])?;
404        let u_axis = (self.point_at([0.5 + STEP, 0.5])? - middle) * (1.0 / STEP);
405        let v_axis = (self.point_at([0.5, 0.5 + STEP])? - middle) * (1.0 / STEP);
406        (u_axis.cross(v_axis).length() > f32::EPSILON).then(|| TexturePlane {
407            origin: middle - u_axis * 0.5 - v_axis * 0.5,
408            u_axis,
409            v_axis,
410        })
411    }
412
413    fn triangle_count(&self) -> u32 {
414        (self.indices.len() / 3) as u32
415    }
416
417    fn triangle_indices(&self, triangle: u32) -> [usize; 3] {
418        let first = triangle as usize * 3;
419        [0, 1, 2].map(|corner| self.indices[first + corner] as usize)
420    }
421
422    fn corners(&self, triangle: u32) -> [Vec3; 3] {
423        self.triangle_indices(triangle)
424            .map(|index| Vec3::from_array(self.positions[index]))
425    }
426
427    fn uv_at(&self, triangle: u32, [wa, wb, wc]: [f32; 3]) -> [f32; 2] {
428        let [a, b, c] = self.triangle_indices(triangle).map(|index| self.uvs[index]);
429        [
430            a[0] * wa + b[0] * wb + c[0] * wc,
431            a[1] * wa + b[1] * wb + c[1] * wc,
432        ]
433    }
434
435    pub(super) fn assembled(
436        positions: Vec<[f32; 3]>,
437        uvs: Vec<[f32; 2]>,
438        normals: Vec<[f32; 3]>,
439        indices: Vec<u32>,
440    ) -> Self {
441        Self {
442            id: NEXT_MESH_ID.fetch_add(1, Ordering::Relaxed),
443            positions,
444            normals,
445            uvs,
446            indices,
447            texture_plane: OnceLock::new(),
448            bounds: OnceLock::new(),
449        }
450    }
451}
452
453impl TexturePlane {
454    /// Where `uv` lies in the plane.
455    pub fn at(&self, [u, v]: [f32; 2]) -> Vec3 {
456        self.origin + self.u_axis * u + self.v_axis * v
457    }
458}
459
460#[derive(Default)]
461struct MeshParts {
462    positions: Vec<[f32; 3]>,
463    normals: Vec<[f32; 3]>,
464    uvs: Vec<[f32; 2]>,
465    indices: Vec<u32>,
466}
467
468impl MeshParts {
469    /// A square face with the texture's top left corner at `top_left`, its
470    /// x along `right` and its y along `down`, facing the side from which
471    /// `right` turns clockwise into `down`.
472    fn quad(&mut self, top_left: Vec3, right: Vec3, down: Vec3) {
473        let base = self.positions.len() as u32;
474        let normal = down.cross(right).normalized().unwrap_or(Vec3::ZERO);
475        for (corner, uv) in [
476            (top_left, [0.0, 0.0]),
477            (top_left + right, [1.0, 0.0]),
478            (top_left + down, [0.0, 1.0]),
479            (top_left + right + down, [1.0, 1.0]),
480        ] {
481            self.positions.push(corner.to_array());
482            self.normals.push(normal.to_array());
483            self.uvs.push(uv);
484        }
485        self.indices
486            .extend([base, base + 1, base + 2, base + 2, base + 1, base + 3]);
487    }
488
489    fn into_mesh(self) -> Mesh {
490        Mesh::assembled(self.positions, self.uvs, self.normals, self.indices)
491    }
492}
493
494/// Normals that follow the triangles around each vertex, weighted by their
495/// areas.
496pub(super) fn smooth_normals(positions: &[[f32; 3]], indices: &[u32]) -> Vec<[f32; 3]> {
497    let mut sums = vec![Vec3::ZERO; positions.len()];
498    for triangle in indices.as_chunks::<3>().0 {
499        let corner = |index: u32| Vec3::from_array(positions[index as usize]);
500        let (a, b, c) = (
501            corner(triangle[0]),
502            corner(triangle[1]),
503            corner(triangle[2]),
504        );
505        let face = (b - a).cross(c - a);
506        for &index in triangle {
507            sums[index as usize] = sums[index as usize] + face;
508        }
509    }
510    sums.into_iter()
511        .map(|sum| {
512            sum.normalized()
513                .unwrap_or(Vec3::new(0.0, 0.0, -1.0))
514                .to_array()
515        })
516        .collect()
517}
518
519/// Where the ray meets the triangle, from either side: the distance along
520/// `direction` and the corners' weights at the hit.
521fn ray_triangle(origin: Vec3, direction: Vec3, [a, b, c]: [Vec3; 3]) -> Option<(f32, [f32; 3])> {
522    let (edge_b, edge_c) = (b - a, c - a);
523    let p = direction.cross(edge_c);
524    let determinant = edge_b.dot(p);
525    if determinant.abs() <= f32::EPSILON * edge_b.length() * edge_c.length() * direction.length() {
526        return None;
527    }
528    let inverse = 1.0 / determinant;
529    let offset = origin - a;
530    let wb = offset.dot(p) * inverse;
531    if !(0.0..=1.0).contains(&wb) {
532        return None;
533    }
534    let q = offset.cross(edge_b);
535    let wc = direction.dot(q) * inverse;
536    if wc < 0.0 || wb + wc > 1.0 {
537        return None;
538    }
539    let distance = edge_c.dot(q) * inverse;
540    (distance > 0.0).then_some((distance, [1.0 - wb - wc, wb, wc]))
541}
542
543/// The point of the triangle nearest `point`, and the weights of its corners
544/// there, by the regions of the triangle's corners and edges (Ericson, Real-
545/// Time Collision Detection, 5.1.5).
546fn nearest_on_triangle(point: Vec3, [a, b, c]: [Vec3; 3]) -> (Vec3, [f32; 3]) {
547    let (ab, ac) = (b - a, c - a);
548    let (d1, d2) = (ab.dot(point - a), ac.dot(point - a));
549    if d1 <= 0.0 && d2 <= 0.0 {
550        return (a, [1.0, 0.0, 0.0]);
551    }
552    let (d3, d4) = (ab.dot(point - b), ac.dot(point - b));
553    if d3 >= 0.0 && d4 <= d3 {
554        return (b, [0.0, 1.0, 0.0]);
555    }
556    let vc = d1 * d4 - d3 * d2;
557    if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
558        let v = d1 / (d1 - d3);
559        return (a + ab * v, [1.0 - v, v, 0.0]);
560    }
561    let (d5, d6) = (ab.dot(point - c), ac.dot(point - c));
562    if d6 >= 0.0 && d5 <= d6 {
563        return (c, [0.0, 0.0, 1.0]);
564    }
565    let vb = d5 * d2 - d1 * d6;
566    if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
567        let w = d2 / (d2 - d6);
568        return (a + ac * w, [1.0 - w, 0.0, w]);
569    }
570    let va = d3 * d6 - d5 * d4;
571    if va <= 0.0 && d4 - d3 >= 0.0 && d5 - d6 >= 0.0 {
572        let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
573        return (b + (c - b) * w, [0.0, 1.0 - w, w]);
574    }
575    let total = va + vb + vc;
576    if !total.is_normal() {
577        return (a, [1.0, 0.0, 0.0]);
578    }
579    let (v, w) = (vb / total, vc / total);
580    (a + ab * v + ac * w, [1.0 - v - w, v, w])
581}
582
583/// The weights of the triangle's corners at `point`, which lies in its
584/// plane; `normal` is the plane's. `None` for a triangle with no area.
585fn barycentric(point: Vec3, [a, b, c]: [Vec3; 3], normal: Vec3) -> Option<[f32; 3]> {
586    let area = normal.dot(normal);
587    if !area.is_normal() {
588        return None;
589    }
590    let wb = normal.dot((point - a).cross(c - a)) / area;
591    let wc = normal.dot((b - a).cross(point - a)) / area;
592    Some([1.0 - wb - wc, wb, wc])
593}