Skip to main content

ranim_items/mesh/
mod.rs

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