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::{DMat4, DVec3},
9    traits::{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#[derive(Debug, Clone, PartialEq)]
53pub struct Surface {
54    /// Vertices — `nu * nv` points in row-major order.
55    pub vertices: Vec<DVec3>,
56    /// Per-vertex colors.
57    pub vertex_colors: Vec<AlphaColor<Srgb>>,
58    /// Per-vertex normals for smooth shading. All-zero → flat shading.
59    pub vertex_normals: Vec<DVec3>,
60    /// Triangle indices — `6 * (nu-1) * (nv-1)` entries.
61    pub triangle_indices: Vec<u32>,
62    /// Grid resolution `(nu, nv)`.
63    pub resolution: (u32, u32),
64    /// Transform matrix applied when rendering.
65    pub transform: DMat4,
66}
67
68impl Surface {
69    /// Construct a surface by sampling `uv_func` over a uniform grid.
70    ///
71    /// `u_range` and `v_range` define the parameter domain.
72    /// `resolution` `(nu, nv)` must each be >= 2.
73    pub fn from_uv_func(
74        uv_func: impl Fn(f64, f64) -> DVec3,
75        u_range: (f64, f64),
76        v_range: (f64, f64),
77        resolution: (u32, u32),
78    ) -> Self {
79        let (nu, nv) = resolution;
80        assert!(nu >= 2 && nv >= 2, "resolution must be >= (2, 2)");
81
82        let mut points = Vec::with_capacity((nu * nv) as usize);
83        for i in 0..nu {
84            let u = u_range.0 + (u_range.1 - u_range.0) * (i as f64 / (nu - 1) as f64);
85            for j in 0..nv {
86                let v = v_range.0 + (v_range.1 - v_range.0) * (j as f64 / (nv - 1) as f64);
87                points.push(uv_func(u, v));
88            }
89        }
90
91        let triangle_indices = generate_grid_indices(nu, nv);
92
93        let vertex_colors = vec![color::palette::css::BLUE.with_alpha(1.0); points.len()];
94        let vertex_normals = vec![DVec3::ZERO; points.len()];
95        Self {
96            vertices: points,
97            triangle_indices,
98            resolution,
99            vertex_colors,
100            vertex_normals,
101            transform: DMat4::IDENTITY,
102        }
103    }
104
105    /// Set per-vertex colors. Returns `self` for chaining.
106    pub fn with_vertex_colors(mut self, colors: Vec<AlphaColor<Srgb>>) -> Self {
107        self.vertex_colors = colors;
108        self
109    }
110
111    /// Set per-vertex colors by mapping the Z coordinate of each vertex through a colorscale.
112    ///
113    /// `colorscale` is a list of `(color, z_value)` pairs sorted by ascending `z_value`.
114    /// The vertex color is linearly interpolated between adjacent entries.
115    pub fn with_fill_by_z(mut self, colorscale: &[(AlphaColor<Srgb>, f64)]) -> Self {
116        let colors = self
117            .vertices
118            .iter()
119            .map(|p| colorscale_lookup(colorscale, p.z))
120            .collect();
121        self.vertex_colors = colors;
122        self
123    }
124
125    /// Set the transform matrix. Returns `self` for chaining.
126    pub fn with_transform(mut self, transform: DMat4) -> Self {
127        self.transform = transform;
128        self
129    }
130
131    /// Update per-vertex normals to smooth shading. Returns `self` for chaining.
132    pub fn with_smooth_normals(mut self) -> Self {
133        self.update_smooth_normals();
134        self
135    }
136    /// Update per-vertex normals to smooth shading.
137    pub fn update_smooth_normals(&mut self) -> &mut Self {
138        self.vertex_normals = compute_smooth_normals(&self.vertices, &self.triangle_indices);
139        self
140    }
141}
142
143impl Interpolatable for Surface {
144    fn lerp(&self, target: &Self, t: f64) -> Self {
145        Self {
146            vertices: self.vertices.lerp(&target.vertices, t),
147            // TODO: better interpolation
148            triangle_indices: if t < 0.5 {
149                self.triangle_indices.clone()
150            } else {
151                target.triangle_indices.clone()
152            },
153            resolution: if t < 0.5 {
154                self.resolution
155            } else {
156                target.resolution
157            },
158            vertex_colors: self.vertex_colors.lerp(&target.vertex_colors, t),
159            vertex_normals: self.vertex_normals.lerp(&target.vertex_normals, t),
160            transform: Interpolatable::lerp(&self.transform, &target.transform, t),
161        }
162    }
163}
164
165impl FillColor for Surface {
166    fn fill_color(&self) -> AlphaColor<Srgb> {
167        // TODO: make it better
168        let Rgba(rgba) = self
169            .vertex_colors
170            .first()
171            .cloned()
172            .map(Rgba::from)
173            .unwrap_or_default();
174        AlphaColor::new([rgba.x, rgba.y, rgba.z, rgba.w])
175    }
176
177    fn set_fill_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
178        self.vertex_colors.fill(color);
179        self
180    }
181
182    fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
183        self.vertex_colors
184            .iter_mut()
185            .for_each(|x| *x = x.with_alpha(opacity));
186        self
187    }
188}
189
190impl Opacity for Surface {
191    fn set_opacity(&mut self, opacity: f32) -> &mut Self {
192        self.set_fill_opacity(opacity)
193    }
194}
195
196impl From<Surface> for MeshItem {
197    fn from(value: Surface) -> Self {
198        MeshItem {
199            points: value
200                .vertices
201                .iter()
202                .map(|p| p.as_vec3())
203                .collect::<Vec<_>>()
204                .into(),
205            triangle_indices: value.triangle_indices,
206            transform: value.transform.as_mat4(),
207            vertex_colors: value
208                .vertex_colors
209                .into_iter()
210                .map(Rgba::from)
211                .collect::<Vec<_>>()
212                .into(),
213            vertex_normals: value
214                .vertex_normals
215                .iter()
216                .map(|n| n.as_vec3())
217                .collect::<Vec<_>>()
218                .into(),
219        }
220    }
221}
222
223impl Extract for Surface {
224    type Target = CoreItem;
225    fn extract_into(&self, buf: &mut Vec<Self::Target>) {
226        MeshItem::from(self.clone()).extract_into(buf);
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}