Skip to main content

rapier3d/dynamics/soft_body/soft_body_builder/
soft_body_builder_shapes.rs

1//! Shape constructors of the soft-body builder: meshes, ropes, cloths, cuboids, spheres, polygons, disks and grids.
2
3use crate::alloc_prelude::*;
4#[cfg(feature = "dim3")]
5use crate::dynamics::SpringCoefficients;
6use crate::geometry::ColliderBuilder;
7use crate::math::{DIM, Real, Vector};
8use na::SimdRealField;
9use parry::utils::hashmap::HashMap;
10#[cfg(not(feature = "std"))]
11#[allow(unused_imports)]
12use simba::scalar::{ComplexField as _, RealField as _};
13
14use super::super::{SoftBodyCellModel, SoftBodyMaterial};
15#[cfg(feature = "dim3")]
16use super::soft_body_builder_mesh_helpers::icosphere;
17use super::soft_body_builder_mesh_helpers::mean_edge_length;
18#[cfg(any(feature = "dim3", feature = "std"))]
19use super::soft_body_builder_mesh_helpers::{cell_faces, drop_stray_pieces};
20use super::{SoftBodyBuilder, SoftBodyParticleSettings};
21
22impl SoftBodyBuilder {
23    /// A builder over the given world-space particle positions, with no element.
24    pub fn new(positions: Vec<Vector>) -> Self {
25        Self {
26            positions,
27            masses: Vec::new(),
28            particle_mass: 1.0,
29            pinned: Vec::new(),
30            edges: Vec::new(),
31            bend_edges: Vec::new(),
32            tension_only_edges: Vec::new(),
33            edge_softness: Vec::new(),
34            edge_tear_resistance: Vec::new(),
35            #[cfg(feature = "dim3")]
36            dihedrals: Vec::new(),
37            cells: Vec::new(),
38            surface: Vec::new(),
39            material: SoftBodyMaterial::default(),
40            cell_model: SoftBodyCellModel::default(),
41            #[cfg(feature = "fem")]
42            solver: super::SoftBodySolver::default(),
43            volume_preservation: false,
44            volume_factor: 1.0,
45            shape_matching: false,
46            self_contacts: false,
47            oriented: None,
48            particle_radius: 0.01,
49            skin: None,
50            #[cfg(feature = "dim3")]
51            wire: Vec::new(),
52            skin_collision: false,
53            collider_template: Some(ColliderBuilder::ball(0.05).density(0.0)),
54            particle_settings: SoftBodyParticleSettings::default(),
55            user_data: 0,
56        }
57    }
58
59    /*
60     * Generators.
61     */
62
63    /// A triangle-mesh soft body without volumetric cells (`None` if empty): vertices are
64    /// particles, edges are structural and shape matching holds the shape. 3D: the triangles are
65    /// the surface (dihedral bending across interior edges); 2D: the boundary is the surface.
66    pub fn trimesh(vertices: Vec<Vector>, indices: Vec<[u32; 3]>) -> Option<Self> {
67        if vertices.is_empty() || indices.is_empty() {
68            return None;
69        }
70        #[cfg(feature = "dim3")]
71        {
72            let mut builder = Self::new(vertices).surface(indices);
73            let edges = builder.surface_edges();
74            let dihedrals = builder.surface_dihedrals();
75            let mean_edge = mean_edge_length(&builder.positions, &edges);
76            builder = builder
77                .edges(edges)
78                .dihedrals(dihedrals)
79                .shape_matching(true)
80                .particle_radius(mean_edge * 0.5);
81            Some(builder)
82        }
83        #[cfg(feature = "dim2")]
84        {
85            // Counter-clockwise triangles, so their unshared (boundary) edges wind
86            // counter-clockwise around the shape (and clockwise around its holes).
87            let triangles: Vec<[u32; 3]> = indices
88                .iter()
89                .map(|&[a, b, c]| {
90                    let (pa, pb, pc) = (
91                        vertices[a as usize],
92                        vertices[b as usize],
93                        vertices[c as usize],
94                    );
95                    if (pb - pa).perp_dot(pc - pa) >= 0.0 {
96                        [a, b, c]
97                    } else {
98                        [a, c, b]
99                    }
100                })
101                .collect();
102            let mut directed: HashMap<[u32; 2], ()> = HashMap::default();
103            let mut structural: HashMap<[u32; 2], ()> = HashMap::default();
104            let mut edges = Vec::new();
105            for tri in &triangles {
106                for k in 0..3 {
107                    let (u, v) = (tri[k], tri[(k + 1) % 3]);
108                    directed.insert([u, v], ());
109                    let key = [u.min(v), u.max(v)];
110                    if structural.insert(key, ()).is_none() {
111                        edges.push(key);
112                    }
113                }
114            }
115            // A directed edge whose reverse is used by no triangle bounds the shape.
116            let mut boundary: Vec<[u32; 2]> = Vec::new();
117            for tri in &triangles {
118                for k in 0..3 {
119                    let (u, v) = (tri[k], tri[(k + 1) % 3]);
120                    if !directed.contains_key(&[v, u]) {
121                        boundary.push([u, v]);
122                    }
123                }
124            }
125            let mean_edge = mean_edge_length(&vertices, &edges);
126            Some(
127                Self::new(vertices)
128                    .edges(edges)
129                    .surface(boundary)
130                    .shape_matching(true)
131                    .particle_radius(mean_edge * 0.5),
132            )
133        }
134    }
135
136    /// A soft body from a polyline (2D only): particles at the vertices, structural edges and the
137    /// surface on the segments, bending edges at two-segment vertices, shape matching, an inside
138    /// ([`Self::volume_preservation`]) if closed counter-clockwise; `None` for an empty polyline.
139    #[cfg(feature = "dim2")]
140    pub fn polyline(vertices: Vec<Vector>, indices: Option<Vec<[u32; 2]>>) -> Option<Self> {
141        if vertices.len() < 2 {
142            return None;
143        }
144        let segments =
145            indices.unwrap_or_else(|| (0..vertices.len() as u32 - 1).map(|i| [i, i + 1]).collect());
146        if segments.is_empty() {
147            return None;
148        }
149        // The two neighbors of every vertex along the polyline (`u32::MAX`: none).
150        let mut neighbors: Vec<[u32; 2]> = vec![[u32::MAX; 2]; vertices.len()];
151        let mut valence: Vec<u32> = vec![0; vertices.len()];
152        for &[a, b] in &segments {
153            for (v, other) in [(a, b), (b, a)] {
154                let n = &mut neighbors[v as usize];
155                let count = &mut valence[v as usize];
156                if *count < 2 {
157                    n[*count as usize] = other;
158                }
159                *count += 1;
160            }
161        }
162        let bend_edges: Vec<[u32; 2]> = neighbors
163            .iter()
164            .zip(&valence)
165            .filter(|(n, count)| **count == 2 && n[0] != n[1])
166            .map(|(n, _)| [n[0].min(n[1]), n[0].max(n[1])])
167            .collect();
168        let mean_edge = mean_edge_length(&vertices, &segments);
169        Some(
170            Self::new(vertices)
171                .edges(segments.clone())
172                .bend_edges(bend_edges)
173                .surface(segments)
174                .shape_matching(true)
175                .particle_radius(mean_edge * 0.5),
176        )
177    }
178
179    /// A rope of `num_particles` particles from `start` to `end`: structural edges between
180    /// neighbors, bending edges between second neighbors; in 2D the segments are also the surface.
181    /// The particle radius (the rope's thickness) defaults to half the segment length.
182    pub fn rope(start: Vector, end: Vector, num_particles: usize) -> Self {
183        let num_particles = num_particles.max(2);
184        let positions: Vec<Vector> = (0..num_particles)
185            .map(|i| start.lerp(end, i as Real / (num_particles - 1) as Real))
186            .collect();
187        let edges: Vec<[u32; 2]> = (0..num_particles as u32 - 1).map(|i| [i, i + 1]).collect();
188        let bend_edges = (0..num_particles as u32)
189            .filter_map(|i| (i + 2 < num_particles as u32).then_some([i, i + 2]))
190            .collect();
191        let segment = (end - start).length() / (num_particles - 1) as Real;
192        let builder = Self::new(positions).particle_radius(segment * 0.5);
193        // The rope collides through its own segments: a polyline surface in 2D, a wire in 3D.
194        #[cfg(feature = "dim2")]
195        let builder = builder.surface(edges.clone());
196        #[cfg(feature = "dim3")]
197        let builder = builder.wire(edges.clone());
198        builder.edges(edges).bend_edges(bend_edges)
199    }
200
201    /// A rectangular cloth of `nu × nv` particles (3D only): `origin + i * du + j * dv`, with
202    /// structural, shear and bending edges, and a triangle surface for rendering. The particle
203    /// radius defaults to half the smallest edge length so the particles tile the surface.
204    #[cfg(feature = "dim3")]
205    pub fn cloth(origin: Vector, du: Vector, dv: Vector, nu: usize, nv: usize) -> Self {
206        let (nu, nv) = (nu.max(2), nv.max(2));
207        let idx = |i: usize, j: usize| (i * nv + j) as u32;
208        let mut positions = Vec::with_capacity(nu * nv);
209        for i in 0..nu {
210            for j in 0..nv {
211                positions.push(origin + du * i as Real + dv * j as Real);
212            }
213        }
214        let mut edges = Vec::new();
215        let mut bend_edges = Vec::new();
216        let mut surface = Vec::new();
217        for i in 0..nu {
218            for j in 0..nv {
219                if i + 1 < nu {
220                    edges.push([idx(i, j), idx(i + 1, j)]);
221                }
222                if j + 1 < nv {
223                    edges.push([idx(i, j), idx(i, j + 1)]);
224                }
225                if i + 1 < nu && j + 1 < nv {
226                    // Shear edges (both diagonals).
227                    edges.push([idx(i, j), idx(i + 1, j + 1)]);
228                    edges.push([idx(i + 1, j), idx(i, j + 1)]);
229                    // Two triangles per quad, alternating diagonals.
230                    if (i + j) % 2 == 0 {
231                        surface.push([idx(i, j), idx(i + 1, j), idx(i + 1, j + 1)]);
232                        surface.push([idx(i, j), idx(i + 1, j + 1), idx(i, j + 1)]);
233                    } else {
234                        surface.push([idx(i, j), idx(i + 1, j), idx(i, j + 1)]);
235                        surface.push([idx(i + 1, j), idx(i + 1, j + 1), idx(i, j + 1)]);
236                    }
237                }
238                if i + 2 < nu {
239                    bend_edges.push([idx(i, j), idx(i + 2, j)]);
240                }
241                if j + 2 < nv {
242                    bend_edges.push([idx(i, j), idx(i, j + 2)]);
243                }
244            }
245        }
246        let radius = du.length().min(dv.length()) * 0.5;
247        Self::new(positions)
248            .edges(edges)
249            .bend_edges(bend_edges)
250            .surface(surface)
251            .particle_radius(radius)
252    }
253
254    /// A cloth tube: `num_along` rings of `num_around` particles around the segment `origin` to
255    /// `origin + axis`, radius linear from `radius_start` to `radius_end`; edges and surface of
256    /// [`Self::cloth`] closed around the axis. Ring-major particles: ring `0` is `0..num_around`.
257    #[cfg(feature = "dim3")]
258    pub fn cloth_tube(
259        origin: Vector,
260        axis: Vector,
261        radius_start: Real,
262        radius_end: Real,
263        num_around: usize,
264        num_along: usize,
265    ) -> Self {
266        use crate::utils::OrthonormalBasis;
267        let (na, nl) = (num_around.max(3), num_along.max(2));
268        let dir = axis.try_normalize().unwrap_or(Vector::Y);
269        let [u, v] = dir.orthonormal_basis();
270        let idx = |ring: usize, k: usize| (ring * na + (k % na)) as u32;
271        let mut positions = Vec::with_capacity(na * nl);
272        for ring in 0..nl {
273            let t = ring as Real / (nl - 1) as Real;
274            let radius = radius_start + (radius_end - radius_start) * t;
275            let center = origin + axis * t;
276            for k in 0..na {
277                let a = k as Real / na as Real * Real::simd_two_pi();
278                positions.push(center + (u * a.cos() + v * a.sin()) * radius);
279            }
280        }
281        let mut edges = Vec::new();
282        let mut bend_edges = Vec::new();
283        let mut surface = Vec::new();
284        for ring in 0..nl {
285            for k in 0..na {
286                edges.push([idx(ring, k), idx(ring, k + 1)]);
287                bend_edges.push([idx(ring, k), idx(ring, k + 2)]);
288                if ring + 1 < nl {
289                    edges.push([idx(ring, k), idx(ring + 1, k)]);
290                    edges.push([idx(ring, k), idx(ring + 1, k + 1)]);
291                    edges.push([idx(ring, k + 1), idx(ring + 1, k)]);
292                    if (ring + k) % 2 == 0 {
293                        surface.push([idx(ring, k), idx(ring + 1, k), idx(ring + 1, k + 1)]);
294                        surface.push([idx(ring, k), idx(ring + 1, k + 1), idx(ring, k + 1)]);
295                    } else {
296                        surface.push([idx(ring, k), idx(ring + 1, k), idx(ring, k + 1)]);
297                        surface.push([idx(ring + 1, k), idx(ring + 1, k + 1), idx(ring, k + 1)]);
298                    }
299                }
300                if ring + 2 < nl {
301                    bend_edges.push([idx(ring, k), idx(ring + 2, k)]);
302                }
303            }
304        }
305        let ring_step = radius_start.min(radius_end) * Real::simd_two_pi() / na as Real;
306        let radius = ring_step.min(axis.length() / (nl - 1) as Real) * 0.5;
307        Self::new(positions)
308            .edges(edges)
309            .bend_edges(bend_edges)
310            .surface(surface)
311            .particle_radius(radius)
312    }
313
314    /// [`Self::cloth`] with anisotropic stiffness: the edges along `du` (warp), along `dv`
315    /// (weft) and the diagonals (shear) get their own softness (the material's `bend_softness`
316    /// still applies to the bending edges).
317    #[cfg(feature = "dim3")]
318    pub fn cloth_anisotropic(
319        origin: Vector,
320        du: Vector,
321        dv: Vector,
322        nu: usize,
323        nv: usize,
324        warp: SpringCoefficients<Real>,
325        weft: SpringCoefficients<Real>,
326        shear: SpringCoefficients<Real>,
327    ) -> Self {
328        let mut builder = Self::cloth(origin, du, dv, nu, nv);
329        let (nu, nv) = (nu.max(2), nv.max(2));
330        let idx = |i: usize, j: usize| (i * nv + j) as u32;
331        // Same edge order as `cloth`.
332        let mut k = 0u32;
333        for i in 0..nu {
334            for j in 0..nv {
335                if i + 1 < nu {
336                    debug_assert_eq!(builder.edges[k as usize], [idx(i, j), idx(i + 1, j)]);
337                    builder.edge_softness.push((k, warp));
338                    k += 1;
339                }
340                if j + 1 < nv {
341                    builder.edge_softness.push((k, weft));
342                    k += 1;
343                }
344                if i + 1 < nu && j + 1 < nv {
345                    builder.edge_softness.push((k, shear));
346                    builder.edge_softness.push((k + 1, shear));
347                    k += 2;
348                }
349            }
350        }
351        builder
352    }
353
354    /// A solid box tetrahedralized on a `nx × ny × nz` particle grid (3D only), with structural
355    /// edges along the tetrahedra edges and the boundary triangles as surface.
356    ///
357    /// The particle radius defaults to half the smallest cell size.
358    #[cfg(feature = "dim3")]
359    pub fn cuboid(center: Vector, half_extents: Vector, nx: usize, ny: usize, nz: usize) -> Self {
360        let (nx, ny, nz) = (nx.max(2), ny.max(2), nz.max(2));
361        let step = Vector::new(
362            2.0 * half_extents.x / (nx - 1) as Real,
363            2.0 * half_extents.y / (ny - 1) as Real,
364            2.0 * half_extents.z / (nz - 1) as Real,
365        );
366        let idx = |i: usize, j: usize, k: usize| ((i * ny + j) * nz + k) as u32;
367        let mut positions = Vec::with_capacity(nx * ny * nz);
368        for i in 0..nx {
369            for j in 0..ny {
370                for k in 0..nz {
371                    positions.push(
372                        center - half_extents
373                            + Vector::new(
374                                step.x * i as Real,
375                                step.y * j as Real,
376                                step.z * k as Real,
377                            ),
378                    );
379                }
380            }
381        }
382        // Five tetrahedra per cube, split parity alternating so neighboring cubes share their
383        // face diagonals.
384        let mut cells = Vec::new();
385        for i in 0..nx - 1 {
386            for j in 0..ny - 1 {
387                for k in 0..nz - 1 {
388                    // Corner `c` has bits (x, y, z) = (c & 1, c >> 1 & 1, c >> 2 & 1).
389                    let v: [u32; 8] = core::array::from_fn(|c| {
390                        idx(i + (c & 1), j + ((c >> 1) & 1), k + ((c >> 2) & 1))
391                    });
392                    let tets: [[usize; 4]; 5] = if (i + j + k) % 2 == 0 {
393                        [
394                            [0, 3, 5, 6],
395                            [1, 0, 3, 5],
396                            [2, 0, 3, 6],
397                            [4, 0, 5, 6],
398                            [7, 3, 5, 6],
399                        ]
400                    } else {
401                        [
402                            [1, 2, 4, 7],
403                            [0, 1, 2, 4],
404                            [3, 1, 2, 7],
405                            [5, 1, 4, 7],
406                            [6, 2, 4, 7],
407                        ]
408                    };
409                    for t in tets {
410                        cells.push([v[t[0]], v[t[1]], v[t[2]], v[t[3]]]);
411                    }
412                }
413            }
414        }
415        let radius = step.x.min(step.y).min(step.z) * 0.5;
416        Self::new(positions).cells(cells).particle_radius(radius)
417    }
418
419    /// A volumetric body filling a closed, outward-oriented mesh (triangles in 3D, segments in 2D)
420    /// with cells of size `cell_size`; the surface is their boundary. Particle radius defaults to
421    /// half the mean cell edge; `None` if the mesh is empty, open or encloses nothing at that size.
422    // Filling a 2D boundary goes through parry's Delaunay refinement, which rapier2d
423    // enables with `std` (see the crate's feature list).
424    #[cfg(any(feature = "dim3", feature = "std"))]
425    pub fn volumetric(
426        vertices: &[Vector],
427        indices: &[[u32; DIM]],
428        cell_size: Real,
429    ) -> Option<Self> {
430        Self::volumetric_with(
431            vertices,
432            indices,
433            &parry::transformation::VolumeMeshParameters::new(cell_size),
434        )
435    }
436
437    /// The same as [`Self::volumetric`], with the meshing parameters spelled out: element size,
438    /// the cover's smoothing, guard and subdivision, and whether the surface alone is covered
439    /// (the crust).
440    // Filling a 2D boundary goes through parry's Delaunay refinement, which rapier2d
441    // enables with `std` (see the crate's feature list).
442    #[cfg(any(feature = "dim3", feature = "std"))]
443    pub fn volumetric_with(
444        vertices: &[Vector],
445        indices: &[[u32; DIM]],
446        params: &parry::transformation::VolumeMeshParameters,
447    ) -> Option<Self> {
448        let mut mesh = parry::transformation::volume_mesh(vertices, indices, params)?;
449        let components = mesh.connected_components();
450        let count = components.iter().max().map(|id| *id as usize + 1)?;
451        if count > 1 {
452            drop_stray_pieces(&mut mesh, &components, count);
453        }
454        Self::from_cells(mesh)
455    }
456
457    /// A body of the cells of a filled mesh, with the particle radius the mesh asks for.
458    #[cfg(any(feature = "dim3", feature = "std"))]
459    fn from_cells(mesh: parry::transformation::VolumeMesh) -> Option<Self> {
460        let mut builder = Self::new(mesh.vertices).cells(mesh.cells);
461        // Half the mean edge of the *boundary*: with a subdivided cover the interior edges are
462        // longer than the boundary's, and it is the boundary the radius is a thickness for.
463        let mut counts: HashMap<[u32; DIM], u32> = HashMap::default();
464        for cell in &builder.cells {
465            for face in cell_faces(*cell) {
466                let mut key = face;
467                key.sort_unstable();
468                *counts.entry(key).or_insert(0) += 1;
469            }
470        }
471        let mut seen: HashMap<[u32; 2], ()> = HashMap::default();
472        let mut edges = Vec::new();
473        for (face, count) in &counts {
474            if *count != 1 {
475                continue;
476            }
477            for k in 0..DIM {
478                let (a, b) = (face[k], face[(k + 1) % DIM]);
479                let key = [a.min(b), a.max(b)];
480                if seen.insert(key, ()).is_none() {
481                    edges.push(key);
482                }
483            }
484        }
485        let radius = mean_edge_length(&builder.positions, &edges) * 0.5;
486        builder = builder.particle_radius(radius);
487        Some(builder)
488    }
489
490    /// The same as [`Self::volumetric`], keeping the boundary mesh as the body's skin: the cells
491    /// hold it and the body is drawn as it, so thin features survive a cell size that cannot
492    /// resolve them; it still collides through its cells, so contacts are as coarse as they are.
493    // Filling a 2D boundary goes through parry's Delaunay refinement, which rapier2d
494    // enables with `std` (see the crate's feature list).
495    #[cfg(any(feature = "dim3", feature = "std"))]
496    pub fn volumetric_skinned(
497        vertices: &[Vector],
498        indices: &[[u32; DIM]],
499        cell_size: Real,
500    ) -> Option<Self> {
501        let builder = Self::volumetric(vertices, indices, cell_size)?;
502        Some(builder.skin(vertices.to_vec(), indices.to_vec()))
503    }
504
505    /// A hollow sphere (3D only): an icosphere surface with `subdivisions` refinement levels,
506    /// structural edges along the triangle edges, dihedral bending constraints across them, and
507    /// volume preservation. The particle radius defaults to half the mean edge length.
508    #[cfg(feature = "dim3")]
509    pub fn sphere(center: Vector, radius: Real, subdivisions: usize) -> Self {
510        let (positions, triangles) = icosphere(center, radius, subdivisions);
511        let mut builder = Self::new(positions).surface(triangles);
512        let dihedrals = builder.surface_dihedrals();
513        let edges = builder.surface_edges();
514        let mean_edge = edges
515            .iter()
516            .map(|e| (builder.positions[e[0] as usize] - builder.positions[e[1] as usize]).length())
517            .sum::<Real>()
518            / edges.len().max(1) as Real;
519        builder = builder
520            .edges(edges)
521            .dihedrals(dihedrals)
522            .volume_preservation(true)
523            .particle_radius(mean_edge * 0.5);
524        builder
525    }
526
527    /// A closed polygon (counter-clockwise `points`): structural edges along the boundary,
528    /// bending edges between second neighbors, and area preservation (2D only).
529    ///
530    /// The particle radius defaults to half the mean edge length.
531    #[cfg(feature = "dim2")]
532    pub fn polygon(points: Vec<Vector>) -> Self {
533        let n = points.len().max(3) as u32;
534        let edges: Vec<[u32; 2]> = (0..n).map(|i| [i, (i + 1) % n]).collect();
535        let bend_edges: Vec<[u32; 2]> = if n > 4 {
536            (0..n).map(|i| [i, (i + 2) % n]).collect()
537        } else {
538            Vec::new()
539        };
540        let surface: Vec<[u32; DIM]> = edges.clone();
541        let mean_edge = edges
542            .iter()
543            .map(|e| (points[e[0] as usize] - points[e[1] as usize]).length())
544            .sum::<Real>()
545            / n as Real;
546        Self::new(points)
547            .edges(edges)
548            .bend_edges(bend_edges)
549            .surface(surface)
550            .volume_preservation(true)
551            .particle_radius(mean_edge * 0.5)
552    }
553
554    /// A regular polygon with `num_particles` boundary particles (see [`Self::polygon`], 2D only).
555    #[cfg(feature = "dim2")]
556    pub fn disk(center: Vector, radius: Real, num_particles: usize) -> Self {
557        let n = num_particles.max(3);
558        let points = (0..n)
559            .map(|i| {
560                let angle = i as Real / n as Real * Real::simd_two_pi();
561                center + Vector::new(angle.cos(), angle.sin()) * radius
562            })
563            .collect();
564        Self::polygon(points)
565    }
566
567    /// A solid rectangle triangulated on a `nx × ny` particle grid (2D only), with structural
568    /// edges along the triangle edges and the boundary segments as surface.
569    ///
570    /// The particle radius defaults to half the smallest cell size.
571    #[cfg(feature = "dim2")]
572    pub fn grid(center: Vector, half_extents: Vector, nx: usize, ny: usize) -> Self {
573        let (nx, ny) = (nx.max(2), ny.max(2));
574        let step = Vector::new(
575            2.0 * half_extents.x / (nx - 1) as Real,
576            2.0 * half_extents.y / (ny - 1) as Real,
577        );
578        let idx = |i: usize, j: usize| (i * ny + j) as u32;
579        let mut positions = Vec::with_capacity(nx * ny);
580        for i in 0..nx {
581            for j in 0..ny {
582                positions.push(
583                    center - half_extents + Vector::new(step.x * i as Real, step.y * j as Real),
584                );
585            }
586        }
587        let mut cells = Vec::new();
588        for i in 0..nx - 1 {
589            for j in 0..ny - 1 {
590                let (a, b, c, d) = (idx(i, j), idx(i + 1, j), idx(i + 1, j + 1), idx(i, j + 1));
591                if (i + j) % 2 == 0 {
592                    cells.push([a, b, c]);
593                    cells.push([a, c, d]);
594                } else {
595                    cells.push([a, b, d]);
596                    cells.push([b, c, d]);
597                }
598            }
599        }
600        let radius = step.x.min(step.y) * 0.5;
601        Self::new(positions).cells(cells).particle_radius(radius)
602    }
603}