1use heapless::Vec;
38use nalgebra::Vector3;
39
40#[cfg(not(feature = "std"))]
41use micromath::F32Ext;
42
43#[derive(Debug, Clone)]
47pub struct Particle {
48 pub position: Vector3<f32>,
50
51 pub previous_position: Vector3<f32>,
53
54 pub velocity: Vector3<f32>,
56
57 pub mass: f32,
59
60 pub inv_mass: f32,
62
63 pub pinned: bool,
65
66 pub force: Vector3<f32>,
68
69 pub radius: f32,
71}
72
73impl Particle {
74 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 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 pub fn apply_force(&mut self, force: Vector3<f32>) {
104 if !self.pinned {
105 self.force += force;
106 }
107 }
108
109 pub fn with_radius(mut self, radius: f32) -> Self {
111 self.radius = radius;
112 self
113 }
114}
115
116#[derive(Debug, Clone, Copy)]
120pub struct Spring {
121 pub particle_a: usize,
123
124 pub particle_b: usize,
126
127 pub rest_length: f32,
129
130 pub stiffness: f32,
132
133 pub damping: f32,
135
136 pub enabled: bool,
138}
139
140impl Spring {
141 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 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 let extension = distance - self.rest_length;
189 let spring_force = direction * (self.stiffness * extension);
190
191 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#[derive(Debug, Clone, Copy)]
203pub struct PressureConfig {
204 pub target_volume: f32,
206
207 pub pressure_coefficient: f32,
209
210 pub enabled: bool,
212}
213
214impl PressureConfig {
215 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#[derive(Debug, Clone)]
241pub struct SoftBody<const P: usize, const S: usize> {
242 pub particles: Vec<Particle, P>,
244
245 pub springs: Vec<Spring, S>,
247
248 pub gravity: Vector3<f32>,
250
251 pub damping: f32,
253
254 pub pressure_config: PressureConfig,
256
257 pub ground_plane: Option<f32>,
259
260 pub ground_restitution: f32,
262
263 pub ground_friction: f32,
265}
266
267impl<const P: usize, const S: usize> SoftBody<P, S> {
268 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 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 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 pub fn get_particle(&self, index: usize) -> Option<&Particle> {
304 self.particles.get(index)
305 }
306
307 pub fn get_particle_mut(&mut self, index: usize) -> Option<&mut Particle> {
309 self.particles.get_mut(index)
310 }
311
312 pub fn set_gravity(&mut self, gravity: Vector3<f32>) {
314 self.gravity = gravity;
315 }
316
317 pub fn step(&mut self, dt: f32) {
321 if dt <= 0.0 {
322 return;
323 }
324
325 for particle in self.particles.iter_mut() {
327 if !particle.pinned {
328 particle.apply_force(self.gravity * particle.mass);
329 }
330 }
331
332 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 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 self.particles[idx_a].apply_force(force);
355 self.particles[idx_b].apply_force(-force);
356 }
357
358 if self.pressure_config.enabled {
360 self.apply_pressure_forces();
361 }
362
363 for particle in self.particles.iter_mut() {
365 if particle.pinned {
366 particle.force = Vector3::zeros();
367 continue;
368 }
369
370 let acceleration = particle.force * particle.inv_mass;
372 particle.velocity += acceleration * dt;
373
374 particle.velocity *= self.damping;
376
377 particle.previous_position = particle.position;
379 particle.position += particle.velocity * dt;
380
381 particle.force = Vector3::zeros();
383 }
384
385 self.apply_constraints();
387 }
388
389 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 if particle.position.y - particle.radius < ground_y {
399 particle.position.y = ground_y + particle.radius;
401
402 if particle.velocity.y < 0.0 {
404 particle.velocity.y *= -self.ground_restitution;
405 }
406
407 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 fn apply_pressure_forces(&mut self) {
420 if self.particles.is_empty() {
422 return;
423 }
424
425 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 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 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 pub fn clear_forces(&mut self) {
471 for particle in self.particles.iter_mut() {
472 particle.force = Vector3::zeros();
473 }
474 }
475
476 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
490impl<const P: usize, const S: usize> SoftBody<P, S> {
492 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 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 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 for y in 0..height {
530 for x in 0..width {
531 let idx = y * width + x;
532
533 if x < width - 1 {
535 soft_body.add_spring(idx, idx + 1, spacing, stiffness, damping)?;
536 }
537
538 if y < height - 1 {
540 soft_body.add_spring(idx, idx + width, spacing, stiffness, damping)?;
541 }
542 }
543 }
544
545 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; 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 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 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 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 if x < size - 1 {
609 soft_body.add_spring(idx, idx + 1, spacing, stiffness, damping)?;
610 }
611
612 if y < size - 1 {
614 soft_body.add_spring(idx, idx + size, spacing, stiffness, damping)?;
615 }
616
617 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 soft_body.pressure_config =
633 PressureConfig::new((size as f32 * spacing).powi(3), stiffness * 0.1);
634
635 Ok(soft_body)
636 }
637
638 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 let t = (1.0 + 5.0_f32.sqrt()) / 2.0;
651
652 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 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 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); assert!(cloth.springs.len() > 0); }
767}