Skip to main content

embedded_3dgfx/
softbody.rs

1//! Soft body physics using mass-spring systems and position-based dynamics.
2//!
3//! Provides deformable objects like cloth, jelly, and soft bodies with:
4//! - Particle systems with constraints
5//! - Spring-damper networks
6//! - Collision detection and response
7//! - Pressure/volume preservation
8//! - Integration with rigid body physics
9//!
10//! # Example
11//! ```
12//! use embedded_3dgfx::softbody::{SoftBody, Particle};
13//! use nalgebra::Vector3;
14//!
15//! let mut soft_body = SoftBody::<64, 128>::new();
16//!
17//! // Create a simple 2x2 cloth grid
18//! for y in 0..2 {
19//!     for x in 0..2 {
20//!         let pos = Vector3::new(x as f32, y as f32, 0.0);
21//!         soft_body.add_particle(Particle::new(pos, 1.0)).unwrap();
22//!     }
23//! }
24//!
25//! // Add springs between particles
26//! soft_body.add_spring(0, 1, 1.0, 100.0, 0.5).unwrap(); // Horizontal spring
27//! soft_body.add_spring(0, 2, 1.0, 100.0, 0.5).unwrap(); // Vertical spring
28//!
29//! // Pin top corners
30//! soft_body.get_particle_mut(0).unwrap().pinned = true;
31//! soft_body.get_particle_mut(1).unwrap().pinned = true;
32//!
33//! // Simulate
34//! soft_body.step(0.016);
35//! ```
36
37use heapless::Vec;
38use nalgebra::Vector3;
39
40#[cfg(not(feature = "std"))]
41use micromath::F32Ext;
42
43/// A particle in a soft body system.
44///
45/// Particles have mass, position, velocity, and can be pinned in place.
46#[derive(Debug, Clone)]
47pub struct Particle {
48    /// Current position
49    pub position: Vector3<f32>,
50
51    /// Previous position (for Verlet integration)
52    pub previous_position: Vector3<f32>,
53
54    /// Current velocity
55    pub velocity: Vector3<f32>,
56
57    /// Mass of the particle
58    pub mass: f32,
59
60    /// Inverse mass (0.0 for infinite mass/pinned particles)
61    pub inv_mass: f32,
62
63    /// If true, particle won't move (infinite mass)
64    pub pinned: bool,
65
66    /// Accumulated forces for this timestep
67    pub force: Vector3<f32>,
68
69    /// Radius for collision detection
70    pub radius: f32,
71}
72
73impl Particle {
74    /// Create a new particle at the given position with the given mass.
75    pub fn new(position: Vector3<f32>, mass: f32) -> Self {
76        Self {
77            position,
78            previous_position: position,
79            velocity: Vector3::zeros(),
80            mass,
81            inv_mass: if mass > 0.0 { 1.0 / mass } else { 0.0 },
82            pinned: false,
83            force: Vector3::zeros(),
84            radius: 0.1,
85        }
86    }
87
88    /// Create a pinned particle (infinite mass, won't move).
89    pub fn new_pinned(position: Vector3<f32>) -> Self {
90        Self {
91            position,
92            previous_position: position,
93            velocity: Vector3::zeros(),
94            mass: 0.0,
95            inv_mass: 0.0,
96            pinned: true,
97            force: Vector3::zeros(),
98            radius: 0.1,
99        }
100    }
101
102    /// Apply a force to this particle.
103    pub fn apply_force(&mut self, force: Vector3<f32>) {
104        if !self.pinned {
105            self.force += force;
106        }
107    }
108
109    /// Set the particle's radius for collision detection.
110    pub fn with_radius(mut self, radius: f32) -> Self {
111        self.radius = radius;
112        self
113    }
114}
115
116/// A spring constraint between two particles.
117///
118/// Springs create restoring forces based on distance deviation from rest length.
119#[derive(Debug, Clone, Copy)]
120pub struct Spring {
121    /// Index of first particle
122    pub particle_a: usize,
123
124    /// Index of second particle
125    pub particle_b: usize,
126
127    /// Rest length of the spring
128    pub rest_length: f32,
129
130    /// Spring stiffness (higher = stiffer)
131    pub stiffness: f32,
132
133    /// Damping coefficient (higher = more damping)
134    pub damping: f32,
135
136    /// Whether this spring is enabled
137    pub enabled: bool,
138}
139
140impl Spring {
141    /// Create a new spring between two particles.
142    ///
143    /// # Arguments
144    /// * `particle_a` - Index of first particle
145    /// * `particle_b` - Index of second particle
146    /// * `rest_length` - Natural length of the spring
147    /// * `stiffness` - Spring constant (force per unit extension)
148    /// * `damping` - Damping coefficient (velocity damping)
149    pub fn new(
150        particle_a: usize,
151        particle_b: usize,
152        rest_length: f32,
153        stiffness: f32,
154        damping: f32,
155    ) -> Self {
156        Self {
157            particle_a,
158            particle_b,
159            rest_length,
160            stiffness,
161            damping,
162            enabled: true,
163        }
164    }
165
166    /// Compute spring force on particle A (force on B is negated).
167    pub fn compute_force(
168        &self,
169        pos_a: Vector3<f32>,
170        vel_a: Vector3<f32>,
171        pos_b: Vector3<f32>,
172        vel_b: Vector3<f32>,
173    ) -> Vector3<f32> {
174        if !self.enabled {
175            return Vector3::zeros();
176        }
177
178        let delta = pos_b - pos_a;
179        let distance = delta.norm();
180
181        if distance < 0.0001 {
182            return Vector3::zeros();
183        }
184
185        let direction = delta / distance;
186
187        // Spring force: F = -k * (x - x0)
188        let extension = distance - self.rest_length;
189        let spring_force = direction * (self.stiffness * extension);
190
191        // Damping force: F = -c * v_rel
192        let relative_velocity = vel_b - vel_a;
193        let damping_force = direction * (self.damping * relative_velocity.dot(&direction));
194
195        spring_force + damping_force
196    }
197}
198
199/// Configuration for soft body pressure/volume preservation.
200///
201/// Helps maintain the volume of enclosed soft bodies (like balloons or jelly).
202#[derive(Debug, Clone, Copy)]
203pub struct PressureConfig {
204    /// Target volume to maintain
205    pub target_volume: f32,
206
207    /// Pressure coefficient (higher = stronger volume preservation)
208    pub pressure_coefficient: f32,
209
210    /// Whether pressure is enabled
211    pub enabled: bool,
212}
213
214impl PressureConfig {
215    /// Create a new pressure configuration.
216    pub fn new(target_volume: f32, pressure_coefficient: f32) -> Self {
217        Self {
218            target_volume,
219            pressure_coefficient,
220            enabled: true,
221        }
222    }
223}
224
225impl Default for PressureConfig {
226    fn default() -> Self {
227        Self {
228            target_volume: 1.0,
229            pressure_coefficient: 1.0,
230            enabled: false,
231        }
232    }
233}
234
235/// A soft body made of particles connected by springs.
236///
237/// Generic parameters:
238/// * `P` - Maximum number of particles
239/// * `S` - Maximum number of springs
240#[derive(Debug, Clone)]
241pub struct SoftBody<const P: usize, const S: usize> {
242    /// Particles in the soft body
243    pub particles: Vec<Particle, P>,
244
245    /// Spring constraints between particles
246    pub springs: Vec<Spring, S>,
247
248    /// Gravity acceleration applied to all particles
249    pub gravity: Vector3<f32>,
250
251    /// Global damping factor (0.0 = no damping, 1.0 = full damping)
252    pub damping: f32,
253
254    /// Pressure/volume preservation configuration
255    pub pressure_config: PressureConfig,
256
257    /// Ground plane height (particles bounce off this)
258    pub ground_plane: Option<f32>,
259
260    /// Ground restitution (bounciness of ground collisions)
261    pub ground_restitution: f32,
262
263    /// Ground friction coefficient
264    pub ground_friction: f32,
265}
266
267impl<const P: usize, const S: usize> SoftBody<P, S> {
268    /// Create a new empty soft body.
269    pub fn new() -> Self {
270        Self {
271            particles: Vec::new(),
272            springs: Vec::new(),
273            gravity: Vector3::new(0.0, -9.81, 0.0),
274            damping: 0.99,
275            pressure_config: PressureConfig::default(),
276            ground_plane: Some(0.0),
277            ground_restitution: 0.3,
278            ground_friction: 0.5,
279        }
280    }
281
282    /// Add a particle to the soft body.
283    pub fn add_particle(&mut self, particle: Particle) -> Result<usize, ()> {
284        let id = self.particles.len();
285        self.particles.push(particle).map_err(|_| ())?;
286        Ok(id)
287    }
288
289    /// Add a spring constraint between two particles.
290    pub fn add_spring(
291        &mut self,
292        particle_a: usize,
293        particle_b: usize,
294        rest_length: f32,
295        stiffness: f32,
296        damping: f32,
297    ) -> Result<(), ()> {
298        let spring = Spring::new(particle_a, particle_b, rest_length, stiffness, damping);
299        self.springs.push(spring).map_err(|_| ())
300    }
301
302    /// Get a particle by index.
303    pub fn get_particle(&self, index: usize) -> Option<&Particle> {
304        self.particles.get(index)
305    }
306
307    /// Get a mutable reference to a particle by index.
308    pub fn get_particle_mut(&mut self, index: usize) -> Option<&mut Particle> {
309        self.particles.get_mut(index)
310    }
311
312    /// Set the gravity vector for all particles.
313    pub fn set_gravity(&mut self, gravity: Vector3<f32>) {
314        self.gravity = gravity;
315    }
316
317    /// Advance the soft body simulation by one timestep.
318    ///
319    /// Uses semi-implicit Euler integration with spring forces.
320    pub fn step(&mut self, dt: f32) {
321        if dt <= 0.0 {
322            return;
323        }
324
325        // Apply gravity to all particles
326        for particle in self.particles.iter_mut() {
327            if !particle.pinned {
328                particle.apply_force(self.gravity * particle.mass);
329            }
330        }
331
332        // Compute and apply spring forces
333        for i in 0..self.springs.len() {
334            let spring = &self.springs[i];
335            if !spring.enabled {
336                continue;
337            }
338
339            let (idx_a, idx_b) = (spring.particle_a, spring.particle_b);
340            if idx_a >= self.particles.len() || idx_b >= self.particles.len() {
341                continue;
342            }
343
344            // Get particle data (must do this carefully to avoid borrow checker issues)
345            let (pos_a, vel_a, pos_b, vel_b) = {
346                let pa = &self.particles[idx_a];
347                let pb = &self.particles[idx_b];
348                (pa.position, pa.velocity, pb.position, pb.velocity)
349            };
350
351            let force = spring.compute_force(pos_a, vel_a, pos_b, vel_b);
352
353            // Apply forces
354            self.particles[idx_a].apply_force(force);
355            self.particles[idx_b].apply_force(-force);
356        }
357
358        // Apply pressure forces if enabled
359        if self.pressure_config.enabled {
360            self.apply_pressure_forces();
361        }
362
363        // Integrate particle motion
364        for particle in self.particles.iter_mut() {
365            if particle.pinned {
366                particle.force = Vector3::zeros();
367                continue;
368            }
369
370            // Semi-implicit Euler: v' = v + a*dt, x' = x + v'*dt
371            let acceleration = particle.force * particle.inv_mass;
372            particle.velocity += acceleration * dt;
373
374            // Apply damping
375            particle.velocity *= self.damping;
376
377            // Update position
378            particle.previous_position = particle.position;
379            particle.position += particle.velocity * dt;
380
381            // Clear forces
382            particle.force = Vector3::zeros();
383        }
384
385        // Apply constraints (ground collision, etc.)
386        self.apply_constraints();
387    }
388
389    /// Apply ground plane collisions.
390    fn apply_constraints(&mut self) {
391        if let Some(ground_y) = self.ground_plane {
392            for particle in self.particles.iter_mut() {
393                if particle.pinned {
394                    continue;
395                }
396
397                // Check if particle is below ground
398                if particle.position.y - particle.radius < ground_y {
399                    // Position correction
400                    particle.position.y = ground_y + particle.radius;
401
402                    // Velocity response with restitution
403                    if particle.velocity.y < 0.0 {
404                        particle.velocity.y *= -self.ground_restitution;
405                    }
406
407                    // Apply friction to horizontal velocity
408                    let horizontal_vel =
409                        Vector3::new(particle.velocity.x, 0.0, particle.velocity.z);
410                    let friction_impulse = horizontal_vel * -self.ground_friction;
411                    particle.velocity.x += friction_impulse.x;
412                    particle.velocity.z += friction_impulse.z;
413                }
414            }
415        }
416    }
417
418    /// Apply pressure forces to maintain volume (for enclosed soft bodies).
419    fn apply_pressure_forces(&mut self) {
420        // Simple pressure model: push particles away from center of mass
421        if self.particles.is_empty() {
422            return;
423        }
424
425        // Compute center of mass
426        let mut center = Vector3::zeros();
427        let mut total_mass = 0.0;
428
429        for particle in self.particles.iter() {
430            center += particle.position * particle.mass;
431            total_mass += particle.mass;
432        }
433
434        if total_mass > 0.0 {
435            center /= total_mass;
436        }
437
438        // Apply radial pressure forces
439        for particle in self.particles.iter_mut() {
440            if particle.pinned {
441                continue;
442            }
443
444            let to_particle = particle.position - center;
445            let distance = to_particle.norm();
446
447            if distance > 0.001 {
448                let direction = to_particle / distance;
449                let pressure_force = direction * self.pressure_config.pressure_coefficient;
450                particle.apply_force(pressure_force);
451            }
452        }
453    }
454
455    /// Get vertex positions for rendering.
456    ///
457    /// Converts particle positions to vertex array format.
458    pub fn get_vertex_positions(&self, output: &mut [[f32; 3]]) -> usize {
459        let count = self.particles.len().min(output.len());
460
461        for i in 0..count {
462            let pos = self.particles[i].position;
463            output[i] = [pos.x, pos.y, pos.z];
464        }
465
466        count
467    }
468
469    /// Reset all forces on particles.
470    pub fn clear_forces(&mut self) {
471        for particle in self.particles.iter_mut() {
472            particle.force = Vector3::zeros();
473        }
474    }
475
476    /// Apply an external force to all particles.
477    pub fn apply_global_force(&mut self, force: Vector3<f32>) {
478        for particle in self.particles.iter_mut() {
479            particle.apply_force(force);
480        }
481    }
482}
483
484impl<const P: usize, const S: usize> Default for SoftBody<P, S> {
485    fn default() -> Self {
486        Self::new()
487    }
488}
489
490/// Helper functions for creating common soft body shapes.
491impl<const P: usize, const S: usize> SoftBody<P, S> {
492    /// Create a cloth grid suspended from the top edge.
493    ///
494    /// # Arguments
495    /// * `width` - Number of particles along X axis
496    /// * `height` - Number of particles along Y axis
497    /// * `spacing` - Distance between particles
498    /// * `stiffness` - Spring stiffness
499    /// * `damping` - Spring damping
500    ///
501    /// Returns the soft body with structural and shear springs, or an error if capacity exceeded.
502    pub fn create_cloth(
503        width: usize,
504        height: usize,
505        spacing: f32,
506        stiffness: f32,
507        damping: f32,
508    ) -> Result<Self, ()> {
509        let mut soft_body = Self::new();
510
511        // Create particle grid
512        for y in 0..height {
513            for x in 0..width {
514                let position = Vector3::new(x as f32 * spacing, -(y as f32 * spacing), 0.0);
515                let particle = Particle::new(position, 1.0);
516                soft_body.add_particle(particle)?;
517            }
518        }
519
520        // Pin top row
521        for x in 0..width {
522            let idx = x;
523            if let Some(p) = soft_body.get_particle_mut(idx) {
524                p.pinned = true;
525            }
526        }
527
528        // Add structural springs (horizontal and vertical)
529        for y in 0..height {
530            for x in 0..width {
531                let idx = y * width + x;
532
533                // Horizontal spring
534                if x < width - 1 {
535                    soft_body.add_spring(idx, idx + 1, spacing, stiffness, damping)?;
536                }
537
538                // Vertical spring
539                if y < height - 1 {
540                    soft_body.add_spring(idx, idx + width, spacing, stiffness, damping)?;
541                }
542            }
543        }
544
545        // Add shear springs (diagonals for stability)
546        for y in 0..height - 1 {
547            for x in 0..width - 1 {
548                let idx = y * width + x;
549                let diagonal_length = spacing * 1.414; // sqrt(2)
550
551                // Diagonal springs
552                soft_body.add_spring(
553                    idx,
554                    idx + width + 1,
555                    diagonal_length,
556                    stiffness * 0.5,
557                    damping,
558                )?;
559                soft_body.add_spring(
560                    idx + 1,
561                    idx + width,
562                    diagonal_length,
563                    stiffness * 0.5,
564                    damping,
565                )?;
566            }
567        }
568
569        Ok(soft_body)
570    }
571
572    /// Create a soft cube/jelly block.
573    ///
574    /// # Arguments
575    /// * `size` - Number of particles along each axis
576    /// * `spacing` - Distance between particles
577    /// * `stiffness` - Spring stiffness
578    /// * `damping` - Spring damping
579    pub fn create_jelly_cube(
580        size: usize,
581        spacing: f32,
582        stiffness: f32,
583        damping: f32,
584    ) -> Result<Self, ()> {
585        let mut soft_body = Self::new();
586
587        // Create 3D grid of particles
588        for z in 0..size {
589            for y in 0..size {
590                for x in 0..size {
591                    let position = Vector3::new(
592                        x as f32 * spacing - (size as f32 * spacing) / 2.0,
593                        y as f32 * spacing,
594                        z as f32 * spacing - (size as f32 * spacing) / 2.0,
595                    );
596                    soft_body.add_particle(Particle::new(position, 1.0))?;
597                }
598            }
599        }
600
601        // Add springs between neighboring particles
602        for z in 0..size {
603            for y in 0..size {
604                for x in 0..size {
605                    let idx = z * size * size + y * size + x;
606
607                    // X-axis springs
608                    if x < size - 1 {
609                        soft_body.add_spring(idx, idx + 1, spacing, stiffness, damping)?;
610                    }
611
612                    // Y-axis springs
613                    if y < size - 1 {
614                        soft_body.add_spring(idx, idx + size, spacing, stiffness, damping)?;
615                    }
616
617                    // Z-axis springs
618                    if z < size - 1 {
619                        soft_body.add_spring(
620                            idx,
621                            idx + size * size,
622                            spacing,
623                            stiffness,
624                            damping,
625                        )?;
626                    }
627                }
628            }
629        }
630
631        // Enable pressure to maintain volume
632        soft_body.pressure_config =
633            PressureConfig::new((size as f32 * spacing).powi(3), stiffness * 0.1);
634
635        Ok(soft_body)
636    }
637
638    /// Create a soft sphere/ball.
639    ///
640    /// Creates a geodesic sphere approximation with springs.
641    pub fn create_soft_sphere(
642        radius: f32,
643        _subdivisions: usize,
644        stiffness: f32,
645        damping: f32,
646    ) -> Result<Self, ()> {
647        let mut soft_body = Self::new();
648
649        // Create icosphere vertices
650        let t = (1.0 + 5.0_f32.sqrt()) / 2.0;
651
652        // Initial 12 vertices of icosahedron
653        let initial_verts = [
654            Vector3::new(-1.0, t, 0.0).normalize() * radius,
655            Vector3::new(1.0, t, 0.0).normalize() * radius,
656            Vector3::new(-1.0, -t, 0.0).normalize() * radius,
657            Vector3::new(1.0, -t, 0.0).normalize() * radius,
658            Vector3::new(0.0, -1.0, t).normalize() * radius,
659            Vector3::new(0.0, 1.0, t).normalize() * radius,
660            Vector3::new(0.0, -1.0, -t).normalize() * radius,
661            Vector3::new(0.0, 1.0, -t).normalize() * radius,
662            Vector3::new(t, 0.0, -1.0).normalize() * radius,
663            Vector3::new(t, 0.0, 1.0).normalize() * radius,
664            Vector3::new(-t, 0.0, -1.0).normalize() * radius,
665            Vector3::new(-t, 0.0, 1.0).normalize() * radius,
666        ];
667
668        for vert in initial_verts.iter() {
669            soft_body.add_particle(Particle::new(*vert, 1.0).with_radius(0.05))?;
670        }
671
672        // Add springs between connected vertices (simplified - full icosphere would need proper subdivision)
673        // For now, just connect nearby particles
674        for i in 0..soft_body.particles.len() {
675            for j in i + 1..soft_body.particles.len() {
676                let dist =
677                    (soft_body.particles[i].position - soft_body.particles[j].position).norm();
678                if dist < radius * 1.5 {
679                    soft_body.add_spring(i, j, dist, stiffness, damping)?;
680                }
681            }
682        }
683
684        // Enable pressure to maintain spherical shape
685        soft_body.pressure_config = PressureConfig::new(
686            4.0 / 3.0 * core::f32::consts::PI * radius.powi(3),
687            stiffness * 0.5,
688        );
689
690        Ok(soft_body)
691    }
692}
693
694#[cfg(test)]
695mod tests {
696    use super::*;
697
698    #[test]
699    fn test_particle_creation() {
700        let particle = Particle::new(Vector3::new(1.0, 2.0, 3.0), 5.0);
701        assert_eq!(particle.position, Vector3::new(1.0, 2.0, 3.0));
702        assert_eq!(particle.mass, 5.0);
703        assert!((particle.inv_mass - 0.2).abs() < 0.001);
704        assert!(!particle.pinned);
705    }
706
707    #[test]
708    fn test_particle_pinned() {
709        let particle = Particle::new_pinned(Vector3::zeros());
710        assert!(particle.pinned);
711        assert_eq!(particle.inv_mass, 0.0);
712    }
713
714    #[test]
715    fn test_spring_creation() {
716        let spring = Spring::new(0, 1, 1.0, 100.0, 0.5);
717        assert_eq!(spring.particle_a, 0);
718        assert_eq!(spring.particle_b, 1);
719        assert_eq!(spring.rest_length, 1.0);
720        assert_eq!(spring.stiffness, 100.0);
721        assert!(spring.enabled);
722    }
723
724    #[test]
725    fn test_softbody_creation() {
726        let soft_body = SoftBody::<16, 32>::new();
727        assert_eq!(soft_body.particles.len(), 0);
728        assert_eq!(soft_body.springs.len(), 0);
729        assert_eq!(soft_body.gravity, Vector3::new(0.0, -9.81, 0.0));
730    }
731
732    #[test]
733    fn test_softbody_add_particle() {
734        let mut soft_body = SoftBody::<16, 32>::new();
735        let particle = Particle::new(Vector3::new(1.0, 2.0, 3.0), 1.0);
736
737        let result = soft_body.add_particle(particle);
738        assert!(result.is_ok());
739        assert_eq!(soft_body.particles.len(), 1);
740    }
741
742    #[test]
743    fn test_softbody_add_spring() {
744        let mut soft_body = SoftBody::<16, 32>::new();
745
746        soft_body
747            .add_particle(Particle::new(Vector3::zeros(), 1.0))
748            .unwrap();
749        soft_body
750            .add_particle(Particle::new(Vector3::new(1.0, 0.0, 0.0), 1.0))
751            .unwrap();
752
753        let result = soft_body.add_spring(0, 1, 1.0, 100.0, 0.5);
754        assert!(result.is_ok());
755        assert_eq!(soft_body.springs.len(), 1);
756    }
757
758    #[test]
759    fn test_cloth_creation() {
760        let cloth = SoftBody::<64, 128>::create_cloth(4, 4, 0.5, 100.0, 0.5);
761        assert!(cloth.is_ok());
762
763        let cloth = cloth.unwrap();
764        assert_eq!(cloth.particles.len(), 16); // 4x4 grid
765        assert!(cloth.springs.len() > 0); // Should have multiple springs
766    }
767}