Skip to main content

ranim_items/mesh/
mod.rs

1//! Mesh-based items (Surface, Sphere, etc.)
2
3use ranim_core::{
4    Extract,
5    anchor::Aabb,
6    color::{AlphaColor, Srgb},
7    components::{PointVec, rgba::Rgba},
8    core_item::CoreItem,
9    glam::{DVec3, Mat4},
10    traits::{Alignable, ApplyTransform, Empty, FillColor, Interpolatable, Opacity},
11};
12
13mod sphere;
14mod surface;
15
16/// glTF scene-graph import (opt-in via the `gltf` feature).
17#[cfg(feature = "gltf")]
18pub mod gltf;
19
20pub use sphere::*;
21pub use surface::*;
22
23/// A high-level mesh item with per-vertex data wrapped in PointVec for animation support.
24///
25/// This struct uses [`PointVec`] to wrap vertex data, enabling proper alignment
26/// and interpolation for animations. When extracted, it converts to the low-level
27/// [`ranim_core::core_item::mesh_item::MeshItem`] for rendering.
28///
29/// The vertices are expressed in the mesh's local space. To place or animate the
30/// mesh with an external transform, wrap it in
31/// [`ranim_core::core_item::transformed::Transformed`], commonly storing
32/// [`ranim_core::glam::DAffine3`] as the transform representation.
33#[derive(Debug, Clone, PartialEq)]
34pub struct MeshItem {
35    /// The vertices of the mesh (local space)
36    pub points: PointVec<DVec3>,
37    /// The triangle indices
38    pub triangle_indices: Vec<u32>,
39    /// Per-vertex colors
40    pub vertex_colors: PointVec<Rgba>,
41    /// Per-vertex normals for smooth shading.
42    /// All-zero (or empty) → shader falls back to flat shading via `dpdx`/`dpdy`.
43    pub vertex_normals: PointVec<DVec3>,
44}
45
46impl MeshItem {
47    /// Create a MeshItem from vertices only (no indices, suitable for point clouds).
48    pub fn from_vertices(points: Vec<DVec3>) -> Self {
49        let len = points.len();
50        Self {
51            points: points.into(),
52            triangle_indices: Vec::new(),
53            vertex_colors: vec![Rgba::default(); len].into(),
54            vertex_normals: vec![DVec3::ZERO; len].into(),
55        }
56    }
57
58    /// Create a MeshItem from vertices and triangle indices.
59    pub fn from_indexed_vertices(points: Vec<DVec3>, triangle_indices: Vec<u32>) -> Self {
60        let len = points.len();
61        Self {
62            points: points.into(),
63            triangle_indices,
64            vertex_colors: vec![Rgba::default(); len].into(),
65            vertex_normals: vec![DVec3::ZERO; len].into(),
66        }
67    }
68
69    /// Set all vertex colors to the same value.
70    pub fn with_color(mut self, color: AlphaColor<Srgb>) -> Self {
71        let rgba: Rgba = color.into();
72        self.vertex_colors = vec![rgba; self.points.len()].into();
73        self
74    }
75}
76
77impl From<MeshItem> for ranim_core::core_item::mesh_item::MeshItem {
78    fn from(value: MeshItem) -> Self {
79        Self {
80            points: value.points.iter().map(|p| p.as_vec3()).collect(),
81            triangle_indices: value.triangle_indices,
82            transform: Mat4::IDENTITY,
83            vertex_colors: value.vertex_colors.iter().copied().collect(),
84            vertex_normals: value.vertex_normals.iter().map(|n| n.as_vec3()).collect(),
85        }
86    }
87}
88
89impl Extract for MeshItem {
90    type Target = CoreItem;
91    fn extract_into(&self, buf: &mut Vec<Self::Target>) {
92        buf.push(CoreItem::MeshItem(self.clone().into()));
93    }
94}
95
96impl<G: Into<ranim_core::glam::DAffine3>> ApplyTransform<G> for MeshItem {
97    fn apply(&mut self, transform: G) -> &mut Self {
98        let transform = transform.into();
99        self.points.apply(transform);
100        if transform.matrix3.determinant().abs() > 1e-12 {
101            let normal_matrix = transform.matrix3.inverse().transpose();
102            self.vertex_normals.iter_mut().for_each(|normal| {
103                if let Some(unit) = (normal_matrix * *normal).try_normalize() {
104                    *normal = unit;
105                }
106            });
107        }
108        self
109    }
110}
111
112impl Alignable for MeshItem {
113    fn is_aligned(&self, other: &Self) -> bool {
114        self.points.is_aligned(&other.points)
115            && self.vertex_colors.is_aligned(&other.vertex_colors)
116            && self.vertex_normals.is_aligned(&other.vertex_normals)
117    }
118
119    fn align_with(&mut self, other: &mut Self) {
120        self.points.align_with(&mut other.points);
121        self.vertex_colors.align_with(&mut other.vertex_colors);
122        self.vertex_normals.align_with(&mut other.vertex_normals);
123    }
124}
125
126impl Interpolatable for MeshItem {
127    fn lerp(&self, target: &Self, t: f64) -> Self {
128        Self {
129            points: self.points.lerp(&target.points, t),
130            triangle_indices: if t < 0.5 {
131                self.triangle_indices.clone()
132            } else {
133                target.triangle_indices.clone()
134            },
135            vertex_colors: self.vertex_colors.lerp(&target.vertex_colors, t),
136            vertex_normals: self.vertex_normals.lerp(&target.vertex_normals, t),
137        }
138    }
139}
140
141impl FillColor for MeshItem {
142    fn fill_color(&self) -> AlphaColor<Srgb> {
143        let Rgba(rgba) = self.vertex_colors.first().cloned().unwrap_or_default();
144        AlphaColor::new([rgba.x, rgba.y, rgba.z, rgba.w])
145    }
146
147    fn set_fill_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
148        let rgba: Rgba = color.into();
149        self.vertex_colors.iter_mut().for_each(|c| *c = rgba);
150        self
151    }
152
153    fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
154        self.vertex_colors.set_opacity(opacity);
155        self
156    }
157}
158
159impl Opacity for MeshItem {
160    fn set_opacity(&mut self, opacity: f32) -> &mut Self {
161        self.vertex_colors.set_opacity(opacity);
162        self
163    }
164}
165
166impl Aabb for MeshItem {
167    fn aabb(&self) -> [DVec3; 2] {
168        if self.points.is_empty() {
169            return [DVec3::ZERO, DVec3::ZERO];
170        }
171
172        // TODO: do some optimize and caching
173        let mut min = self.points[0];
174        let mut max = self.points[0];
175
176        for &p in &self.points[1..] {
177            min = min.min(p);
178            max = max.max(p);
179        }
180
181        [min, max]
182    }
183}
184
185impl Empty for MeshItem {
186    fn empty() -> Self {
187        Self {
188            points: Vec::new().into(),
189            triangle_indices: Vec::new(),
190            vertex_colors: Vec::new().into(),
191            vertex_normals: Vec::new().into(),
192        }
193    }
194}
195
196/// Compute smooth vertex normals from a triangle mesh.
197///
198/// Each face normal is weighted by the angle at the vertex before accumulation.
199/// The result is normalized per vertex. Degenerate triangles are skipped.
200pub fn compute_smooth_normals(points: &[DVec3], triangle_indices: &[u32]) -> Vec<DVec3> {
201    let mut normals = vec![DVec3::ZERO; points.len()];
202
203    for tri in triangle_indices.as_chunks::<3>().0 {
204        let (i0, i1, i2) = (tri[0] as usize, tri[1] as usize, tri[2] as usize);
205        let (p0, p1, p2) = (points[i0], points[i1], points[i2]);
206
207        let e01 = p1 - p0;
208        let e02 = p2 - p0;
209        let face_normal = e01.cross(e02);
210
211        // Skip degenerate triangles
212        if face_normal.length_squared() < 1e-20 {
213            continue;
214        }
215
216        // Weight by angle at each vertex
217        let e10 = p0 - p1;
218        let e12 = p2 - p1;
219        let e20 = p0 - p2;
220        let e21 = p1 - p2;
221
222        let angle0 = angle_between(e01, e02);
223        let angle1 = angle_between(e10, e12);
224        let angle2 = angle_between(e20, e21);
225
226        normals[i0] += face_normal * angle0;
227        normals[i1] += face_normal * angle1;
228        normals[i2] += face_normal * angle2;
229    }
230
231    for n in &mut normals {
232        let len = n.length();
233        if len > 1e-10 {
234            *n /= len;
235        }
236    }
237
238    normals
239}
240
241/// Angle (in radians) between two vectors.
242fn angle_between(a: DVec3, b: DVec3) -> f64 {
243    let denom = a.length() * b.length();
244    if denom < 1e-20 {
245        return 0.0;
246    }
247    (a.dot(b) / denom).clamp(-1.0, 1.0).acos()
248}
249
250/// Generate triangle indices for a `nu × nv` grid of vertices (row-major layout).
251///
252/// Each quad `[i, j]` → 2 triangles: `[tl, bl, tr]` and `[tr, bl, br]`
253/// where `tl = i*nv + j`, `tr = i*nv + j+1`, `bl = (i+1)*nv + j`, `br = (i+1)*nv + j+1`.
254///
255/// Total index count = `6 * (nu - 1) * (nv - 1)`.
256pub fn generate_grid_indices(nu: u32, nv: u32) -> Vec<u32> {
257    let mut indices = Vec::with_capacity(6 * (nu as usize - 1) * (nv as usize - 1));
258    for i in 0..nu - 1 {
259        for j in 0..nv - 1 {
260            let tl = i * nv + j;
261            let tr = i * nv + j + 1;
262            let bl = (i + 1) * nv + j;
263            let br = (i + 1) * nv + j + 1;
264            // Triangle 1: tl, bl, tr
265            indices.push(tl);
266            indices.push(bl);
267            indices.push(tr);
268            // Triangle 2: tr, bl, br
269            indices.push(tr);
270            indices.push(bl);
271            indices.push(br);
272        }
273    }
274    indices
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use ranim_core::{
281        anchor::Aabb,
282        color::palette::css,
283        glam::DVec3,
284        traits::{Alignable, Empty},
285    };
286
287    #[test]
288    fn generate_grid_indices_follows_quad_layout() {
289        // 2x2 grid -> 1 quad -> 2 triangles -> 6 indices, with the
290        // documented vertex order (tl, tr, bl, br).
291        let indices = generate_grid_indices(2, 2);
292        assert_eq!(indices, vec![0, 2, 1, 1, 2, 3]);
293
294        // General count: 6 indices per cell.
295        let nu = 10;
296        let nv = 5;
297        assert_eq!(
298            generate_grid_indices(nu, nv).len(),
299            6 * (nu as usize - 1) * (nv as usize - 1)
300        );
301    }
302
303    #[test]
304    fn test_mesh_item_alignable() {
305        let mut mesh1 = MeshItem::from_indexed_vertices(
306            vec![DVec3::new(0.0, 0.0, 0.0), DVec3::new(1.0, 0.0, 0.0)],
307            vec![0, 1, 2],
308        );
309
310        let mut mesh2 = MeshItem::from_indexed_vertices(
311            vec![
312                DVec3::new(0.0, 0.0, 0.0),
313                DVec3::new(1.0, 0.0, 0.0),
314                DVec3::new(0.0, 1.0, 0.0),
315                DVec3::new(1.0, 1.0, 0.0),
316            ],
317            vec![0, 1, 2, 1, 3, 2],
318        );
319
320        // Initially not aligned
321        assert!(!mesh1.is_aligned(&mesh2));
322
323        // Align them
324        mesh1.align_with(&mut mesh2);
325
326        // Now they should be aligned
327        assert!(mesh1.is_aligned(&mesh2));
328
329        // All vertex arrays should have same length
330        assert_eq!(mesh1.points.len(), 4);
331        assert_eq!(mesh2.points.len(), 4);
332        assert_eq!(mesh1.vertex_colors.len(), 4);
333        assert_eq!(mesh2.vertex_colors.len(), 4);
334        assert_eq!(mesh1.vertex_normals.len(), 4);
335        assert_eq!(mesh2.vertex_normals.len(), 4);
336
337        // mesh1's new points should be last point repeated (from PointVec::align_with)
338        assert_eq!(mesh1.points[2], DVec3::new(1.0, 0.0, 0.0));
339        assert_eq!(mesh1.points[3], DVec3::new(1.0, 0.0, 0.0));
340
341        // mesh2's points should remain unchanged (it was already longer)
342        assert_eq!(mesh2.points[0], DVec3::new(0.0, 0.0, 0.0));
343        assert_eq!(mesh2.points[3], DVec3::new(1.0, 1.0, 0.0));
344    }
345
346    #[test]
347    fn test_mesh_item_interpolate() {
348        use ranim_core::traits::Interpolatable;
349
350        let mut mesh1 = MeshItem::from_indexed_vertices(
351            vec![DVec3::new(0.0, 0.0, 0.0), DVec3::new(1.0, 0.0, 0.0)],
352            vec![0, 1, 2],
353        )
354        .with_color(css::RED.with_alpha(1.0));
355
356        let mut mesh2 = MeshItem::from_indexed_vertices(
357            vec![DVec3::new(2.0, 0.0, 0.0), DVec3::new(3.0, 0.0, 0.0)],
358            vec![0, 1, 3],
359        )
360        .with_color(css::GREEN.with_alpha(1.0));
361
362        // Align first
363        mesh1.align_with(&mut mesh2);
364
365        // Interpolate at t = 0.5
366        let interpolated = mesh1.lerp(&mesh2, 0.5);
367
368        // Points should be halfway between
369        assert_eq!(interpolated.points[0], DVec3::new(1.0, 0.0, 0.0));
370        assert_eq!(interpolated.points[1], DVec3::new(2.0, 0.0, 0.0));
371
372        // triangle_indices should be from mesh2 (since t >= 0.5)
373        assert_eq!(interpolated.triangle_indices, vec![0, 1, 3]);
374    }
375
376    #[test]
377    fn mesh_item_empty_and_bounds() {
378        let mesh = MeshItem::empty();
379        assert_eq!(mesh.points.len(), 0);
380        assert_eq!(mesh.triangle_indices.len(), 0);
381        assert_eq!(mesh.vertex_colors.len(), 0);
382        assert_eq!(mesh.vertex_normals.len(), 0);
383
384        let mesh = MeshItem::from_indexed_vertices(
385            vec![
386                DVec3::new(-1.0, -1.0, -1.0),
387                DVec3::new(1.0, -1.0, -1.0),
388                DVec3::new(1.0, 1.0, -1.0),
389                DVec3::new(-1.0, 1.0, 1.0),
390            ],
391            vec![0, 1, 2],
392        );
393        let [min, max] = mesh.aabb();
394        assert_eq!(min, DVec3::splat(-1.0));
395        assert_eq!(max, DVec3::splat(1.0));
396    }
397}