Skip to main content

kvas_engine/
meshmod.rs

1//! Mesh modifiers: a Blender-style stack that reshapes an object's mesh without touching the
2//! source model. Add [`MeshModifiers`] to an object with a `Shape` or a model and stack
3//! modifiers (Array, Mirror, Subdivide, Displace, Twist, Bend, Taper, Solidify) in any order.
4//!
5//! The result is a `DynamicMesh`, rebuilt only when the stack or the source changes.
6//!
7//! ```
8//! use kvas_engine::{prelude::*, scene::Scene, meshmod::*};
9//! let mut scene = Scene::default();
10//! let e = scene.spawn("Fence");
11//! scene.world.insert(e, Shape::of(ShapeType::Cube));
12//! scene.world.insert(e, MeshRenderer::default());
13//! scene.world.insert(e, MeshModifiers(vec![Modifier::array(3, Vec3::new(1.5, 0.0, 0.0))]));
14//! update(&mut scene.world);
15//! assert_eq!(scene.world.get::<DynamicMesh>(e).unwrap().data.vertices.len(), 3 * 24);
16//! ```
17
18use crate::assets::Assets;
19use crate::components::*;
20use crate::mesh::MeshData;
21use crate::noise::{Noise, NoiseKind};
22use crate::procedural::signature;
23use crate::reflect::Field;
24use glam::{Mat4, Quat, Vec3};
25use kvas_ecs::World;
26use serde::{Deserialize, Serialize};
27use std::sync::{Arc, RwLock};
28
29crate::choice_enum!(
30    /// Axis a modifier works along.
31    ModAxis { X, Y, Z } default Y
32);
33
34impl ModAxis {
35    fn index(self) -> usize {
36        self as usize
37    }
38}
39
40/// One entry of the stack. Fields are public so scripts can build stacks in code.
41#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
42#[serde(tag = "type")]
43pub enum Modifier {
44    /// Repeats the mesh `count` times, each copy moved, rotated and scaled relative to the previous one.
45    Array {
46        /// Whether the modifier is applied.
47        enabled: bool,
48        /// Number of copies including the original (1..64).
49        count: i32,
50        /// Offset between copies in local metres.
51        offset: Vec3,
52        /// Extra rotation per copy in degrees.
53        rotation: Vec3,
54        /// Scale multiplier per copy (1 = unchanged).
55        scale: f32,
56    },
57    /// Adds mirrored copies across the chosen local planes.
58    Mirror {
59        /// Whether the modifier is applied.
60        enabled: bool,
61        /// Mirror across the YZ plane.
62        x: bool,
63        /// Mirror across the XZ plane.
64        y: bool,
65        /// Mirror across the XY plane.
66        z: bool,
67    },
68    /// Splits every triangle into four, `levels` times (more vertices for Displace / Bend).
69    Subdivide {
70        /// Whether the modifier is applied.
71        enabled: bool,
72        /// Number of subdivision passes (1..4).
73        levels: i32,
74        /// Pull new vertices onto a sphere of the original size, rounding the shape.
75        smooth: f32,
76    },
77    /// Pushes vertices along their normals by noise.
78    Displace {
79        /// Whether the modifier is applied.
80        enabled: bool,
81        /// Displacement in metres.
82        strength: f32,
83        /// Noise features per metre.
84        scale: f32,
85        /// Noise seed.
86        seed: i32,
87    },
88    /// Rotates slices of the mesh progressively around an axis (a screw / wring).
89    Twist {
90        /// Whether the modifier is applied.
91        enabled: bool,
92        /// Total twist in degrees from one end to the other.
93        angle: f32,
94        /// Axis the twist runs along.
95        axis: ModAxis,
96    },
97    /// Curves the mesh along an axis.
98    Bend {
99        /// Whether the modifier is applied.
100        enabled: bool,
101        /// Total bend in degrees from one end to the other.
102        angle: f32,
103        /// Axis that gets bent.
104        axis: ModAxis,
105    },
106    /// Scales the cross-section along an axis: pointed or flared shapes.
107    Taper {
108        /// Whether the modifier is applied.
109        enabled: bool,
110        /// -1 pinches the top, +1 doubles it.
111        amount: f32,
112        /// Axis the taper runs along.
113        axis: ModAxis,
114    },
115    /// Gives surfaces thickness by adding an inner shell.
116    Solidify {
117        /// Whether the modifier is applied.
118        enabled: bool,
119        /// Shell thickness in metres.
120        thickness: f32,
121    },
122    /// Ripples the surface like a flag or water: a sine wave along one axis lifts the vertices.
123    Wave {
124        /// Whether the modifier is applied.
125        enabled: bool,
126        /// Height of the ripples in metres.
127        amplitude: f32,
128        /// Distance between crests in metres.
129        wavelength: f32,
130        /// Axis the wave travels along.
131        axis: ModAxis,
132        /// Shifts the wave pattern (0..1 = one full wavelength).
133        phase: f32,
134    },
135    /// Pulls vertices towards a sphere (or pushes them away): turns boxes into balls.
136    Cast {
137        /// Whether the modifier is applied.
138        enabled: bool,
139        /// 0 = unchanged, 1 = a perfect sphere, negative = inflated spikes.
140        factor: f32,
141    },
142    /// Slants the mesh: the further along the axis, the more it leans sideways.
143    Shear {
144        /// Whether the modifier is applied.
145        enabled: bool,
146        /// Lean per metre along the axis.
147        amount: f32,
148        /// Axis that decides how far each vertex leans.
149        axis: ModAxis,
150    },
151    /// Averages every vertex with its neighbours: rounds off hard edges and noise.
152    Smooth {
153        /// Whether the modifier is applied.
154        enabled: bool,
155        /// Passes (1..10).
156        iterations: i32,
157        /// How far each pass moves a vertex towards its neighbours, 0..1.
158        factor: f32,
159    },
160    /// Random push of every vertex: rough stone, crumpled paper, broken glass.
161    Jitter {
162        /// Whether the modifier is applied.
163        enabled: bool,
164        /// Maximum offset in metres.
165        amount: f32,
166        /// Random seed.
167        seed: i32,
168    },
169    /// Copies the mesh around a circle (columns of a temple, a fence ring, a wheel of spokes).
170    RadialArray {
171        /// Whether the modifier is applied.
172        enabled: bool,
173        /// Copies including the original (2..64).
174        count: i32,
175        /// Distance of each copy from the centre in metres.
176        radius: f32,
177        /// Axis the circle turns around.
178        axis: ModAxis,
179        /// Turn every copy to face outwards.
180        face_outward: bool,
181    },
182}
183
184impl Modifier {
185    /// Default array: `count` copies spaced by `offset`.
186    pub fn array(count: i32, offset: Vec3) -> Self {
187        Modifier::Array { enabled: true, count, offset, rotation: Vec3::ZERO, scale: 1.0 }
188    }
189
190    /// Default instance of a kind by name ("Array", "Mirror", ...), used by the editor's Add menu.
191    pub fn named(name: &str) -> Option<Self> {
192        Some(match name {
193            "Array" => Self::array(3, Vec3::new(1.2, 0.0, 0.0)),
194            "Mirror" => Modifier::Mirror { enabled: true, x: true, y: false, z: false },
195            "Subdivide" => Modifier::Subdivide { enabled: true, levels: 1, smooth: 0.0 },
196            "Displace" => Modifier::Displace { enabled: true, strength: 0.15, scale: 2.0, seed: 1 },
197            "Twist" => Modifier::Twist { enabled: true, angle: 90.0, axis: ModAxis::Y },
198            "Bend" => Modifier::Bend { enabled: true, angle: 60.0, axis: ModAxis::Y },
199            "Taper" => Modifier::Taper { enabled: true, amount: -0.6, axis: ModAxis::Y },
200            "Solidify" => Modifier::Solidify { enabled: true, thickness: 0.1 },
201            "Wave" => Modifier::Wave { enabled: true, amplitude: 0.2, wavelength: 2.0, axis: ModAxis::X, phase: 0.0 },
202            "Cast" => Modifier::Cast { enabled: true, factor: 0.6 },
203            "Shear" => Modifier::Shear { enabled: true, amount: 0.4, axis: ModAxis::Y },
204            "Smooth" => Modifier::Smooth { enabled: true, iterations: 2, factor: 0.5 },
205            "Jitter" => Modifier::Jitter { enabled: true, amount: 0.05, seed: 1 },
206            "RadialArray" => Modifier::RadialArray { enabled: true, count: 8, radius: 3.0, axis: ModAxis::Y, face_outward: true },
207            _ => return None,
208        })
209    }
210
211    /// Names accepted by [`Self::named`], in menu order.
212    pub const NAMES: &'static [&'static str] = &["Array", "RadialArray", "Mirror", "Subdivide", "Smooth", "Displace", "Jitter", "Wave", "Twist", "Bend", "Taper", "Shear", "Cast", "Solidify"];
213
214    /// Display name of this modifier.
215    pub fn name(&self) -> &'static str {
216        match self {
217            Modifier::Array { .. } => "Array",
218            Modifier::Mirror { .. } => "Mirror",
219            Modifier::Subdivide { .. } => "Subdivide",
220            Modifier::Displace { .. } => "Displace",
221            Modifier::Twist { .. } => "Twist",
222            Modifier::Bend { .. } => "Bend",
223            Modifier::Taper { .. } => "Taper",
224            Modifier::Solidify { .. } => "Solidify",
225            Modifier::Wave { .. } => "Wave",
226            Modifier::Cast { .. } => "Cast",
227            Modifier::Shear { .. } => "Shear",
228            Modifier::Smooth { .. } => "Smooth",
229            Modifier::Jitter { .. } => "Jitter",
230            Modifier::RadialArray { .. } => "RadialArray",
231        }
232    }
233
234    /// Whether the modifier is applied.
235    pub fn enabled(&self) -> bool {
236        match self {
237            Modifier::Array { enabled, .. }
238            | Modifier::Mirror { enabled, .. }
239            | Modifier::Subdivide { enabled, .. }
240            | Modifier::Displace { enabled, .. }
241            | Modifier::Twist { enabled, .. }
242            | Modifier::Bend { enabled, .. }
243            | Modifier::Taper { enabled, .. }
244            | Modifier::Solidify { enabled, .. }
245            | Modifier::Wave { enabled, .. }
246            | Modifier::Cast { enabled, .. }
247            | Modifier::Shear { enabled, .. }
248            | Modifier::Smooth { enabled, .. }
249            | Modifier::Jitter { enabled, .. }
250            | Modifier::RadialArray { enabled, .. } => *enabled,
251        }
252    }
253
254    /// Inspector fields of this modifier (including the enabled switch).
255    pub fn fields(&mut self) -> Vec<Field<'_>> {
256        match self {
257            Modifier::Array { enabled, count, offset, rotation, scale } => {
258                vec![Field::bool("enabled", enabled), Field::i32("count", count), Field::vec3("offset", offset), Field::vec3("rotation", rotation), Field::f32("scale", scale)]
259            }
260            Modifier::Mirror { enabled, x, y, z } => vec![Field::bool("enabled", enabled), Field::bool("x", x), Field::bool("y", y), Field::bool("z", z)],
261            Modifier::Subdivide { enabled, levels, smooth } => vec![Field::bool("enabled", enabled), Field::i32("levels", levels), Field::f32("smooth", smooth)],
262            Modifier::Displace { enabled, strength, scale, seed } => {
263                vec![Field::bool("enabled", enabled), Field::f32("strength", strength), Field::f32("scale", scale), Field::i32("seed", seed)]
264            }
265            Modifier::Twist { enabled, angle, axis } | Modifier::Bend { enabled, angle, axis } => {
266                vec![Field::bool("enabled", enabled), Field::f32("angle", angle), Field::choice("axis", axis)]
267            }
268            Modifier::Taper { enabled, amount, axis } => vec![Field::bool("enabled", enabled), Field::f32("amount", amount), Field::choice("axis", axis)],
269            Modifier::Solidify { enabled, thickness } => vec![Field::bool("enabled", enabled), Field::f32("thickness", thickness)],
270            Modifier::Wave { enabled, amplitude, wavelength, axis, phase } => {
271                vec![Field::bool("enabled", enabled), Field::f32("amplitude", amplitude), Field::f32("wavelength", wavelength), Field::choice("axis", axis), Field::f32("phase", phase)]
272            }
273            Modifier::Cast { enabled, factor } => vec![Field::bool("enabled", enabled), Field::f32("factor", factor)],
274            Modifier::Shear { enabled, amount, axis } => vec![Field::bool("enabled", enabled), Field::f32("amount", amount), Field::choice("axis", axis)],
275            Modifier::Smooth { enabled, iterations, factor } => vec![Field::bool("enabled", enabled), Field::i32("iterations", iterations), Field::f32("factor", factor)],
276            Modifier::Jitter { enabled, amount, seed } => vec![Field::bool("enabled", enabled), Field::f32("amount", amount), Field::i32("seed", seed)],
277            Modifier::RadialArray { enabled, count, radius, axis, face_outward } => {
278                vec![Field::bool("enabled", enabled), Field::i32("count", count), Field::f32("radius", radius), Field::choice("axis", axis), Field::bool("face_outward", face_outward)]
279            }
280        }
281    }
282}
283
284/// The modifier stack of an object, applied top to bottom.
285#[derive(Clone, Default, Serialize, Deserialize)]
286#[serde(default)]
287pub struct MeshModifiers(
288    /// Modifiers in application order.
289    pub Vec<Modifier>,
290);
291
292impl Component for MeshModifiers {}
293
294/// Cache of what the last build used, so unchanged stacks cost nothing.
295#[derive(Clone, Default)]
296pub struct ModifierState {
297    sig: u64,
298}
299
300static ASSETS: RwLock<Option<Assets>> = RwLock::new(None);
301
302/// Tells the modifier system where to load model files from (the editor and player call this).
303pub fn set_assets(assets: Assets) {
304    if let Ok(mut slot) = ASSETS.write() {
305        *slot = Some(assets);
306    }
307}
308
309/// The asset source set with [`set_assets`], if any.
310pub fn assets() -> Option<Assets> {
311    ASSETS.read().ok()?.clone()
312}
313
314fn source_mesh(key: &str) -> Option<MeshData> {
315    let assets = ASSETS.read().ok()?.clone().unwrap_or(Assets::Dir(std::path::PathBuf::new()));
316    MeshData::load(&assets, key).ok()
317}
318
319fn axes(axis: ModAxis) -> (usize, usize, usize) {
320    let a = axis.index();
321    (a, (a + 1) % 3, (a + 2) % 3)
322}
323
324fn extent(mesh: &MeshData, axis: usize) -> (f32, f32) {
325    mesh.vertices.iter().fold((f32::MAX, f32::MIN), |(lo, hi), v| (lo.min(v.pos[axis]), hi.max(v.pos[axis])))
326}
327
328fn renormalize(mesh: &mut MeshData) {
329    let _ = mesh.recalculate_normals();
330}
331
332fn subdivide(mesh: &mut MeshData, levels: i32, smooth: f32) {
333    let radius = mesh.vertices.iter().map(|v| Vec3::from(v.pos).length()).fold(0.0, f32::max);
334    for _ in 0..levels.clamp(1, 4) {
335        if mesh.indices.len() > 400_000 {
336            break;
337        }
338        let mut out = MeshData { albedo: mesh.albedo.clone(), base_color: mesh.base_color, dependencies: mesh.dependencies.clone(), ..Default::default() };
339        out.parts = mesh.parts.iter().map(|p| crate::mesh::MeshPart { first_index: p.first_index * 4, index_count: p.index_count * 4, albedo: p.albedo.clone(), normal: p.normal.clone(), orm: p.orm.clone() }).collect();
340        for tri in mesh.indices.chunks_exact(3) {
341            let v = [mesh.vertices[tri[0] as usize], mesh.vertices[tri[1] as usize], mesh.vertices[tri[2] as usize]];
342            let mid = |a: crate::mesh::Vertex, b: crate::mesh::Vertex| {
343                let p = (Vec3::from(a.pos) + Vec3::from(b.pos)) * 0.5;
344                let p = if smooth > 0.0 && radius > 1e-4 && p.length() > 1e-4 { p.lerp(p.normalize() * radius, smooth.clamp(0.0, 1.0)) } else { p };
345                let n = (Vec3::from(a.normal) + Vec3::from(b.normal)).normalize_or_zero();
346                crate::mesh::Vertex { pos: p.into(), normal: n.into(), uv: [(a.uv[0] + b.uv[0]) * 0.5, (a.uv[1] + b.uv[1]) * 0.5] }
347            };
348            let (m01, m12, m20) = (mid(v[0], v[1]), mid(v[1], v[2]), mid(v[2], v[0]));
349            let base = out.vertices.len() as u32;
350            out.vertices.extend([v[0], m01, m20, m01, v[1], m12, m20, m12, v[2], m01, m12, m20]);
351            out.indices.extend([base, base + 1, base + 2, base + 3, base + 4, base + 5, base + 6, base + 7, base + 8, base + 9, base + 10, base + 11]);
352        }
353        *mesh = out;
354    }
355}
356
357fn apply(mesh: &mut MeshData, modifier: &Modifier) {
358    match modifier {
359        Modifier::Array { count, offset, rotation, scale, .. } => {
360            let count = (*count).clamp(1, 64) as usize;
361            let original = mesh.clone();
362            let mut matrix = Mat4::IDENTITY;
363            let step = Mat4::from_scale_rotation_translation(Vec3::splat(scale.max(0.01)), Quat::from_euler(glam::EulerRot::YXZ, rotation.y.to_radians(), rotation.x.to_radians(), rotation.z.to_radians()), *offset);
364            for _ in 1..count {
365                if mesh.vertices.len() + original.vertices.len() > 600_000 {
366                    break;
367                }
368                matrix *= step;
369                if let Ok(copy) = original.transformed(matrix) {
370                    let _ = mesh.append(&copy);
371                }
372            }
373        }
374        Modifier::Mirror { x, y, z, .. } => {
375            for (on, scale) in [(*x, Vec3::new(-1.0, 1.0, 1.0)), (*y, Vec3::new(1.0, -1.0, 1.0)), (*z, Vec3::new(1.0, 1.0, -1.0))] {
376                if on && mesh.vertices.len() < 300_000 {
377                    if let Ok(copy) = mesh.transformed(Mat4::from_scale(scale)) {
378                        let _ = mesh.append(&copy);
379                    }
380                }
381            }
382        }
383        Modifier::Subdivide { levels, smooth, .. } => {
384            subdivide(mesh, *levels, *smooth);
385        }
386        Modifier::Displace { strength, scale, seed, .. } => {
387            let noise = Noise::new(NoiseKind::Perlin).with_seed(*seed as u32).with_octaves(3);
388            for v in &mut mesh.vertices {
389                let p = Vec3::from(v.pos);
390                let d = noise.sample3(p.x * scale, p.y * scale, p.z * scale) * strength;
391                v.pos = (p + Vec3::from(v.normal) * d).into();
392            }
393            renormalize(mesh);
394        }
395        Modifier::Twist { angle, axis, .. } => {
396            let (a, b, c) = axes(*axis);
397            let (lo, hi) = extent(mesh, a);
398            let span = (hi - lo).max(1e-4);
399            for v in &mut mesh.vertices {
400                let t = (v.pos[a] - lo) / span - 0.5;
401                let (s, co) = (angle.to_radians() * t).sin_cos();
402                let (pb, pc) = (v.pos[b], v.pos[c]);
403                v.pos[b] = pb * co - pc * s;
404                v.pos[c] = pb * s + pc * co;
405            }
406            renormalize(mesh);
407        }
408        Modifier::Bend { angle, axis, .. } => {
409            let (a, b, _) = axes(*axis);
410            let (lo, hi) = extent(mesh, a);
411            let span = (hi - lo).max(1e-4);
412            let k = angle.to_radians() / span;
413            if k.abs() > 1e-4 {
414                let r = 1.0 / k;
415                let mid = (lo + hi) * 0.5;
416                for v in &mut mesh.vertices {
417                    let along = v.pos[a] - mid;
418                    let theta = k * along;
419                    let off = v.pos[b];
420                    v.pos[a] = (r - off) * theta.sin() + mid;
421                    v.pos[b] = r - (r - off) * theta.cos();
422                }
423                renormalize(mesh);
424            }
425        }
426        Modifier::Taper { amount, axis, .. } => {
427            let (a, b, c) = axes(*axis);
428            let (lo, hi) = extent(mesh, a);
429            let span = (hi - lo).max(1e-4);
430            for v in &mut mesh.vertices {
431                let t = (v.pos[a] - lo) / span;
432                let f = (1.0 + amount * t).max(0.01);
433                v.pos[b] *= f;
434                v.pos[c] *= f;
435            }
436            renormalize(mesh);
437        }
438        Modifier::Wave { amplitude, wavelength, axis, phase, .. } => {
439            let (a, b, _) = axes(*axis);
440            let up = (a + 1) % 3;
441            let _ = b;
442            let k = std::f32::consts::TAU / wavelength.max(0.05);
443            for v in &mut mesh.vertices {
444                v.pos[up] += ((v.pos[a] * k) + phase * std::f32::consts::TAU).sin() * amplitude;
445            }
446            renormalize(mesh);
447        }
448        Modifier::Cast { factor, .. } => {
449            let radius = mesh.vertices.iter().map(|v| Vec3::from(v.pos).length()).sum::<f32>() / mesh.vertices.len().max(1) as f32;
450            for v in &mut mesh.vertices {
451                let p = Vec3::from(v.pos);
452                if p.length() > 1e-5 {
453                    v.pos = p.lerp(p.normalize() * radius, *factor).into();
454                }
455            }
456            renormalize(mesh);
457        }
458        Modifier::Shear { amount, axis, .. } => {
459            let (a, b, _) = axes(*axis);
460            for v in &mut mesh.vertices {
461                v.pos[b] += v.pos[a] * amount;
462            }
463            renormalize(mesh);
464        }
465        Modifier::Smooth { iterations, factor, .. } => {
466            // Vertices that share a position (hard edges are split in the mesh) are treated as one.
467            let key = |p: [f32; 3]| ((p[0] * 1000.0).round() as i64, (p[1] * 1000.0).round() as i64, (p[2] * 1000.0).round() as i64);
468            let mut ids = std::collections::HashMap::new();
469            let mut slot = Vec::with_capacity(mesh.vertices.len());
470            for v in &mesh.vertices {
471                let next = ids.len();
472                slot.push(*ids.entry(key(v.pos)).or_insert(next));
473            }
474            let mut neighbours: Vec<std::collections::HashSet<usize>> = vec![Default::default(); ids.len()];
475            for tri in mesh.indices.chunks_exact(3) {
476                let s = [slot[tri[0] as usize], slot[tri[1] as usize], slot[tri[2] as usize]];
477                for i in 0..3 {
478                    neighbours[s[i]].insert(s[(i + 1) % 3]);
479                    neighbours[s[i]].insert(s[(i + 2) % 3]);
480                }
481            }
482            let mut positions = vec![Vec3::ZERO; ids.len()];
483            for (v, &i) in mesh.vertices.iter().zip(&slot) {
484                positions[i] = Vec3::from(v.pos);
485            }
486            for _ in 0..(*iterations).clamp(1, 10) {
487                let next: Vec<Vec3> = (0..positions.len())
488                    .map(|i| {
489                        if neighbours[i].is_empty() {
490                            return positions[i];
491                        }
492                        let avg = neighbours[i].iter().map(|&n| positions[n]).sum::<Vec3>() / neighbours[i].len() as f32;
493                        positions[i].lerp(avg, factor.clamp(0.0, 1.0))
494                    })
495                    .collect();
496                positions = next;
497            }
498            for (v, &i) in mesh.vertices.iter_mut().zip(&slot) {
499                v.pos = positions[i].into();
500            }
501            renormalize(mesh);
502        }
503        Modifier::Jitter { amount, seed, .. } => {
504            let key = |p: [f32; 3]| ((p[0] * 1000.0).round() as i64, (p[1] * 1000.0).round() as i64, (p[2] * 1000.0).round() as i64);
505            let mut rng = crate::procedural::Rng::new(*seed as u64);
506            let mut offsets = std::collections::HashMap::new();
507            for v in &mut mesh.vertices {
508                // Split vertices at the same spot get the same push, so the mesh stays closed.
509                let o = *offsets.entry(key(v.pos)).or_insert_with(|| Vec3::new(rng.f32() - 0.5, rng.f32() - 0.5, rng.f32() - 0.5) * 2.0 * *amount);
510                v.pos = (Vec3::from(v.pos) + o).into();
511            }
512            renormalize(mesh);
513        }
514        Modifier::RadialArray { count, radius, axis, face_outward, .. } => {
515            let count = (*count).clamp(2, 64) as usize;
516            let original = mesh.clone();
517            let (a, b, c) = axes(*axis);
518            for i in 1..count {
519                if mesh.vertices.len() + original.vertices.len() > 600_000 {
520                    break;
521                }
522                let angle = std::f32::consts::TAU * i as f32 / count as f32;
523                let mut offset = [0.0f32; 3];
524                offset[b] = angle.cos() * radius;
525                offset[c] = angle.sin() * radius;
526                let mut axis_vec = Vec3::ZERO;
527                axis_vec[a] = 1.0;
528                let spin = if *face_outward { Quat::from_axis_angle(axis_vec, -angle) } else { Quat::IDENTITY };
529                let matrix = Mat4::from_rotation_translation(spin, Vec3::from(offset));
530                if let Ok(copy) = original.transformed(matrix) {
531                    let _ = mesh.append(&copy);
532                }
533            }
534        }
535        Modifier::Solidify { thickness, .. } => {
536            if mesh.vertices.len() < 300_000 {
537                let base = mesh.vertices.len() as u32;
538                let inner: Vec<_> = mesh
539                    .vertices
540                    .iter()
541                    .map(|v| crate::mesh::Vertex { pos: (Vec3::from(v.pos) - Vec3::from(v.normal) * *thickness).into(), normal: (-Vec3::from(v.normal)).into(), uv: v.uv })
542                    .collect();
543                let tris: Vec<u32> = mesh.indices.chunks_exact(3).flat_map(|t| [t[0] + base, t[2] + base, t[1] + base]).collect();
544                if !mesh.parts.is_empty() {
545                    let shift = mesh.indices.len() as u32;
546                    let copy: Vec<_> = mesh.parts.iter().map(|p| crate::mesh::MeshPart { first_index: p.first_index + shift, ..p.clone() }).collect();
547                    mesh.parts.extend(copy);
548                }
549                mesh.vertices.extend(inner);
550                mesh.indices.extend(tris);
551            }
552        }
553    }
554}
555
556/// Runs the stack on a source mesh.
557pub fn build(source: MeshData, stack: &[Modifier]) -> MeshData {
558    let mut mesh = source;
559    for m in stack.iter().filter(|m| m.enabled()) {
560        apply(&mut mesh, m);
561    }
562    mesh
563}
564
565/// Rebuilds the `DynamicMesh` of every object with modifiers whose stack or source changed.
566pub fn update(world: &mut World) {
567    for e in world.entities_with::<MeshModifiers>() {
568        let Some(key) = world.get::<Shape>(e).map(Shape::key).or_else(|| world.get::<MeshRenderer>(e).map(|m| m.mesh.clone())).filter(|k| !k.is_empty()) else { continue };
569        let stack = world.get::<MeshModifiers>(e).map(|m| m.0.clone()).unwrap_or_default();
570        let sig = signature(&(&stack, &key));
571        let known = world.get::<ModifierStateCell>(e).map(|c| c.0.sig);
572        if known == Some(sig) && world.has::<DynamicMesh>(e) {
573            continue;
574        }
575        if stack.iter().all(|m| !m.enabled()) {
576            if known.is_some() {
577                world.remove::<DynamicMesh>(e);
578                world.remove::<ModifierStateCell>(e);
579            }
580            continue;
581        }
582        let Some(source) = source_mesh(&key) else { continue };
583        let version = world.get::<DynamicMesh>(e).map_or(0, |d| d.version) + 1;
584        let mesh = build(source, &stack);
585        world.insert(e, DynamicMesh { data: Arc::new(mesh), version });
586        world.insert(e, ModifierStateCell(ModifierState { sig }));
587    }
588}
589
590/// Runtime-only marker holding the build signature (never saved).
591#[derive(Clone)]
592pub struct ModifierStateCell(pub ModifierState);
593
594#[cfg(test)]
595mod tests {
596    use super::*;
597
598    #[test]
599    fn every_modifier_keeps_the_mesh_valid() {
600        for name in Modifier::NAMES {
601            let source = MeshData::primitive("sphere").unwrap();
602            let before = source.vertices.len();
603            let modifier = Modifier::named(name).unwrap();
604            let out = build(source, &[Modifier::named("Subdivide").unwrap(), modifier]);
605            assert!(out.vertices.len() >= before, "{name}");
606            assert!(out.validate().is_ok(), "{name} produced an invalid mesh");
607            assert!(out.vertices.iter().all(|v| v.pos.iter().all(|c| c.is_finite())), "{name} produced NaN");
608        }
609    }
610}