Skip to main content

kvas_engine/
mesh_deform.rs

1//! Vertex access and deformation for [`EditableMesh`]: the building blocks for procedural
2//! animation, destruction, terrain tools and "squash and stretch" effects from scripts.
3//!
4//! Every method works in the mesh's **local space** (before the object's transform), marks the
5//! mesh as changed so the GPU copy is refreshed, and keeps the normals as they are. Call
6//! [`EditableMesh::recalculate_normals`] after a deformation when lighting matters.
7//!
8//! ```
9//! use kvas_engine::{mesh::MeshData, mesh_edit::EditableMesh};
10//! use glam::Vec3;
11//! let mut mesh = EditableMesh::from_mesh(&MeshData::primitive("cube").unwrap()).unwrap();
12//! let (min, max) = mesh.bounds();
13//! mesh.twist(Vec3::Y, 45.0);          // 45 degrees per metre along Y
14//! mesh.wave(Vec3::X, Vec3::Y, 0.1, 1.0, 0.0);
15//! mesh.recalculate_normals().unwrap();
16//! assert!(mesh.bounds() != (min, max), "the shape changed");
17//! ```
18
19use crate::mesh_edit::EditableMesh;
20use crate::noise::{Noise, NoiseKind};
21use glam::{Mat4, Quat, Vec3};
22
23impl EditableMesh {
24    /// Number of vertices.
25    pub fn vertex_count(&self) -> usize {
26        self.vertices.len()
27    }
28
29    /// Local position of vertex `i`.
30    pub fn position(&self, i: usize) -> Option<Vec3> {
31        self.vertices.get(i).map(|v| Vec3::from(v.pos))
32    }
33
34    /// Moves one vertex. Returns `false` for a bad index or a non-finite position.
35    pub fn set_position(&mut self, i: usize, p: Vec3) -> bool {
36        match self.vertices.get_mut(i) {
37            Some(v) if p.is_finite() => {
38                v.pos = p.to_array();
39                self.changed();
40                true
41            }
42            _ => false,
43        }
44    }
45
46    /// Axis-aligned local bounds `(min, max)`; both zero for an empty mesh.
47    pub fn bounds(&self) -> (Vec3, Vec3) {
48        let mut it = self.vertices.iter().map(|v| Vec3::from(v.pos));
49        let Some(first) = it.next() else { return (Vec3::ZERO, Vec3::ZERO) };
50        it.fold((first, first), |(lo, hi), p| (lo.min(p), hi.max(p)))
51    }
52
53    /// Replaces every position with `f(index, position, normal)`. Non-finite results are ignored.
54    pub fn map_positions(&mut self, mut f: impl FnMut(usize, Vec3, Vec3) -> Vec3) {
55        for (i, v) in self.vertices.iter_mut().enumerate() {
56            let p = f(i, Vec3::from(v.pos), Vec3::from(v.normal));
57            if p.is_finite() {
58                v.pos = p.to_array();
59            }
60        }
61        self.changed();
62    }
63
64    /// Indices of the vertices inside a sphere (local space): the selection for brush-like edits.
65    pub fn select_sphere(&self, center: Vec3, radius: f32) -> Vec<usize> {
66        let r2 = radius * radius;
67        self.vertices.iter().enumerate().filter(|(_, v)| Vec3::from(v.pos).distance_squared(center) <= r2).map(|(i, _)| i).collect()
68    }
69
70    /// Indices of the vertices on the positive side of a plane through `point` with `normal`.
71    pub fn select_half_space(&self, point: Vec3, normal: Vec3) -> Vec<usize> {
72        let n = normal.normalize_or_zero();
73        self.vertices.iter().enumerate().filter(|(_, v)| (Vec3::from(v.pos) - point).dot(n) >= 0.0).map(|(i, _)| i).collect()
74    }
75
76    /// Adds `delta` to the listed vertices.
77    pub fn translate_vertices(&mut self, indices: &[usize], delta: Vec3) {
78        for &i in indices {
79            if let Some(v) = self.vertices.get_mut(i) {
80                v.pos = (Vec3::from(v.pos) + delta).to_array();
81            }
82        }
83        self.changed();
84    }
85
86    /// Moves the whole mesh.
87    pub fn translate(&mut self, delta: Vec3) {
88        self.map_positions(|_, p, _| p + delta);
89    }
90
91    /// Scales around `center` per axis.
92    pub fn scale_about(&mut self, center: Vec3, scale: Vec3) {
93        self.map_positions(|_, p, _| center + (p - center) * scale);
94    }
95
96    /// Applies an arbitrary matrix to positions and (rotation part only) normals.
97    pub fn transform(&mut self, m: Mat4) {
98        let (_, rot, _) = m.to_scale_rotation_translation();
99        for v in &mut self.vertices {
100            v.pos = m.transform_point3(Vec3::from(v.pos)).to_array();
101            v.normal = (rot * Vec3::from(v.normal)).normalize_or_zero().to_array();
102        }
103        self.changed();
104    }
105
106    /// Pushes every vertex along its normal (`amount` metres; negative shrinks).
107    pub fn inflate(&mut self, amount: f32) {
108        self.map_positions(|_, p, n| p + n * amount);
109    }
110
111    /// Twists the mesh around `axis` through the origin: `degrees_per_metre` of rotation per metre along the axis.
112    pub fn twist(&mut self, axis: Vec3, degrees_per_metre: f32) {
113        let axis = axis.normalize_or_zero();
114        if axis == Vec3::ZERO {
115            return;
116        }
117        self.map_positions(|_, p, _| Quat::from_axis_angle(axis, (p.dot(axis) * degrees_per_metre).to_radians()) * p);
118    }
119
120    /// Scales the cross-section along `axis`: `start` at the lowest point of the mesh, `end` at the highest.
121    pub fn taper(&mut self, axis: Vec3, start: f32, end: f32) {
122        let axis = axis.normalize_or_zero();
123        if axis == Vec3::ZERO {
124            return;
125        }
126        let (lo, hi) = self.bounds();
127        let (a, b) = (lo.dot(axis).min(hi.dot(axis)), lo.dot(axis).max(hi.dot(axis)));
128        self.map_positions(|_, p, _| {
129            let t = if b - a > 1e-6 { (p.dot(axis) - a) / (b - a) } else { 0.0 };
130            let along = axis * p.dot(axis);
131            along + (p - along) * (start + (end - start) * t)
132        });
133    }
134
135    /// Bends the mesh around the `bend_axis` direction: points are rotated around it by
136    /// `degrees_per_metre` times their distance along `along`.
137    pub fn bend(&mut self, along: Vec3, bend_axis: Vec3, degrees_per_metre: f32) {
138        let (along, axis) = (along.normalize_or_zero(), bend_axis.normalize_or_zero());
139        if along == Vec3::ZERO || axis == Vec3::ZERO {
140            return;
141        }
142        self.map_positions(|_, p, _| Quat::from_axis_angle(axis, (p.dot(along) * degrees_per_metre).to_radians()) * p);
143    }
144
145    /// A travelling sine wave: vertices move along `displace` by `amplitude * sin(2pi * (d / wavelength + phase))`,
146    /// where `d` is the position along `direction`.
147    pub fn wave(&mut self, direction: Vec3, displace: Vec3, amplitude: f32, wavelength: f32, phase: f32) {
148        let direction = direction.normalize_or_zero();
149        let wavelength = wavelength.max(1e-4);
150        self.map_positions(|_, p, _| p + displace * amplitude * ((p.dot(direction) / wavelength + phase) * std::f32::consts::TAU).sin());
151    }
152
153    /// Displaces vertices along their normals by 3D noise (rocks, terrain, hand-made wobble).
154    /// `time` slides the noise so repeated calls animate.
155    pub fn noise_displace(&mut self, noise: &Noise, amplitude: f32, time: f32) {
156        self.map_positions(|_, p, n| p + n * amplitude * noise.sample3(p.x, p.y, p.z + time));
157    }
158
159    /// Laplacian smoothing: moves each vertex towards the average of its neighbours.
160    /// Vertices that share a position stay together (hard-edged meshes do not tear).
161    pub fn smooth(&mut self, iterations: u32, factor: f32) {
162        let key = |p: [f32; 3]| p.map(|v| (v as f64 * 10_000.0).round() as i64);
163        for _ in 0..iterations.min(32) {
164            let mut sum = std::collections::HashMap::<[i64; 3], (Vec3, f32)>::new();
165            for t in self.indices.chunks_exact(3) {
166                for k in 0..3 {
167                    let (a, b) = (t[k] as usize, t[(k + 1) % 3] as usize);
168                    let (pa, pb) = (self.vertices[a].pos, self.vertices[b].pos);
169                    let e = sum.entry(key(pa)).or_default();
170                    e.0 += Vec3::from(pb);
171                    e.1 += 1.0;
172                    let e = sum.entry(key(pb)).or_default();
173                    e.0 += Vec3::from(pa);
174                    e.1 += 1.0;
175                }
176            }
177            for v in &mut self.vertices {
178                if let Some((s, n)) = sum.get(&key(v.pos)) {
179                    if *n > 0.0 {
180                        let p = Vec3::from(v.pos);
181                        v.pos = p.lerp(*s / *n, factor.clamp(0.0, 1.0)).to_array();
182                    }
183                }
184            }
185        }
186        self.changed();
187    }
188
189    /// A ready noise for [`Self::noise_displace`]: smooth Perlin at the given frequency.
190    pub fn default_noise(frequency: f32, seed: u32) -> Noise {
191        Noise::new(NoiseKind::Perlin).with_seed(seed).with_frequency(frequency)
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use crate::mesh::MeshData;
199
200    fn cube() -> EditableMesh {
201        EditableMesh::from_mesh(&MeshData::primitive("cube").unwrap()).unwrap()
202    }
203
204    #[test]
205    fn deformers_change_geometry_and_bump_the_revision() {
206        let mut m = cube();
207        let rev = m.revision;
208        let before = m.bounds();
209        m.taper(Vec3::Y, 1.0, 0.0);
210        assert!(m.revision > rev);
211        // The top of a fully tapered cube collapses to a point.
212        let top: Vec<_> = m.vertices.iter().filter(|v| v.pos[1] > 0.49).collect();
213        assert!(top.iter().all(|v| v.pos[0].abs() < 1e-4 && v.pos[2].abs() < 1e-4));
214        assert_eq!(m.bounds().1.y, before.1.y);
215        let mut m = cube();
216        m.bend(Vec3::Y, Vec3::Z, 30.0);
217        assert!(m.bounds() != before);
218        let mut m = cube();
219        m.noise_displace(&EditableMesh::default_noise(2.0, 1), 0.2, 0.0);
220        m.smooth(2, 0.5);
221        m.recalculate_normals().unwrap();
222        assert!(m.vertices.iter().all(|v| Vec3::from(v.pos).is_finite()));
223    }
224
225    #[test]
226    fn selection_and_access_are_checked() {
227        let mut m = cube();
228        assert_eq!(m.select_sphere(Vec3::ZERO, 10.0).len(), m.vertex_count());
229        assert!(m.select_half_space(Vec3::ZERO, Vec3::Y).len() < m.vertex_count());
230        assert!(!m.set_position(9999, Vec3::ZERO));
231        assert!(!m.set_position(0, Vec3::NAN));
232        assert!(m.set_position(0, Vec3::ONE));
233        let sel = m.select_half_space(Vec3::ZERO, Vec3::Y);
234        let y0 = m.position(sel[0]).unwrap().y;
235        m.translate_vertices(&sel, Vec3::Y);
236        assert!((m.position(sel[0]).unwrap().y - y0 - 1.0).abs() < 1e-5);
237    }
238}