Skip to main content

ranim_items/mesh/
surface.rs

1//! Surface — a parametric surface mesh item.
2
3use ranim_core::{
4    Extract,
5    color::{self, AlphaColor, Srgb},
6    components::rgba::Rgba,
7    core_item::CoreItem,
8    glam::DVec3,
9    traits::{ApplyTransform, FillColor, Interpolatable, Opacity},
10};
11
12use crate::mesh::MeshItem;
13
14use super::{compute_smooth_normals, generate_grid_indices};
15
16/// Linearly interpolate a color from a sorted colorscale based on a value.
17fn colorscale_lookup(colorscale: &[(AlphaColor<Srgb>, f64)], value: f64) -> AlphaColor<Srgb> {
18    if colorscale.is_empty() {
19        return color::palette::css::WHITE.with_alpha(1.0);
20    }
21    if value <= colorscale[0].1 {
22        return colorscale[0].0;
23    }
24    if value >= colorscale[colorscale.len() - 1].1 {
25        return colorscale[colorscale.len() - 1].0;
26    }
27    for i in 0..colorscale.len() - 1 {
28        let (c0, v0) = colorscale[i];
29        let (c1, v1) = colorscale[i + 1];
30        if value >= v0 && value <= v1 {
31            let t = ((value - v0) / (v1 - v0)) as f32;
32            let [r0, g0, b0, a0] = c0.components;
33            let [r1, g1, b1, a1] = c1.components;
34            return AlphaColor::new([
35                r0 + (r1 - r0) * t,
36                g0 + (g1 - g0) * t,
37                b0 + (b1 - b0) * t,
38                a0 + (a1 - a0) * t,
39            ]);
40        }
41    }
42    colorscale[colorscale.len() - 1].0
43}
44
45/// A parametric surface defined by pre-generated mesh data.
46///
47/// Vertices are stored in row-major order: `points[i * nv + j]` where
48/// `i` is the u-index and `j` is the v-index.
49///
50/// By default, vertex normals are all-zero, which causes flat shading.
51/// To enable smooth shading, call [`Self::with_smooth_normals`] or [`Self::update_smooth_normals`] to update the normals.
52///
53/// The vertices are expressed in the surface's local space. To place or animate
54/// the surface with an external transform, wrap it in
55/// [`ranim_core::core_item::transformed::Transformed`], commonly storing
56/// [`ranim_core::glam::DAffine3`] as the transform representation.
57#[derive(Debug, Clone, PartialEq)]
58pub struct Surface {
59    /// Vertices — `nu * nv` points in row-major order (local space).
60    pub vertices: Vec<DVec3>,
61    /// Per-vertex colors.
62    pub vertex_colors: Vec<AlphaColor<Srgb>>,
63    /// Per-vertex normals for smooth shading. All-zero → flat shading.
64    pub vertex_normals: Vec<DVec3>,
65    /// Triangle indices — `6 * (nu-1) * (nv-1)` entries.
66    pub triangle_indices: Vec<u32>,
67    /// Grid resolution `(nu, nv)`.
68    pub resolution: (u32, u32),
69}
70
71impl Surface {
72    /// Construct a surface by sampling `uv_func` over a uniform grid.
73    ///
74    /// `u_range` and `v_range` define the parameter domain.
75    /// `resolution` `(nu, nv)` must each be >= 2.
76    pub fn from_uv_func(
77        uv_func: impl Fn(f64, f64) -> DVec3,
78        u_range: (f64, f64),
79        v_range: (f64, f64),
80        resolution: (u32, u32),
81    ) -> Self {
82        let (nu, nv) = resolution;
83        assert!(nu >= 2 && nv >= 2, "resolution must be >= (2, 2)");
84
85        let mut points = Vec::with_capacity((nu * nv) as usize);
86        for i in 0..nu {
87            let u = u_range.0 + (u_range.1 - u_range.0) * (i as f64 / (nu - 1) as f64);
88            for j in 0..nv {
89                let v = v_range.0 + (v_range.1 - v_range.0) * (j as f64 / (nv - 1) as f64);
90                points.push(uv_func(u, v));
91            }
92        }
93
94        let triangle_indices = generate_grid_indices(nu, nv);
95
96        let vertex_colors = vec![color::palette::css::BLUE.with_alpha(1.0); points.len()];
97        let vertex_normals = vec![DVec3::ZERO; points.len()];
98        Self {
99            vertices: points,
100            triangle_indices,
101            resolution,
102            vertex_colors,
103            vertex_normals,
104        }
105    }
106
107    /// Set per-vertex colors. Returns `self` for chaining.
108    pub fn with_vertex_colors(mut self, colors: Vec<AlphaColor<Srgb>>) -> Self {
109        self.vertex_colors = colors;
110        self
111    }
112
113    /// Set per-vertex colors by mapping the Z coordinate of each vertex through a colorscale.
114    ///
115    /// `colorscale` is a list of `(color, z_value)` pairs sorted by ascending `z_value`.
116    /// The vertex color is linearly interpolated between adjacent entries.
117    pub fn with_fill_by_z(mut self, colorscale: &[(AlphaColor<Srgb>, f64)]) -> Self {
118        let colors = self
119            .vertices
120            .iter()
121            .map(|p| colorscale_lookup(colorscale, p.z))
122            .collect();
123        self.vertex_colors = colors;
124        self
125    }
126
127    /// Update per-vertex normals to smooth shading. Returns `self` for chaining.
128    pub fn with_smooth_normals(mut self) -> Self {
129        self.update_smooth_normals();
130        self
131    }
132    /// Update per-vertex normals to smooth shading.
133    pub fn update_smooth_normals(&mut self) -> &mut Self {
134        self.vertex_normals = compute_smooth_normals(&self.vertices, &self.triangle_indices);
135        self
136    }
137}
138
139impl Interpolatable for Surface {
140    fn lerp(&self, target: &Self, t: f64) -> Self {
141        Self {
142            vertices: self.vertices.lerp(&target.vertices, t),
143            // TODO: better interpolation
144            triangle_indices: if t < 0.5 {
145                self.triangle_indices.clone()
146            } else {
147                target.triangle_indices.clone()
148            },
149            resolution: if t < 0.5 {
150                self.resolution
151            } else {
152                target.resolution
153            },
154            vertex_colors: self.vertex_colors.lerp(&target.vertex_colors, t),
155            vertex_normals: self.vertex_normals.lerp(&target.vertex_normals, t),
156        }
157    }
158}
159
160impl FillColor for Surface {
161    fn fill_color(&self) -> AlphaColor<Srgb> {
162        // TODO: make it better
163        let Rgba(rgba) = self
164            .vertex_colors
165            .first()
166            .cloned()
167            .map(Rgba::from)
168            .unwrap_or_default();
169        AlphaColor::new([rgba.x, rgba.y, rgba.z, rgba.w])
170    }
171
172    fn set_fill_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
173        self.vertex_colors.fill(color);
174        self
175    }
176
177    fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
178        self.vertex_colors
179            .iter_mut()
180            .for_each(|x| *x = x.with_alpha(opacity));
181        self
182    }
183}
184
185impl Opacity for Surface {
186    fn set_opacity(&mut self, opacity: f32) -> &mut Self {
187        self.set_fill_opacity(opacity)
188    }
189}
190
191impl From<Surface> for MeshItem {
192    fn from(value: Surface) -> Self {
193        MeshItem {
194            points: value.vertices.into(),
195            triangle_indices: value.triangle_indices,
196            vertex_colors: value
197                .vertex_colors
198                .into_iter()
199                .map(Rgba::from)
200                .collect::<Vec<_>>()
201                .into(),
202            vertex_normals: value.vertex_normals.into(),
203        }
204    }
205}
206
207impl Extract for Surface {
208    type Target = CoreItem;
209    fn extract_into(&self, buf: &mut Vec<Self::Target>) {
210        MeshItem::from(self.clone()).extract_into(buf);
211    }
212}
213
214impl<G: Into<ranim_core::glam::DAffine3>> ApplyTransform<G> for Surface {
215    fn apply(&mut self, transform: G) -> &mut Self {
216        let transform = transform.into();
217        self.vertices.apply(transform);
218        if transform.matrix3.determinant().abs() > 1e-12 {
219            let normal_matrix = transform.matrix3.inverse().transpose();
220            self.vertex_normals.iter_mut().for_each(|normal| {
221                if let Some(unit) = (normal_matrix * *normal).try_normalize() {
222                    *normal = unit;
223                }
224            });
225        }
226        self
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use ranim_core::glam::dvec3;
234
235    #[test]
236    fn test_flat_surface() {
237        let surface =
238            Surface::from_uv_func(|u, v| dvec3(u, v, 0.0), (0.0, 1.0), (0.0, 1.0), (3, 3));
239        assert_eq!(surface.vertices.len(), 9);
240        assert_eq!(surface.triangle_indices.len(), 24);
241        assert_eq!(surface.resolution, (3, 3));
242
243        // Check corners
244        assert_eq!(surface.vertices[0], dvec3(0.0, 0.0, 0.0));
245        assert_eq!(surface.vertices[2], dvec3(0.0, 1.0, 0.0));
246        assert_eq!(surface.vertices[6], dvec3(1.0, 0.0, 0.0));
247        assert_eq!(surface.vertices[8], dvec3(1.0, 1.0, 0.0));
248    }
249
250    #[test]
251    fn test_surface_extract() {
252        let surface =
253            Surface::from_uv_func(|u, v| dvec3(u, v, 0.0), (0.0, 1.0), (0.0, 1.0), (2, 2));
254        let items = surface.extract();
255        assert_eq!(items.len(), 1);
256        match &items[0] {
257            CoreItem::MeshItem(mesh) => {
258                assert_eq!(mesh.points.len(), 4);
259                assert_eq!(mesh.triangle_indices.len(), 6);
260            }
261            _ => panic!("expected MeshItem"),
262        }
263    }
264
265    #[test]
266    fn test_surface_interpolation() {
267        let a = Surface::from_uv_func(|u, v| dvec3(u, v, 0.0), (0.0, 1.0), (0.0, 1.0), (2, 2));
268        let b = Surface::from_uv_func(|u, v| dvec3(u, v, 1.0), (0.0, 1.0), (0.0, 1.0), (2, 2));
269        let mid = a.lerp(&b, 0.5);
270        // z should be 0.5 for all points
271        for p in &mid.vertices {
272            assert!((p.z - 0.5).abs() < 1e-10);
273        }
274    }
275}