Skip to main content

ranim_items/mesh/
surface.rs

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