Skip to main content

gizmo_physics_soft/
soft_body.rs

1use gizmo_math::{Mat3, Vec3};
2
3/// Represents a single vertex/node in the FEM soft body mesh.
4#[derive(Debug, Clone, Copy)]
5pub struct SoftBodyNode {
6    pub position: Vec3,
7    pub velocity: Vec3,
8    pub mass: f32,
9    pub is_fixed: bool, // For pinning parts of the body (e.g. chassis mounts)
10}
11
12/// A 3D Tetrahedron (4 nodes) used as the fundamental finite element.
13#[derive(Debug, Clone, Copy)]
14pub struct Tetrahedron {
15    /// Indices into the nodes array of the parent SoftBody
16    pub node_indices: [u32; 4],
17
18    /// Rest Volume (V0) - calculated once at initialization
19    pub rest_volume: f32,
20
21    /// The inverse of the reference shape matrix (Dm^-1)
22    /// Used by the GPU to quickly calculate the Deformation Gradient (F = Ds * Dm^-1)
23    pub inv_rest_matrix: Mat3,
24}
25
26impl Tetrahedron {
27    /// Calculate the inverse rest matrix (Dm^-1) and rest volume for the tetrahedron.
28    /// Needs the 4 rest positions (x0, x1, x2, x3) of the nodes.
29    pub fn calculate_rest_data(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3) -> (Mat3, f32) {
30        // Dm = [ (p1-p0) | (p2-p0) | (p3-p0) ]
31        let e1 = p1 - p0;
32        let e2 = p2 - p0;
33        let e3 = p3 - p0;
34
35        let dm = Mat3::from_cols(e1, e2, e3);
36
37        // V = 1/6 * det(Dm)
38        let det = dm.determinant();
39        let volume = (det / 6.0).abs();
40
41        let inv_dm = dm.inverse();
42
43        (inv_dm, volume)
44    }
45}
46
47/// The main CPU-side structure for an FEM-based Soft Body.
48/// It contains the geometry and material properties needed to upload to the GPU.
49#[derive(Debug, Clone)]
50pub struct SoftBodyMesh {
51    pub nodes: Vec<SoftBodyNode>,
52    pub elements: Vec<Tetrahedron>,
53
54    // --- Material Properties (Elasto-Plasticity) ---
55    /// Young's Modulus (E) - Stiffness of the material (e.g., higher = stiffer metal)
56    pub youngs_modulus: f32,
57
58    /// Poisson's Ratio (nu) - Incompressibility (0.0 to 0.499, e.g., 0.3 for steel, 0.49 for rubber)
59    pub poissons_ratio: f32,
60
61    /// Lame's first parameter (lambda) - derived
62    pub lambda: f32,
63
64    /// Shear Modulus (mu) - derived
65    pub mu: f32,
66
67    /// Damping factor to prevent infinite oscillation
68    pub damping: f32,
69}
70gizmo_core::impl_component!(SoftBodyMesh);
71
72pub fn resolve_node_collision(
73    mut position: Vec3,
74    mut velocity: Vec3,
75    dt: f32,
76    rigid_colliders: &[(
77        gizmo_core::entity::Entity,
78        gizmo_physics_core::Transform,
79        gizmo_physics_core::Collider,
80    )],
81) -> (Vec3, Vec3, bool) {
82    let mut collided = false;
83    let ray = gizmo_physics_core::raycast::Ray::new(position, velocity.normalize_or_zero());
84    let dist = velocity.length() * dt;
85
86    if dist > 1e-5 {
87        for (_, col_trans, col) in rigid_colliders {
88            if let Some((d, n)) = gizmo_physics_core::raycast::Raycast::ray_shape(&ray, &col.shape, col_trans) {
89                if d <= dist + 0.1 {
90                    let bounce = 0.5;
91                    let friction = 0.8;
92
93                    let vn = velocity.dot(n);
94                    if vn < 0.0 {
95                        let vt = velocity - n * vn;
96                        velocity = vt * (1.0 - friction) - n * (vn * bounce);
97                    }
98
99                    position += ray.direction * (d - 0.1).max(0.0);
100                    collided = true;
101                }
102            }
103        }
104    }
105
106    (position, velocity, collided)
107}
108
109impl SoftBodyMesh {
110    pub fn new(youngs_modulus: f32, poissons_ratio: f32) -> Self {
111        // Calculate Lame Parameters
112        let mu = youngs_modulus / (2.0 * (1.0 + poissons_ratio));
113        let lambda = (youngs_modulus * poissons_ratio)
114            / ((1.0 + poissons_ratio) * (1.0 - 2.0 * poissons_ratio));
115
116        Self {
117            nodes: Vec::new(),
118            elements: Vec::new(),
119            youngs_modulus,
120            poissons_ratio,
121            lambda,
122            mu,
123            damping: 0.99,
124        }
125    }
126
127    pub fn add_node(&mut self, position: Vec3, mass: f32) -> u32 {
128        let idx = self.nodes.len() as u32;
129        self.nodes.push(SoftBodyNode {
130            position,
131            velocity: Vec3::ZERO,
132            mass,
133            is_fixed: false,
134        });
135        idx
136    }
137
138    pub fn add_element(&mut self, i0: u32, i1: u32, i2: u32, i3: u32) {
139        let p0 = self.nodes[i0 as usize].position;
140        let p1 = self.nodes[i1 as usize].position;
141        let p2 = self.nodes[i2 as usize].position;
142        let p3 = self.nodes[i3 as usize].position;
143
144        let (inv_rest_matrix, rest_volume) = Tetrahedron::calculate_rest_data(p0, p1, p2, p3);
145
146        self.elements.push(Tetrahedron {
147            node_indices: [i0, i1, i2, i3],
148            rest_volume,
149            inv_rest_matrix,
150        });
151    }
152
153    /// Advances the FEM simulation by one timestep using a Neo-Hookean hyperelastic model.
154    pub fn step(
155        &mut self,
156        dt: f32,
157        gravity: Vec3,
158        rigid_colliders: &[(
159            gizmo_core::entity::Entity,
160            gizmo_physics_core::Transform,
161            gizmo_physics_core::Collider,
162        )],
163    ) {
164        let num_nodes = self.nodes.len();
165        let mut forces: Vec<Vec3> = self.nodes.iter().map(|n| gravity * n.mass).collect();
166
167        // 1. Calculate and accumulate internal elastic forces from all tetrahedra in PARALLEL
168        use rayon::prelude::*;
169
170        let positions: Vec<Vec3> = self.nodes.iter().map(|n| n.position).collect();
171        const MIN_JACOBIAN: f32 = 0.1;
172
173        let elastic_forces = self
174            .elements
175            .par_iter()
176            .fold(
177                || vec![Vec3::ZERO; num_nodes],
178                |mut acc_forces, elem| {
179                    let i0 = elem.node_indices[0] as usize;
180                    let i1 = elem.node_indices[1] as usize;
181                    let i2 = elem.node_indices[2] as usize;
182                    let i3 = elem.node_indices[3] as usize;
183
184                    let x0 = positions[i0];
185                    let x1 = positions[i1];
186                    let x2 = positions[i2];
187                    let x3 = positions[i3];
188
189                    let ds = Mat3::from_cols(x1 - x0, x2 - x0, x3 - x0);
190                    let f = ds * elem.inv_rest_matrix;
191                    let j = f.determinant();
192
193                    if j < MIN_JACOBIAN {
194                        return acc_forces;
195                    }
196
197                    let f_inv_t = f.inverse().transpose();
198                    let ln_j = j.ln();
199
200                    let p = f_inv_t * (-self.mu) + f * self.mu + f_inv_t * (self.lambda * ln_j);
201                    let h = p * elem.inv_rest_matrix.transpose() * elem.rest_volume;
202
203                    let f1 = -h.col(0);
204                    let f2 = -h.col(1);
205                    let f3 = -h.col(2);
206                    let f0 = -(f1 + f2 + f3);
207
208                    acc_forces[i0] += f0;
209                    acc_forces[i1] += f1;
210                    acc_forces[i2] += f2;
211                    acc_forces[i3] += f3;
212
213                    acc_forces
214                },
215            )
216            .reduce(
217                || vec![Vec3::ZERO; num_nodes],
218                |mut a, b| {
219                    for i in 0..num_nodes {
220                        a[i] += b[i];
221                    }
222                    a
223                },
224            );
225
226        for i in 0..num_nodes {
227            forces[i] += elastic_forces[i];
228        }
229
230        // 2. Integrate velocities and positions
231        for (i, node) in self.nodes.iter_mut().enumerate() {
232            if node.is_fixed {
233                continue;
234            }
235
236            // Explicit Euler Integration
237            let acceleration = forces[i] / node.mass;
238            node.velocity += acceleration * dt;
239
240            // Apply damping normalized by dt
241            node.velocity *= self.damping.powf(dt);
242
243            let next_pos = node.position + node.velocity * dt;
244
245            let (new_pos, new_vel, collided) =
246                resolve_node_collision(node.position, node.velocity, dt, rigid_colliders);
247
248            if collided {
249                node.position = new_pos;
250                node.velocity = new_vel;
251            } else {
252                node.position = next_pos;
253            }
254        }
255    }
256}