1use crate::math::Fix128;
18use crate::sim_field::ScalarField3D;
19use crate::sim_modifier::PhysicsModifier;
20use crate::sim_modifier::{observe_max, StateReader, StateWriter};
21use crate::world_participant::{
22 ObservationSink, Participant, ParticipantFault, ParticipantKind, StateError, SubstepCtx,
23};
24
25#[cfg(not(feature = "std"))]
26use alloc::vec::Vec;
27
28#[derive(Clone, Copy, Debug, PartialEq)]
34pub struct FractureConfig {
35 pub fracture_toughness: f32,
37 pub crack_width: f32,
39 pub max_cracks: usize,
43 pub stress_diffusion: f32,
45 pub stress_decay: f32,
47 pub propagation_speed: f32,
49 pub max_crack_length: f32,
51}
52
53impl Default for FractureConfig {
54 fn default() -> Self {
55 Self {
56 fracture_toughness: 50.0,
57 crack_width: 0.02,
58 max_cracks: 32,
59 stress_diffusion: 0.3,
60 stress_decay: 0.1,
61 propagation_speed: 5.0,
62 max_crack_length: 3.0,
63 }
64 }
65}
66
67#[derive(Clone, Copy, Debug, PartialEq)]
73pub struct Crack {
74 pub start: (f32, f32, f32),
76 pub end: (f32, f32, f32),
78 pub direction: (f32, f32, f32),
80 pub length: f32,
82 pub active: bool,
84}
85
86impl Crack {
87 fn distance(&self, x: f32, y: f32, z: f32, width: f32) -> f32 {
91 let (ax, ay, az) = self.start;
93 let (bx, by, bz) = self.end;
94
95 let pax = x - ax;
96 let pay = y - ay;
97 let paz = z - az;
98 let b_ax = bx - ax;
99 let b_ay = by - ay;
100 let b_az = bz - az;
101
102 let ba_len_sq = b_az.mul_add(b_az, b_ax.mul_add(b_ax, b_ay * b_ay));
103 let h = if ba_len_sq > 1e-10 {
104 (paz.mul_add(b_az, pax.mul_add(b_ax, pay * b_ay)) / ba_len_sq).clamp(0.0, 1.0)
105 } else {
106 0.0
107 };
108
109 let dx = b_ax.mul_add(-h, pax);
110 let dy = b_ay.mul_add(-h, pay);
111 let dz = b_az.mul_add(-h, paz);
112
113 (dx * dx + dy * dy + dz * dz).sqrt() - width
114 }
115}
116
117pub struct FractureModifier {
123 pub config: FractureConfig,
125 pub stress: ScalarField3D,
127 pub cracks: Vec<Crack>,
129 pub enabled: bool,
131}
132
133impl FractureModifier {
134 #[must_use]
136 pub fn new(
137 config: FractureConfig,
138 resolution: usize,
139 min: (f32, f32, f32),
140 max: (f32, f32, f32),
141 ) -> Self {
142 Self {
143 config,
144 stress: ScalarField3D::new(resolution, resolution, resolution, min, max),
145 cracks: Vec::new(),
146 enabled: true,
147 }
148 }
149
150 pub fn apply_stress_at(&mut self, x: f32, y: f32, z: f32, amount: f32, radius: f32) {
152 self.stress.splat(x, y, z, amount, radius);
153 }
154
155 #[must_use]
157 pub fn stress_at(&self, x: f32, y: f32, z: f32) -> f32 {
158 self.stress.sample(x, y, z)
159 }
160
161 #[must_use]
163 pub fn active_crack_count(&self) -> usize {
164 self.cracks.iter().filter(|c| c.active).count()
165 }
166
167 fn check_fracture_seeds(&mut self) {
169 if self.cracks.len() >= self.config.max_cracks {
170 return;
171 }
172
173 let threshold = self.config.fracture_toughness;
174 let nx = self.stress.nx;
175 let ny = self.stress.ny;
176 let nz = self.stress.nz;
177
178 for iz in 0..nz {
179 for iy in 0..ny {
180 for ix in 0..nx {
181 if self.cracks.len() >= self.config.max_cracks {
182 return;
183 }
184
185 let s = self.stress.get(ix, iy, iz);
186 if s > threshold {
187 let wx = self.stress.min.0
188 + ix as f32 * (self.stress.max.0 - self.stress.min.0)
189 / (nx - 1).max(1) as f32;
190 let wy = self.stress.min.1
191 + iy as f32 * (self.stress.max.1 - self.stress.min.1)
192 / (ny - 1).max(1) as f32;
193 let wz = self.stress.min.2
194 + iz as f32 * (self.stress.max.2 - self.stress.min.2)
195 / (nz - 1).max(1) as f32;
196
197 let too_close = self.cracks.iter().any(|c| {
199 let dx = c.start.0 - wx;
200 let dy = c.start.1 - wy;
201 let dz = c.start.2 - wz;
202 (dx * dx + dy * dy + dz * dz).sqrt() < self.config.crack_width * 10.0
203 });
204
205 if !too_close {
206 let (gx, gy, gz) = self.stress.gradient(wx, wy, wz);
208 let glen = gz.mul_add(gz, gx.mul_add(gx, gy * gy)).sqrt();
209
210 let (dx, dy, dz) = if glen > 1e-5 {
212 let cx = gy.mul_add(0.0, -(gz * 1.0));
215 let cy = gz.mul_add(0.0, -(gx * 0.0));
216 let cz = gx.mul_add(1.0, -(gy * 0.0));
217 let clen = (cx * cx + cy * cy + cz * cz).sqrt();
218 if clen > 1e-5 {
219 (cx / clen, cy / clen, cz / clen)
220 } else {
221 (1.0, 0.0, 0.0)
222 }
223 } else {
224 let hash =
226 ((ix * 73856093) ^ (iy * 19349663) ^ (iz * 83492791)) as f32;
227 let angle = hash * 0.0001;
228 (
229 crate::det_math::cos(angle),
230 0.0,
231 crate::det_math::sin(angle),
232 )
233 };
234
235 self.cracks.push(Crack {
236 start: (wx, wy, wz),
237 end: (wx, wy, wz),
238 direction: (dx, dy, dz),
239 length: 0.0,
240 active: true,
241 });
242
243 self.stress.set(ix, iy, iz, 0.0);
245 }
246 }
247 }
248 }
249 }
250 }
251
252 fn propagate_cracks(&mut self, dt: f32) {
254 let speed = self.config.propagation_speed;
255 let max_len = self.config.max_crack_length;
256
257 for crack in &mut self.cracks {
258 if !crack.active {
259 continue;
260 }
261
262 let growth = speed * dt;
263 crack.length += growth;
264
265 if crack.length >= max_len {
266 crack.length = max_len;
267 crack.active = false;
268 }
269
270 crack.end = (
272 crack.direction.0.mul_add(crack.length, crack.start.0),
273 crack.direction.1.mul_add(crack.length, crack.start.1),
274 crack.direction.2.mul_add(crack.length, crack.start.2),
275 );
276 }
277 }
278}
279
280impl PhysicsModifier for FractureModifier {
281 #[inline]
282 fn modify_distance(&self, x: f32, y: f32, z: f32, original_dist: f32) -> f32 {
283 if !self.enabled || self.cracks.is_empty() {
284 return original_dist;
285 }
286
287 let width = self.config.crack_width;
288 let mut d = original_dist;
289
290 for crack in &self.cracks {
291 if crack.length < 1e-5 {
292 continue;
293 }
294 let crack_dist = crack.distance(x, y, z, width);
295 if d < width * 2.0 {
298 d = crate::det_math::max(d, -crack_dist);
299 }
300 }
301
302 d
303 }
304
305 fn update(&mut self, dt: f32) {
306 if !self.enabled {
307 return;
308 }
309
310 self.stress.diffuse(dt, self.config.stress_diffusion);
312
313 self.stress.decay(self.config.stress_decay, dt);
315
316 self.check_fracture_seeds();
318
319 self.propagate_cracks(dt);
321 }
322
323 fn name(&self) -> &'static str {
324 "fracture"
325 }
326
327 fn is_active(&self) -> bool {
328 self.enabled
329 }
330}
331
332impl FractureModifier {
337 pub const PARTICIPANT_KIND: ParticipantKind =
340 ParticipantKind::new(u32::from_be_bytes(*b"FRAC"));
341
342 fn decode_state(bytes: &[u8]) -> Result<Self, StateError> {
343 let mut r = StateReader::new(bytes)?;
344 let config = FractureConfig {
345 fracture_toughness: r.f32()?,
346 crack_width: r.f32()?,
347 max_cracks: r.usize()?,
348 stress_diffusion: r.f32()?,
349 stress_decay: r.f32()?,
350 propagation_speed: r.f32()?,
351 max_crack_length: r.f32()?,
352 };
353 let enabled = r.bool()?;
354 let stress = r.field()?;
355 let n = r.count(10 * 4 + 1)?;
356 let mut cracks = Vec::with_capacity(n);
357 for _ in 0..n {
358 cracks.push(Crack {
359 start: r.vec3()?,
360 end: r.vec3()?,
361 direction: r.vec3()?,
362 length: r.f32()?,
363 active: r.bool()?,
364 });
365 }
366 r.finish()?;
367 Ok(Self {
368 config,
369 stress,
370 cracks,
371 enabled,
372 })
373 }
374}
375
376impl Participant for FractureModifier {
384 fn kind(&self) -> ParticipantKind {
385 Self::PARTICIPANT_KIND
386 }
387
388 fn substep(&mut self, _ctx: &mut SubstepCtx<'_>, h: Fix128) -> Result<(), ParticipantFault> {
389 PhysicsModifier::update(self, h.to_f32());
390 Ok(())
391 }
392
393 fn observe(&self, out: &mut ObservationSink) {
394 observe_max(out, 0, &self.stress);
395 out.push(1, Fix128::from_int(self.cracks.len() as i64));
396 let growing = self.cracks.iter().filter(|c| c.active).count();
397 out.push(2, Fix128::from_int(growing as i64));
398 }
399
400 fn write_state(&self, out: &mut Vec<u8>) {
401 let mut w = StateWriter::new(out);
402 let c = &self.config;
403 w.f32(c.fracture_toughness);
404 w.f32(c.crack_width);
405 w.usize(c.max_cracks);
406 w.f32(c.stress_diffusion);
407 w.f32(c.stress_decay);
408 w.f32(c.propagation_speed);
409 w.f32(c.max_crack_length);
410 w.bool(self.enabled);
411 w.field(&self.stress);
412 w.usize(self.cracks.len());
413 for k in &self.cracks {
414 w.vec3(k.start);
415 w.vec3(k.end);
416 w.vec3(k.direction);
417 w.f32(k.length);
418 w.bool(k.active);
419 }
420 }
421
422 fn check_state(&self, bytes: &[u8]) -> Result<(), StateError> {
423 Self::decode_state(bytes).map(drop)
424 }
425
426 fn read_state(&mut self, bytes: &[u8]) {
427 match Self::decode_state(bytes) {
428 Ok(state) => *self = state,
429 Err(e) => panic!("read_state called with a payload check_state refuses: {e:?}"),
430 }
431 }
432}
433
434#[cfg(test)]
439mod tests {
440 use super::*;
441
442 #[test]
443 fn test_fracture_no_stress() {
444 let config = FractureConfig::default();
445 let modifier = FractureModifier::new(config, 8, (-2.0, -2.0, -2.0), (2.0, 2.0, 2.0));
446
447 let d = modifier.modify_distance(0.0, 0.0, 0.0, -0.5);
448 assert!((d - (-0.5)).abs() < 0.01, "No stress = no cracks");
449 assert_eq!(modifier.active_crack_count(), 0);
450 }
451
452 #[test]
453 fn test_fracture_crack_creation() {
454 let config = FractureConfig {
455 fracture_toughness: 10.0,
456 ..Default::default()
457 };
458 let mut modifier = FractureModifier::new(config, 8, (-2.0, -2.0, -2.0), (2.0, 2.0, 2.0));
459
460 modifier.apply_stress_at(0.0, 0.0, 0.0, 500.0, 1.5);
462
463 modifier.update(0.016);
465
466 assert!(
467 !modifier.cracks.is_empty(),
468 "High stress should create cracks"
469 );
470 }
471
472 #[test]
473 fn test_crack_propagation() {
474 let config = FractureConfig {
475 fracture_toughness: 5.0,
476 propagation_speed: 10.0,
477 max_crack_length: 1.0,
478 ..Default::default()
479 };
480 let mut modifier = FractureModifier::new(config, 8, (-2.0, -2.0, -2.0), (2.0, 2.0, 2.0));
481
482 modifier.apply_stress_at(0.0, 0.0, 0.0, 500.0, 1.5);
483 modifier.update(0.016); let initial_len = modifier.cracks.first().map_or(0.0, |c| c.length);
486
487 for _ in 0..10 {
489 modifier.update(0.016);
490 }
491
492 let final_len = modifier.cracks.first().map_or(0.0, |c| c.length);
493 assert!(
494 final_len > initial_len,
495 "Crack should grow: initial={initial_len}, final={final_len}"
496 );
497 }
498
499 #[test]
500 fn test_crack_sdf_subtraction() {
501 let config = FractureConfig {
502 crack_width: 0.1,
503 ..Default::default()
504 };
505 let mut modifier = FractureModifier::new(config, 8, (-2.0, -2.0, -2.0), (2.0, 2.0, 2.0));
506
507 modifier.cracks.push(Crack {
509 start: (-1.0, 0.0, 0.0),
510 end: (1.0, 0.0, 0.0),
511 direction: (1.0, 0.0, 0.0),
512 length: 2.0,
513 active: false,
514 });
515
516 let d = modifier.modify_distance(0.0, 0.0, 0.0, -0.05);
518 assert!(d > -0.05, "Crack should cut into the SDF, got {d}");
521 }
522
523 fn seeding_config(max_cracks: usize) -> FractureConfig {
524 FractureConfig {
525 fracture_toughness: 50.0,
526 crack_width: 0.01,
527 max_cracks,
528 stress_diffusion: 0.0,
529 stress_decay: 0.0,
530 propagation_speed: 4.0,
531 max_crack_length: 10.0,
532 }
533 }
534
535 #[test]
541 fn one_update_seeds_and_grows_a_crack() {
542 let mut m = FractureModifier::new(seeding_config(4), 1, (1.0, 2.0, 3.0), (2.0, 3.0, 4.0));
543 m.stress.data[0] = 100.0;
544 m.update(0.25);
545 assert_eq!(m.cracks.len(), 1);
546 let c = m.cracks[0];
547 assert_eq!(c.start, (1.0, 2.0, 3.0));
548 assert_eq!(c.direction, (1.0, 0.0, 0.0));
549 assert_eq!(c.length, 1.0);
550 assert_eq!(c.end, (2.0, 2.0, 3.0));
551 assert!(c.active);
552 assert_eq!(m.stress.data[0], 0.0);
553 }
554
555 #[test]
560 fn max_cracks_caps_the_seeds() {
561 let mut m = FractureModifier::new(seeding_config(1), 2, (0.0, 0.0, 0.0), (1.0, 1.0, 1.0));
562 m.stress.data.fill(100.0);
563 m.update(0.25);
564 assert_eq!(m.cracks.len(), 1);
565
566 let mut none =
567 FractureModifier::new(seeding_config(0), 2, (0.0, 0.0, 0.0), (1.0, 1.0, 1.0));
568 none.stress.data.fill(100.0);
569 none.update(0.25);
570 assert!(none.cracks.is_empty());
571 assert!(none.stress.data.iter().all(|&s| s == 100.0));
572 }
573
574 #[test]
579 fn crack_stops_at_the_cap_and_disabled_is_inert() {
580 let mut m = FractureModifier::new(
581 FractureConfig {
582 max_crack_length: 3.0,
583 ..seeding_config(4)
584 },
585 1,
586 (0.0, 0.0, 0.0),
587 (1.0, 1.0, 1.0),
588 );
589 m.cracks.push(Crack {
590 start: (0.0, 0.0, 0.0),
591 end: (0.0, 0.0, 0.0),
592 direction: (0.0, 1.0, 0.0),
593 length: 0.0,
594 active: true,
595 });
596 m.update(1.0);
597 assert_eq!(m.cracks[0].length, 3.0);
598 assert!(!m.cracks[0].active);
599 assert_eq!(m.cracks[0].end, (0.0, 3.0, 0.0));
600 m.update(1.0);
601 assert_eq!(m.cracks[0].length, 3.0);
602
603 m.enabled = false;
604 assert!(!m.is_active());
605 assert_eq!(m.modify_distance(0.0, 1.0, 0.0, -0.5), -0.5);
606 m.stress.data[0] = 100.0;
607 m.update(1.0);
608 assert_eq!(m.cracks.len(), 1);
609 assert_eq!(m.stress.data[0], 100.0);
610 }
611
612 fn sample_modifier() -> FractureModifier {
613 let mut m = FractureModifier::new(
614 FractureConfig {
615 fracture_toughness: 12.5,
616 crack_width: 0.25,
617 max_cracks: 7,
618 stress_diffusion: 0.125,
619 stress_decay: 0.375,
620 propagation_speed: 1.5,
621 max_crack_length: 2.5,
622 },
623 2,
624 (-1.0, -2.0, -3.0),
625 (1.0, 2.0, 3.0),
626 );
627 for (i, v) in m.stress.data.iter_mut().enumerate() {
628 *v = i as f32 * 0.5;
629 }
630 m.cracks.push(Crack {
631 start: (0.5, 0.25, -0.5),
632 end: (1.0, 0.25, -0.5),
633 direction: (1.0, 0.0, 0.0),
634 length: 0.5,
635 active: true,
636 });
637 m.cracks.push(Crack {
638 start: (0.0, 0.0, 0.0),
639 end: (0.0, 0.0, 2.5),
640 direction: (0.0, 0.0, 1.0),
641 length: 2.5,
642 active: false,
643 });
644 m.enabled = false;
645 m
646 }
647
648 #[test]
654 fn participant_state_round_trip_and_observations() {
655 assert_eq!(
656 FractureModifier::PARTICIPANT_KIND,
657 ParticipantKind::new(0x4652_4143)
658 );
659 let src = sample_modifier();
660 assert_eq!(Participant::kind(&src), FractureModifier::PARTICIPANT_KIND);
661 let mut bytes = Vec::new();
662 src.write_state(&mut bytes);
663 assert_eq!(src.check_state(&bytes), Ok(()));
664 let mut dst = FractureModifier::new(
665 FractureConfig::default(),
666 1,
667 (0.0, 0.0, 0.0),
668 (1.0, 1.0, 1.0),
669 );
670 dst.read_state(&bytes);
671 assert_eq!(dst.config, src.config);
672 assert!(!dst.enabled);
673 assert_eq!(dst.stress.data, src.stress.data);
674 assert_eq!(dst.cracks, src.cracks);
675 let mut again = Vec::new();
676 dst.write_state(&mut again);
677 assert_eq!(again, bytes);
678
679 let mut sink = ObservationSink::new();
680 dst.observe(&mut sink);
681 assert_eq!(
682 sink.values(),
683 &[
684 (0, Fix128::from_ratio(7, 2)),
685 (1, Fix128::from_int(2)),
686 (2, Fix128::ONE)
687 ]
688 );
689 }
690
691 #[test]
695 fn participant_state_refusals() {
696 let src = sample_modifier();
697 let mut bytes = Vec::new();
698 src.write_state(&mut bytes);
699 let last = bytes.len() - 1;
700 assert_eq!(bytes[last], 0);
701 let mut bad = bytes.clone();
702 bad[last] = 3;
703 assert_eq!(src.check_state(&bad), Err(StateError::InvalidValue));
704 assert!(matches!(
705 src.check_state(&bytes[..last]),
706 Err(StateError::Length { .. })
707 ));
708 let mut long = bytes.clone();
709 long.push(0);
710 assert!(matches!(
711 src.check_state(&long),
712 Err(StateError::Length { .. })
713 ));
714 let at = bytes.len() - 2 * 41 - 8;
716 assert_eq!(bytes[at..at + 8], 2_u64.to_le_bytes());
717 let mut many = bytes;
718 many[at..at + 8].copy_from_slice(&1000_u64.to_le_bytes());
719 assert!(matches!(
720 src.check_state(&many),
721 Err(StateError::Length { .. })
722 ));
723 }
724
725 #[test]
726 #[should_panic(expected = "read_state called with a payload check_state refuses")]
727 fn participant_read_state_panics_on_a_refused_payload() {
728 let mut m = sample_modifier();
729 m.read_state(&[1, 0, 0, 0]);
730 }
731
732 #[test]
736 fn participant_substep_in_a_world() {
737 let mut m = FractureModifier::new(seeding_config(4), 1, (1.0, 2.0, 3.0), (2.0, 3.0, 4.0));
738 m.stress.data[0] = 100.0;
739 let mut world = crate::solver::PhysicsWorld::new(crate::solver::SolverConfig {
740 substeps: 1,
741 ..Default::default()
742 });
743 world.add_participant(Box::new(m)).expect("register");
744 world.step(Fix128::from_ratio(1, 4));
745 let Some(crate::world_participant::Observed::Exact(sink)) = world.observe_participant(0)
746 else {
747 panic!("observation");
748 };
749 assert_eq!(
750 sink.values(),
751 &[(0, Fix128::ZERO), (1, Fix128::ONE), (2, Fix128::ONE)]
752 );
753 }
754}