Skip to main content

rapier3d/dynamics/soft_body/
soft_body_elements.rs

1//! The element types of a soft body: particles, edges, dihedrals, cells, and the per-body solver and cell-model choices.
2#[cfg(doc)]
3use super::SoftBodyMaterial;
4#[cfg(feature = "serde-serialize")]
5use super::soft_body_material::one;
6use crate::dynamics::SpringCoefficients;
7use crate::math::{DIM, Matrix, Real, Rotation, Vector};
8#[cfg(not(feature = "std"))]
9#[allow(unused_imports)]
10use simba::scalar::{ComplexField as _, RealField as _};
11
12/// One particle (point mass) of a soft body: plain state owned by its
13/// [`crate::dynamics::SoftBody`], given its own solver body for the step when the body is awake.
14/// A pinned particle (`inv_mass == 0`) is kinematic: it moves at its velocity or toward its target.
15#[derive(Copy, Clone, Debug)]
16#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
17pub struct SoftBodyParticle {
18    /// World-space position.
19    pub(crate) position: Vector,
20    /// World-space velocity.
21    #[cfg_attr(feature = "serde-serialize", serde(default))]
22    pub(crate) velocity: Vector,
23    /// Position in the body's rest shape, relative to the rest center of mass (the
24    /// shape-matching goal shape). Follows the plastic flow of the elements (see
25    /// [`Self::initial_rest_position`]).
26    pub(crate) rest_position: Vector,
27    /// The rest position before any plastic flow: the world-space position at insertion,
28    /// relative to the rest center of mass. The cells' initial rest shapes are read from it.
29    pub(crate) initial_rest_position: Vector,
30    /// Nominal mass (used by shape matching even for pinned particles).
31    pub(crate) mass: Real,
32    /// `0.0` for pinned particles.
33    pub(crate) inv_mass: Real,
34    /// User force applied to the particle every step until reset (`SoftBody::reset_forces`).
35    #[cfg_attr(feature = "serde-serialize", serde(default))]
36    pub(crate) force: Vector,
37    /// Where a pinned particle must be at the end of the next step (`None`: it keeps moving at
38    /// its velocity).
39    #[cfg_attr(feature = "serde-serialize", serde(default))]
40    pub(crate) next_position: Option<Vector>,
41    /// Set once an element touching the particle tore (see [`Self::is_damaged`]).
42    #[cfg_attr(feature = "serde-serialize", serde(default))]
43    pub(crate) damaged: bool,
44    /// Whether some surface element has this particle as a vertex (updated with the boundary).
45    #[cfg_attr(feature = "serde-serialize", serde(default))]
46    pub(crate) on_surface: bool,
47    /// The particle this one was split from, shared by every copy of a split particle (its own
48    /// index when it was never split; `u32::MAX` also reads as its own index, see
49    /// [`Self::split_root`]).
50    #[cfg_attr(feature = "serde-serialize", serde(default = "no_split_root"))]
51    pub(crate) split_root: u32,
52}
53
54#[cfg(feature = "serde-serialize")]
55fn no_split_root() -> u32 {
56    u32::MAX
57}
58
59impl SoftBodyParticle {
60    /// The root of this particle's split family, `index` being this particle's own index: two
61    /// particles with the same root are copies of one particle a crack went through.
62    pub(crate) fn split_root(&self, index: u32) -> u32 {
63        if self.split_root == u32::MAX {
64            index
65        } else {
66            self.split_root
67        }
68    }
69
70    /// The world-space position of this particle.
71    pub fn position(&self) -> Vector {
72        self.position
73    }
74
75    /// The world-space velocity of this particle.
76    pub fn velocity(&self) -> Vector {
77        self.velocity
78    }
79
80    /// The user force currently applied to this particle (see [`crate::dynamics::SoftBody::add_particle_force`]).
81    pub fn force(&self) -> Vector {
82        self.force
83    }
84
85    /// The position a pinned particle is moving to over the next step, if one was set with
86    /// [`crate::dynamics::SoftBody::set_particle_kinematic_target`].
87    pub fn kinematic_target(&self) -> Option<Vector> {
88        self.next_position
89    }
90
91    /// The position of this particle in the rest shape (relative to the rest center of mass).
92    /// With a plastic material the rest shape follows the elements' flow (see
93    /// [`SoftBodyMaterial::plastic_yield`]); [`Self::initial_rest_position`] predates any flow.
94    pub fn rest_position(&self) -> Vector {
95        self.rest_position
96    }
97
98    /// The position of this particle in the soft body's rest shape before any plastic flow
99    /// (its position at insertion, relative to the rest center of mass).
100    pub fn initial_rest_position(&self) -> Vector {
101        self.initial_rest_position
102    }
103
104    /// The nominal mass of this particle.
105    pub fn mass(&self) -> Real {
106        self.mass
107    }
108
109    /// The inverse mass of this particle (`0.0` for pinned particles).
110    pub fn inv_mass(&self) -> Real {
111        self.inv_mass
112    }
113
114    /// Whether this particle is pinned (infinite mass, moved kinematically).
115    pub fn is_pinned(&self) -> bool {
116        self.inv_mass == 0.0
117    }
118
119    /// Whether an element touching this particle has torn (or [`crate::dynamics::SoftBody::set_particle_damaged`]
120    /// marked it). The elements around a damaged particle lose the interior strength of
121    /// [`SoftBodyMaterial::interior_strength`]: a tear runs on from where it started.
122    pub fn is_damaged(&self) -> bool {
123        self.damaged
124    }
125
126    /// Whether this particle is a vertex of the body's surface (a boundary segment in 2D, a
127    /// boundary triangle in 3D). An element whose particles are all interior gets the material's
128    /// [`SoftBodyMaterial::interior_strength`].
129    pub fn is_on_surface(&self) -> bool {
130        self.on_surface
131    }
132}
133
134/// The role of a soft-body edge (distance constraint).
135#[derive(Copy, Clone, Debug, PartialEq, Eq)]
136#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
137pub enum SoftBodyEdgeKind {
138    /// A structural edge, keeping two neighboring particles at their rest distance
139    /// (softness: [`SoftBodyMaterial::edge_softness`]).
140    Structural,
141    /// A bending edge, keeping two second-neighbor particles at their rest distance
142    /// (softness: [`SoftBodyMaterial::bend_softness`]).
143    Bend,
144}
145
146/// A distance constraint between two particles of a soft body.
147#[derive(Copy, Clone, Debug)]
148#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
149pub struct SoftBodyEdge {
150    /// The two particles.
151    pub vertices: [u32; 2],
152    /// The distance the edge tries to maintain.
153    pub rest_length: Real,
154    /// Structural or bending edge.
155    pub kind: SoftBodyEdgeKind,
156    /// If `true`, the edge only resists stretching (a rope segment), never compression.
157    pub tension_only: bool,
158    /// Softness of this edge, overriding the material's (`edge_softness` / `bend_softness`)
159    /// when set: anisotropic cloth (warp, weft and shear stiffness), stiffer seams...
160    pub softness: Option<SpringCoefficients<Real>>,
161    /// Multiplier of the material's tear thresholds for this edge (default `1.0`): a seam that
162    /// holds, a perforation that gives. Usually set through [`crate::dynamics::SoftBody::set_edge_tear_resistance`]
163    /// or [`crate::dynamics::SoftBody::set_cluster_tear_resistance`].
164    #[cfg_attr(feature = "serde-serialize", serde(default = "one"))]
165    pub tear_resistance: Real,
166    /// Accumulated impulse of the last step (warm-start state).
167    pub(crate) impulse: Real,
168    /// Accumulated plastic strain: `rest_length = initial rest length * (1 + plastic_strain)`
169    /// (see [`Self::plastic_strain`]).
170    #[cfg_attr(feature = "serde-serialize", serde(default))]
171    pub(crate) plastic_strain: Real,
172    /// Parallel solve color.
173    pub(crate) color: u8,
174    /// Set when the edge was loaded past the material's tear threshold during the last step
175    /// (or by [`crate::dynamics::SoftBody::tear_edge`]): removed by the tearing pass at the end of the step.
176    #[cfg_attr(feature = "serde-serialize", serde(skip))]
177    pub(crate) torn: bool,
178    /// The load of the edge as a fraction of its tear threshold, smoothed over the material's
179    /// `tear_smoothing` (see [`Self::stress`]).
180    #[cfg_attr(feature = "serde-serialize", serde(default))]
181    pub(crate) stress: Real,
182}
183
184impl SoftBodyEdge {
185    /// The impulse this edge applied during the last substep, along the edge direction
186    /// (positive when the edge was resisting stretching).
187    pub fn impulse(&self) -> Real {
188        self.impulse
189    }
190
191    /// Load of this edge as a fraction of its tear threshold, smoothed over
192    /// [`SoftBodyMaterial::tear_smoothing`]: `0.0` slack, `1.0` tearing. The larger of the stretch
193    /// over `tear_strain` and the force over `tear_force`; `0.0` while the material has neither.
194    pub fn stress(&self) -> Real {
195        self.stress
196    }
197}
198
199/// A dihedral bending constraint between two triangles sharing an edge (3D only).
200///
201/// `vertices[0..2]` is the shared edge, `vertices[2]` and `vertices[3]` the two opposite
202/// vertices.
203#[cfg(feature = "dim3")]
204#[derive(Copy, Clone, Debug)]
205#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
206pub struct SoftBodyDihedral {
207    /// The four particles.
208    pub vertices: [u32; 4],
209    /// The angle between the two triangle normals at rest.
210    pub rest_angle: Real,
211    /// Accumulated impulse of the last step (warm-start state).
212    pub(crate) impulse: Real,
213    /// Accumulated plastic set of the rest angle: `rest_angle = initial rest angle +
214    /// plastic_set` (see [`Self::plastic_set`]).
215    #[cfg_attr(feature = "serde-serialize", serde(default))]
216    pub(crate) plastic_set: Real,
217    /// Parallel solve color.
218    pub(crate) color: u8,
219}
220
221#[cfg(feature = "dim3")]
222impl SoftBodyDihedral {
223    /// The permanent set of this dihedral: how far its rest angle has flowed from the angle it
224    /// was created with, in radians (see [`SoftBodyMaterial::edge_plastic_yield`]).
225    pub fn plastic_set(&self) -> Real {
226        self.plastic_set
227    }
228
229    /// The rest angle this dihedral was created with (its `rest_angle` before any plastic
230    /// flow).
231    pub fn initial_rest_angle(&self) -> Real {
232        self.rest_angle - self.plastic_set
233    }
234}
235
236/// Which solver simulates a soft body's elasticity (requires the `fem` cargo feature).
237#[cfg(feature = "fem")]
238#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
239#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
240pub enum SoftBodySolver {
241    /// Every elastic element becomes constraints, warm-started and swept Gauss-Seidel by color once
242    /// per substep with the contacts and joints. Cheap and robust, but converged only as far as the
243    /// sweep count: a stiff body keeps a residual compliance, a load crosses a long body slowly.
244    #[default]
245    Constraints,
246    /// Implicit Euler elasticity: forces and tangent stiffness from the strain-energy density,
247    /// `(M + h D + h² K) Δv = b` solved over the whole body once per substep; constraints see the
248    /// body through its augmented mass. Costs a factorization per step and a solve per constraint.
249    Fem,
250}
251
252/// The constitutive model of a soft body's cells (triangles in 2D, tetrahedra in 3D).
253#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
254#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
255pub enum SoftBodyCellModel {
256    /// One constraint per cell keeping its signed area/volume at its rest value
257    /// (softness: [`SoftBodyMaterial::volume_softness`]). Cheap; combined with the edges this
258    /// gives a convincing jelly.
259    #[default]
260    Volume,
261    /// Corotational elasticity (linear elasticity in the cell's rotation-free frame) plus an exact
262    /// volume constraint, parameterized by [`SoftBodyMaterial::young_modulus`], `poisson_ratio` and
263    /// `elastic_damping_ratio`. Stable at any stiffness, recovers from inverted cells.
264    Corotational,
265    /// Stable Neo-Hookean hyperelasticity (Smith et al. 2018): the parameters (plasticity too) and
266    /// small-strain behavior of [`SoftBodyCellModel::Corotational`], but Neo-Hookean at large
267    /// strains (stiffer than linear elasticity under compression, softer under tension, inverted
268    /// cells pushed back through the collapse); 2x cost.
269    NeoHookean,
270}
271
272/// Number of warm-start impulses stored per cell (the corotational model's strain rows plus its
273/// volumetric row).
274pub(crate) const CELL_IMPULSES: usize = DIM * (DIM + 1) / 2 + 1;
275
276/// A simplex cell of a soft body: a triangle in 2D, a tetrahedron in 3D.
277#[derive(Copy, Clone, Debug)]
278#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
279pub struct SoftBodyCell {
280    /// The cell's particles, positively oriented (positive signed area/volume at rest).
281    pub vertices: [u32; DIM + 1],
282    /// The signed area (2D) or volume (3D) of the cell at rest.
283    pub rest_volume: Real,
284    /// Inverse of the rest edge matrix `[x1 - x0, x2 - x0, (x3 - x0)]`.
285    pub(crate) inv_rest_matrix: Matrix,
286    /// Accumulated plastic stretch of the rest shape (material frame, unit determinant): the rest
287    /// edge matrix is `plastic_stretch * Dm₀`, `Dm₀` the initial rest edge matrix. Bounded by the
288    /// material's `plastic_max`, which keeps flowing cells from degenerating into slivers.
289    #[cfg_attr(feature = "serde-serialize", serde(default = "identity_matrix"))]
290    pub(crate) plastic_stretch: Matrix,
291    /// Accumulated impulses of the last step (warm-start state): the volume constraint's in `[0]`, or
292    /// the corotational constraints' (strain rows in the cell frame, then volumetric).
293    pub(crate) impulses: [Real; CELL_IMPULSES],
294    /// Rotation of the polar decomposition of the cell's deformation gradient at the last step
295    /// (warm start of the corotational constraints' rotation extraction).
296    pub(crate) rotation: Rotation,
297    /// Multiplier of the material's Young modulus for this cell (default `1.0`): per-region
298    /// stiffness, usually set through [`crate::dynamics::SoftBody::set_cluster_stiffness_scale`].
299    #[cfg_attr(feature = "serde-serialize", serde(default = "one"))]
300    pub stiffness_scale: Real,
301    /// Multiplier of the material's tear strain for this cell (default `1.0`), see
302    /// [`crate::dynamics::SoftBody::set_cell_tear_resistance`].
303    #[cfg_attr(feature = "serde-serialize", serde(default = "one"))]
304    pub tear_resistance: Real,
305    /// Parallel solve color.
306    pub(crate) color: u8,
307    /// Set when the cell was strained past the material's `tear_strain` during the last step
308    /// (or by [`crate::dynamics::SoftBody::tear_cell`]): removed by the tearing pass at the end of the step.
309    #[cfg_attr(feature = "serde-serialize", serde(skip))]
310    pub(crate) torn: bool,
311    /// The tensile strain of the cell as a fraction of its tear threshold, smoothed over the
312    /// material's `tear_smoothing` (see [`Self::stress`]).
313    #[cfg_attr(feature = "serde-serialize", serde(default))]
314    pub(crate) stress: Real,
315}
316
317#[cfg(feature = "serde-serialize")]
318fn identity_matrix() -> Matrix {
319    Matrix::IDENTITY
320}
321
322impl SoftBodyCell {
323    /// The accumulated plastic stretch of this cell's rest shape (material frame, unit
324    /// determinant): its current rest shape is this stretch applied to the shape it was created
325    /// with (see [`SoftBodyMaterial::plastic_yield`]); the identity before any flow.
326    pub fn plastic_stretch(&self) -> Matrix {
327        self.plastic_stretch
328    }
329
330    /// The largest tensile strain of this cell as a fraction of its tear threshold, smoothed
331    /// over the material's [`SoftBodyMaterial::tear_smoothing`]: `0.0` slack, `1.0` tearing.
332    /// Stays `0.0` while the material has no `tear_strain` (cells only tear on their strain).
333    pub fn stress(&self) -> Real {
334        self.stress
335    }
336}