1use crate::math::{Fix128, Vec3Fix};
16#[cfg(feature = "std")]
17use crate::sdf_collider::SdfCollider;
18
19#[cfg(not(feature = "std"))]
20use alloc::vec;
21#[cfg(not(feature = "std"))]
22use alloc::vec::Vec;
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub struct DeformableConfig {
31 pub iterations: usize,
33 pub substeps: usize,
35 pub gravity: Vec3Fix,
37 pub damping: Fix128,
39 pub volume_compliance: Fix128,
41 pub edge_compliance: Fix128,
43 pub shape_stiffness: Fix128,
45 pub sdf_friction: Fix128,
47}
48
49impl Default for DeformableConfig {
50 fn default() -> Self {
51 Self {
52 iterations: 8,
53 substeps: 4,
54 gravity: Vec3Fix::new(Fix128::ZERO, Fix128::from_int(-10), Fix128::ZERO),
55 damping: Fix128::from_ratio(99, 100),
56 volume_compliance: Fix128::from_ratio(1, 1000),
57 edge_compliance: Fix128::ZERO,
58 shape_stiffness: Fix128::from_ratio(5, 10),
59 sdf_friction: Fix128::from_ratio(3, 10),
60 }
61 }
62}
63
64#[derive(Clone, Copy, Debug, PartialEq)]
70struct TetConstraint {
71 indices: [usize; 4],
73 rest_volume: Fix128,
75}
76
77#[derive(Clone, Copy, Debug, PartialEq)]
79struct EdgeConstraint {
80 i0: usize,
81 i1: usize,
82 rest_length: Fix128,
83}
84
85pub struct DeformableBody {
91 pub positions: Vec<Vec3Fix>,
93 prev_positions: Vec<Vec3Fix>,
95 pub velocities: Vec<Vec3Fix>,
97 pub inv_masses: Vec<Fix128>,
99 pub tetrahedra: Vec<[usize; 4]>,
101 pub surface_triangles: Vec<[usize; 3]>,
103 tet_constraints: Vec<TetConstraint>,
105 edge_constraints: Vec<EdgeConstraint>,
107 rest_relative: Vec<Vec3Fix>,
109 pub config: DeformableConfig,
111}
112
113impl DeformableBody {
114 #[must_use]
116 pub fn new(
117 positions: Vec<Vec3Fix>,
118 tetrahedra: &[[usize; 4]],
119 surface_triangles: Vec<[usize; 3]>,
120 mass_per_particle: Fix128,
121 ) -> Self {
122 let n = positions.len();
123 let inv_mass = if mass_per_particle.is_zero() {
124 Fix128::ZERO
125 } else {
126 Fix128::ONE / mass_per_particle
127 };
128
129 let rest_center = compute_center_of_mass(&positions);
131 let rest_relative: Vec<Vec3Fix> = positions.iter().map(|p| *p - rest_center).collect();
132
133 let mut body = Self {
134 prev_positions: positions.clone(),
135 velocities: vec![Vec3Fix::ZERO; n],
136 inv_masses: vec![inv_mass; n],
137 tetrahedra: tetrahedra.to_vec(),
138 surface_triangles,
139 tet_constraints: Vec::new(),
140 edge_constraints: Vec::new(),
141 rest_relative,
142 positions,
143 config: DeformableConfig::default(),
144 };
145
146 body.build_constraints();
147 body
148 }
149
150 #[must_use]
152 pub fn new_cube(center: Vec3Fix, half_extent: Fix128, mass: Fix128) -> Self {
153 let h = half_extent;
154 let positions = vec![
155 center + Vec3Fix::new(-h, -h, -h),
156 center + Vec3Fix::new(h, -h, -h),
157 center + Vec3Fix::new(h, h, -h),
158 center + Vec3Fix::new(-h, h, -h),
159 center + Vec3Fix::new(-h, -h, h),
160 center + Vec3Fix::new(h, -h, h),
161 center + Vec3Fix::new(h, h, h),
162 center + Vec3Fix::new(-h, h, h),
163 ];
164
165 let tetrahedra = vec![
167 [0, 1, 3, 4],
168 [1, 2, 3, 6],
169 [1, 4, 5, 6],
170 [3, 4, 6, 7],
171 [1, 3, 4, 6],
172 ];
173
174 let surface_triangles = vec![
175 [0, 1, 2],
176 [0, 2, 3], [4, 6, 5],
178 [4, 7, 6], [0, 4, 5],
180 [0, 5, 1], [2, 6, 7],
182 [2, 7, 3], [0, 3, 7],
184 [0, 7, 4], [1, 5, 6],
186 [1, 6, 2], ];
188
189 let mass_per_particle = mass / Fix128::from_int(8);
190 Self::new(positions, &tetrahedra, surface_triangles, mass_per_particle)
191 }
192
193 fn build_constraints(&mut self) {
195 let min_volume = Fix128::from_ratio(1, 1000000); for tet in &self.tetrahedra {
198 let rest_vol = compute_tet_volume(
199 self.positions[tet[0]],
200 self.positions[tet[1]],
201 self.positions[tet[2]],
202 self.positions[tet[3]],
203 );
204 if rest_vol.abs() < min_volume {
205 continue; }
207 self.tet_constraints.push(TetConstraint {
208 indices: *tet,
209 rest_volume: rest_vol,
210 });
211 }
212
213 let mut edge_set: Vec<(usize, usize)> = Vec::new();
215 for tet in &self.tetrahedra {
216 let edges = [
217 (tet[0], tet[1]),
218 (tet[0], tet[2]),
219 (tet[0], tet[3]),
220 (tet[1], tet[2]),
221 (tet[1], tet[3]),
222 (tet[2], tet[3]),
223 ];
224 for (a, b) in edges {
225 let (lo, hi) = if a < b { (a, b) } else { (b, a) };
226 if !edge_set.contains(&(lo, hi)) {
227 edge_set.push((lo, hi));
228 let rest = (self.positions[a] - self.positions[b]).length();
229 self.edge_constraints.push(EdgeConstraint {
230 i0: a,
231 i1: b,
232 rest_length: rest,
233 });
234 }
235 }
236 }
237 }
238
239 #[inline]
241 #[must_use]
242 pub fn particle_count(&self) -> usize {
243 self.positions.len()
244 }
245
246 pub fn step(&mut self, dt: Fix128) {
248 let substep_dt = dt / Fix128::from_int(self.config.substeps as i64);
249 for _ in 0..self.config.substeps {
250 self.substep(substep_dt);
251 }
252 self.apply_frame_damping();
253 }
254
255 #[cfg(feature = "std")]
257 pub fn step_with_sdf(&mut self, dt: Fix128, sdf_colliders: &[SdfCollider]) {
258 let substep_dt = dt / Fix128::from_int(self.config.substeps as i64);
259 for _ in 0..self.config.substeps {
260 self.substep(substep_dt);
261 self.resolve_sdf_collisions(sdf_colliders);
262 }
263 self.apply_frame_damping();
264 }
265
266 fn apply_frame_damping(&mut self) {
272 let d = self.config.damping;
273 for v in &mut self.velocities {
274 *v = *v * d;
275 }
276 }
277
278 fn substep(&mut self, dt: Fix128) {
280 let n = self.particle_count();
281
282 for i in 0..n {
284 if self.inv_masses[i].is_zero() {
285 continue;
286 }
287 self.prev_positions[i] = self.positions[i];
288 self.velocities[i] = self.velocities[i] + self.config.gravity * dt;
289 self.positions[i] = self.positions[i] + self.velocities[i] * dt;
290 }
291
292 for _ in 0..self.config.iterations {
294 self.solve_edge_constraints(dt);
295 self.solve_volume_constraints(dt);
296 self.apply_shape_matching();
297 }
298
299 let inv_dt = Fix128::ONE / dt;
301 for i in 0..n {
302 if self.inv_masses[i].is_zero() {
303 continue;
304 }
305 self.velocities[i] = (self.positions[i] - self.prev_positions[i]) * inv_dt;
306 }
307 }
308
309 fn solve_edge_constraints(&mut self, dt: Fix128) {
311 let compliance = self.config.edge_compliance / (dt * dt);
312
313 for c_idx in 0..self.edge_constraints.len() {
314 let c = self.edge_constraints[c_idx];
315 let p0 = self.positions[c.i0];
316 let p1 = self.positions[c.i1];
317 let w0 = self.inv_masses[c.i0];
318 let w1 = self.inv_masses[c.i1];
319
320 let w_sum = w0 + w1 + compliance;
321 if w_sum.is_zero() {
322 continue;
323 }
324
325 let delta = p1 - p0;
326 let dist = delta.length();
327 if dist.is_zero() {
328 continue;
329 }
330
331 let error = dist - c.rest_length;
332 let lambda = error / w_sum;
333 let correction = delta / dist * lambda;
334
335 if !w0.is_zero() {
336 self.positions[c.i0] = self.positions[c.i0] + correction * w0;
337 }
338 if !w1.is_zero() {
339 self.positions[c.i1] = self.positions[c.i1] - correction * w1;
340 }
341 }
342 }
343
344 fn solve_volume_constraints(&mut self, dt: Fix128) {
346 let compliance = self.config.volume_compliance / (dt * dt);
347
348 for c_idx in 0..self.tet_constraints.len() {
349 let c = self.tet_constraints[c_idx];
350 let [i0, i1, i2, i3] = c.indices;
351
352 let p0 = self.positions[i0];
353 let p1 = self.positions[i1];
354 let p2 = self.positions[i2];
355 let p3 = self.positions[i3];
356
357 let current_vol = compute_tet_volume(p0, p1, p2, p3);
358 let error = current_vol - c.rest_volume;
359
360 if error.abs() < Fix128::from_ratio(1, 10000) {
361 continue;
362 }
363
364 let g0 = (p1 - p3).cross(p2 - p3);
366 let g1 = (p2 - p3).cross(p0 - p3);
367 let g2 = (p0 - p3).cross(p1 - p3);
368 let g3 = -(g0 + g1 + g2);
369
370 let w0 = self.inv_masses[i0];
371 let w1 = self.inv_masses[i1];
372 let w2 = self.inv_masses[i2];
373 let w3 = self.inv_masses[i3];
374
375 let denom = w0 * g0.length_squared()
376 + w1 * g1.length_squared()
377 + w2 * g2.length_squared()
378 + w3 * g3.length_squared()
379 + compliance;
380
381 if denom.is_zero() {
382 continue;
383 }
384
385 let lambda = error / denom;
386 let six = Fix128::from_int(6);
387
388 if !w0.is_zero() {
389 self.positions[i0] = self.positions[i0] - g0 * (lambda * w0 / six);
390 }
391 if !w1.is_zero() {
392 self.positions[i1] = self.positions[i1] - g1 * (lambda * w1 / six);
393 }
394 if !w2.is_zero() {
395 self.positions[i2] = self.positions[i2] - g2 * (lambda * w2 / six);
396 }
397 if !w3.is_zero() {
398 self.positions[i3] = self.positions[i3] - g3 * (lambda * w3 / six);
399 }
400 }
401 }
402
403 fn apply_shape_matching(&mut self) {
405 let alpha = self.config.shape_stiffness;
406 if alpha.is_zero() {
407 return;
408 }
409
410 let current_center = compute_center_of_mass(&self.positions);
412
413 for i in 0..self.particle_count() {
416 if self.inv_masses[i].is_zero() {
417 continue;
418 }
419
420 let target = current_center + self.rest_relative[i];
421 let delta = target - self.positions[i];
422 self.positions[i] = self.positions[i] + delta * alpha;
423 }
424 }
425
426 #[cfg(feature = "std")]
428 fn resolve_sdf_collisions(&mut self, sdf_colliders: &[SdfCollider]) {
429 for i in 0..self.particle_count() {
430 if self.inv_masses[i].is_zero() {
431 continue;
432 }
433
434 for sdf in sdf_colliders {
435 let (lx, ly, lz) = sdf.world_to_local(self.positions[i]);
436 let dist = sdf.field.distance(lx, ly, lz) * sdf.scale_f32;
437
438 if dist < 0.0 {
439 let (nx, ny, nz) = sdf.field.normal(lx, ly, lz);
440 let normal = sdf.local_normal_to_world(nx, ny, nz);
441 let depth = Fix128::from_f32(-dist);
442 self.positions[i] = self.positions[i] + normal * depth;
443
444 let vel = self.velocities[i];
445 let vn = normal * vel.dot(normal);
446 let vt = vel - vn;
447 self.velocities[i] = vt * (Fix128::ONE - self.config.sdf_friction);
448 }
449 }
450 }
451 }
452
453 #[must_use]
457 pub fn center_of_mass(&self) -> Vec3Fix {
458 compute_center_of_mass(&self.positions)
459 }
460
461 pub fn resolve_rigid_body_collisions(
475 &mut self,
476 rigid_bodies: &mut [crate::solver::RigidBody],
477 body_radii: &[Fix128],
478 dt: Fix128,
479 ) {
480 if dt.is_zero() {
481 return;
482 }
483
484 for i in 0..self.particle_count() {
485 if self.inv_masses[i].is_zero() {
486 continue;
487 }
488
489 for (rb_idx, rb) in rigid_bodies.iter_mut().enumerate() {
490 if rb.is_static() && rb_idx >= body_radii.len() {
491 continue;
492 }
493 let radius = if rb_idx < body_radii.len() {
494 body_radii[rb_idx]
495 } else {
496 Fix128::ONE
497 };
498
499 let delta = self.positions[i] - rb.position;
500 let dist_sq = delta.length_squared();
501 let r_sq = radius * radius;
502
503 if dist_sq < r_sq && !dist_sq.is_zero() {
504 let dist = dist_sq.sqrt();
505 let normal = delta / dist;
506 let penetration = radius - dist;
507
508 let w_particle = self.inv_masses[i];
514 let w_body = rb.inv_mass;
515 let w_sum = w_particle + w_body;
516 if w_sum.is_zero() {
517 continue;
518 }
519
520 let particle_ratio = w_particle / w_sum;
521 let rb_ratio = w_body / w_sum;
522
523 self.positions[i] = self.positions[i] + normal * (penetration * particle_ratio);
525
526 if !rb.inv_mass.is_zero() {
527 rb.position = rb.position - normal * (penetration * rb_ratio);
528 }
529
530 let rel_vel = self.velocities[i] - rb.velocity;
532 let vel_normal = rel_vel.dot(normal);
533
534 if vel_normal < Fix128::ZERO {
535 let impulse_mag = -vel_normal / w_sum;
536 let impulse = normal * impulse_mag;
537
538 self.velocities[i] = self.velocities[i] + impulse * w_particle;
539 if !rb.inv_mass.is_zero() {
540 rb.velocity = rb.velocity - impulse * rb.inv_mass;
541 }
542 }
543 }
544 }
545 }
546 }
547}
548
549fn compute_center_of_mass(positions: &[Vec3Fix]) -> Vec3Fix {
555 if positions.is_empty() {
556 return Vec3Fix::ZERO;
557 }
558 let mut sum = Vec3Fix::ZERO;
559 for p in positions {
560 sum = sum + *p;
561 }
562 sum / Fix128::from_int(positions.len() as i64)
563}
564
565fn compute_tet_volume(p0: Vec3Fix, p1: Vec3Fix, p2: Vec3Fix, p3: Vec3Fix) -> Fix128 {
567 let a = p1 - p0;
568 let b = p2 - p0;
569 let c = p3 - p0;
570 a.dot(b.cross(c)) / Fix128::from_int(6)
571}
572
573impl core::fmt::Debug for DeformableBody {
574 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
575 f.debug_struct("DeformableBody")
576 .field(
577 "positions",
578 &format_args!("[{} items]", self.positions.len()),
579 )
580 .field(
581 "prev_positions",
582 &format_args!("[{} items]", self.prev_positions.len()),
583 )
584 .field(
585 "velocities",
586 &format_args!("[{} items]", self.velocities.len()),
587 )
588 .field(
589 "inv_masses",
590 &format_args!("[{} items]", self.inv_masses.len()),
591 )
592 .field(
593 "tetrahedra",
594 &format_args!("[{} items]", self.tetrahedra.len()),
595 )
596 .field(
597 "surface_triangles",
598 &format_args!("[{} items]", self.surface_triangles.len()),
599 )
600 .field(
601 "tet_constraints",
602 &format_args!("[{} items]", self.tet_constraints.len()),
603 )
604 .field(
605 "edge_constraints",
606 &format_args!("[{} items]", self.edge_constraints.len()),
607 )
608 .field(
609 "rest_relative",
610 &format_args!("[{} items]", self.rest_relative.len()),
611 )
612 .field("config", &self.config)
613 .finish()
614 }
615}
616
617#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[test]
626 fn test_deformable_cube() {
627 let body = DeformableBody::new_cube(Vec3Fix::ZERO, Fix128::ONE, Fix128::from_int(10));
628
629 assert_eq!(body.particle_count(), 8);
630 assert_eq!(body.tetrahedra.len(), 5);
631 assert!(!body.edge_constraints.is_empty());
632 assert!(!body.tet_constraints.is_empty());
633 }
634
635 #[test]
636 fn test_deformable_gravity() {
637 let mut body = DeformableBody::new_cube(
638 Vec3Fix::from_int(0, 5, 0),
639 Fix128::ONE,
640 Fix128::from_int(10),
641 );
642
643 let dt = Fix128::from_ratio(1, 60);
644 for _ in 0..60 {
645 body.step(dt);
646 }
647
648 let com = body.center_of_mass();
649 assert!(
650 com.y < Fix128::from_int(5),
651 "Deformable should fall under gravity"
652 );
653 }
654
655 #[test]
656 fn test_tet_volume() {
657 let vol = compute_tet_volume(
658 Vec3Fix::ZERO,
659 Vec3Fix::from_int(1, 0, 0),
660 Vec3Fix::from_int(0, 1, 0),
661 Vec3Fix::from_int(0, 0, 1),
662 );
663 let expected = Fix128::from_ratio(1, 6);
665 let error = (vol - expected).abs();
666 assert!(
667 error < Fix128::from_ratio(1, 100),
668 "Tet volume should be 1/6"
669 );
670 }
671
672 #[test]
673 fn test_center_of_mass() {
674 let positions = vec![
675 Vec3Fix::from_int(0, 0, 0),
676 Vec3Fix::from_int(2, 0, 0),
677 Vec3Fix::from_int(0, 2, 0),
678 Vec3Fix::from_int(0, 0, 2),
679 ];
680
681 let com = compute_center_of_mass(&positions);
682 let (cx, cy, cz) = com.to_f32();
683 assert!((cx - 0.5).abs() < 0.01);
684 assert!((cy - 0.5).abs() < 0.01);
685 assert!((cz - 0.5).abs() < 0.01);
686 }
687
688 #[test]
689 fn test_soft_rigid_coupling() {
690 use crate::solver::RigidBody;
691
692 let mut body = DeformableBody::new_cube(
693 Vec3Fix::from_int(0, 0, 0),
694 Fix128::ONE,
695 Fix128::from_int(10),
696 );
697
698 let mut rigid_bodies = vec![RigidBody::new(
699 Vec3Fix::from_int(0, 0, 0),
700 Fix128::from_int(5),
701 )];
702 let radii = vec![Fix128::from_int(3)]; let dt = Fix128::from_ratio(1, 60);
705 body.resolve_rigid_body_collisions(&mut rigid_bodies, &radii, dt);
706
707 let com = body.center_of_mass();
711 assert!(
713 com.x.hi >= -10 && com.x.hi <= 10,
714 "COM should be reasonable"
715 );
716 }
717
718 #[cfg(feature = "std")]
720 fn unit_sphere_collider() -> crate::sdf_collider::SdfCollider {
721 use crate::sdf_collider::{ClosureSdf, SdfCollider};
722 let field = ClosureSdf::new(
723 |x, y, z| (x * x + y * y + z * z).sqrt() - 1.0,
724 |x, y, z| {
725 let len = (x * x + y * y + z * z).sqrt();
726 if len > 1e-6 {
727 (x / len, y / len, z / len)
728 } else {
729 (0.0, 1.0, 0.0)
730 }
731 },
732 );
733 SdfCollider::new_static(
734 Box::new(field),
735 Vec3Fix::ZERO,
736 crate::math::QuatFix::IDENTITY,
737 )
738 }
739
740 #[cfg(feature = "std")]
742 fn sphere_dist(p: Vec3Fix) -> f32 {
743 let (x, y, z) = p.to_f32();
744 (x * x + y * y + z * z).sqrt() - 1.0
745 }
746
747 #[cfg(feature = "std")]
748 #[test]
749 fn step_with_sdf_keeps_cube_particles_outside_unit_sphere() {
750 let make = || {
752 DeformableBody::new_cube(
753 Vec3Fix::new(Fix128::ZERO, Fix128::from_ratio(5, 2), Fix128::ZERO),
754 Fix128::from_ratio(1, 2),
755 Fix128::from_int(8),
756 )
757 };
758 let sphere = [unit_sphere_collider()];
759 let mut body = make();
760 let dt = Fix128::from_ratio(1, 60);
761 let mut min_dist = f32::MAX;
762 for frame in 0..60 {
763 body.step_with_sdf(dt, &sphere);
764 for (i, p) in body.positions.iter().enumerate() {
765 let d = sphere_dist(*p);
766 min_dist = min_dist.min(d);
767 assert!(
768 d >= -1e-3,
769 "frame {frame} particle {i} inside sphere: dist {d}"
770 );
771 }
772 }
773 assert!(min_dist < 0.05, "never touched: min dist {min_dist}");
774 let (_, cy, _) = body.center_of_mass().to_f32();
776 assert!(cy > 0.5, "center of mass y {cy}");
777
778 let mut free = make();
780 let mut free_min = f32::MAX;
781 for _ in 0..60 {
782 free.step(dt);
783 for p in &free.positions {
784 free_min = free_min.min(sphere_dist(*p));
785 }
786 }
787 assert!(
788 free_min < -0.1,
789 "without SDF the cube should pass through: {free_min}"
790 );
791
792 let mut a = make();
794 let mut b = make();
795 for _ in 0..10 {
796 a.step_with_sdf(dt, &[]);
797 b.step(dt);
798 }
799 assert_eq!(a.positions, b.positions);
800 assert_eq!(a.velocities, b.velocities);
801 }
802}