Skip to main content

blast_stress_solver/rapier/
runtime.rs

1use std::collections::{HashMap, HashSet};
2use std::ops::{Deref, DerefMut};
3use std::sync::{
4    atomic::{AtomicUsize, Ordering},
5    Arc, Mutex,
6};
7
8use rapier3d::prelude::*;
9
10#[cfg(not(target_arch = "wasm32"))]
11use std::time::Instant;
12
13#[cfg(target_arch = "wasm32")]
14#[derive(Clone, Copy)]
15struct Instant;
16
17#[cfg(target_arch = "wasm32")]
18impl Instant {
19    fn now() -> Self {
20        Self
21    }
22
23    fn elapsed(&self) -> std::time::Duration {
24        std::time::Duration::ZERO
25    }
26}
27
28use crate::types::Vec3 as SolverVec3;
29use crate::{ScenarioDesc, SolverSettings, Vec3};
30
31use super::body_tracker::SplitEditStats;
32use super::destructible::{DestructibleSet, SplitCohort, StepResult};
33use super::fracture_policy::FracturePolicy;
34use super::optimization::OptimizationResult;
35use super::resimulation::BodySnapshots;
36use super::{
37    DebrisCleanupOptions, DebrisCollisionMode, SleepThresholdOptions, SmallBodyDampingOptions,
38};
39
40#[derive(Clone, Copy, Debug)]
41pub struct ContactImpactOptions {
42    pub enabled: bool,
43    pub min_total_impulse: f32,
44    pub min_external_speed: f32,
45    pub min_internal_speed: f32,
46    pub cooldown_secs: f32,
47    pub force_scale: f32,
48    pub max_force_magnitude: f32,
49    pub splash_radius: f32,
50    pub splash_falloff_exponent: f32,
51    pub internal_contact_scale: f32,
52}
53
54impl Default for ContactImpactOptions {
55    fn default() -> Self {
56        Self {
57            enabled: true,
58            min_total_impulse: 8.0,
59            min_external_speed: 0.75,
60            min_internal_speed: 0.25,
61            cooldown_secs: 0.12,
62            force_scale: 30.0,
63            max_force_magnitude: f32::INFINITY,
64            splash_radius: 2.0,
65            splash_falloff_exponent: 2.0,
66            internal_contact_scale: 0.5,
67        }
68    }
69}
70
71#[derive(Clone, Copy, Debug)]
72pub struct GracePeriodOptions {
73    pub sibling_steps: u32,
74    pub impact_source_steps: u32,
75}
76
77impl Default for GracePeriodOptions {
78    fn default() -> Self {
79        Self {
80            sibling_steps: 1,
81            impact_source_steps: 2,
82        }
83    }
84}
85
86#[derive(Clone, Copy, Debug, Default)]
87pub struct DestructionRuntimeOptions {
88    pub contact_impacts: ContactImpactOptions,
89    pub grace: GracePeriodOptions,
90    pub destructible: DestructibleRuntimeOptions,
91}
92
93#[derive(Clone, Copy, Debug, Default)]
94pub struct DestructibleRuntimeOptions {
95    pub sleep_thresholds: Option<SleepThresholdOptions>,
96    pub small_body_damping: Option<SmallBodyDampingOptions>,
97    pub debris_cleanup: Option<DebrisCleanupOptions>,
98    pub debris_collision_mode: Option<DebrisCollisionMode>,
99    pub split_child_recentering_enabled: Option<bool>,
100    pub split_child_velocity_fit_enabled: Option<bool>,
101    pub skip_single_bodies: Option<bool>,
102}
103
104/// Borrowed access to the caller-owned Rapier world state.
105///
106/// `DestructionRuntime` integrates into an existing Rapier application instead
107/// of owning the physics pipeline itself. The consumer still calls
108/// `PhysicsPipeline::step(...)` and passes these borrowed world sets back to the
109/// runtime around that step.
110pub struct RapierWorldAccess<'a> {
111    pub bodies: &'a mut RigidBodySet,
112    pub colliders: &'a mut ColliderSet,
113    pub island_manager: &'a mut IslandManager,
114    pub broad_phase: &'a mut BroadPhaseBvh,
115    pub narrow_phase: &'a mut NarrowPhase,
116    pub impulse_joints: &'a mut ImpulseJointSet,
117    pub multibody_joints: &'a mut MultibodyJointSet,
118    pub ccd_solver: &'a mut CCDSolver,
119}
120
121#[derive(Clone, Debug, Default)]
122pub struct FrameResult {
123    pub rapier_passes: u32,
124    pub contact_pairs: usize,
125    pub active_contact_pairs: usize,
126    pub contact_manifolds: usize,
127    pub accepted_impacts: usize,
128    pub rejected_below_impulse: usize,
129    pub rejected_below_speed: usize,
130    pub rejected_cooldown: usize,
131    pub support_contacts: usize,
132    pub fractures: usize,
133    pub new_bodies: usize,
134    pub split_events: usize,
135    pub split_edits: SplitEditStats,
136    pub split_sanitize_ms: f32,
137    pub split_estimate_ms: f32,
138    pub split_cohorts: Vec<SplitCohort>,
139    pub optimization: OptimizationResult,
140    pub resim_restore_ms: f32,
141    pub resim_snapshot_ms: f32,
142    pub sibling_grace_cohorts: usize,
143    pub sibling_grace_bodies: usize,
144    pub sibling_grace_pairs: usize,
145    pub sibling_grace_filtered_pairs: usize,
146}
147
148pub enum FrameDirective {
149    Resimulate,
150    Done(FrameResult),
151}
152
153#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
154struct BodyKey(u32, u32);
155
156impl From<RigidBodyHandle> for BodyKey {
157    fn from(handle: RigidBodyHandle) -> Self {
158        let (index, generation) = handle.into_raw_parts();
159        Self(index, generation)
160    }
161}
162
163#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
164struct BodyPairKey(BodyKey, BodyKey);
165
166fn canonical_body_pair_key(body1: RigidBodyHandle, body2: RigidBodyHandle) -> Option<BodyPairKey> {
167    if body1 == body2 {
168        return None;
169    }
170    let key1 = BodyKey::from(body1);
171    let key2 = BodyKey::from(body2);
172    Some(if key1 <= key2 {
173        BodyPairKey(key1, key2)
174    } else {
175        BodyPairKey(key2, key1)
176    })
177}
178
179#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
180struct ImpactCooldownKey {
181    pair: BodyPairKey,
182    target_body: BodyKey,
183}
184
185#[derive(Clone)]
186struct GraceCohort {
187    remaining_steps: u32,
188    bodies: Vec<RigidBodyHandle>,
189}
190
191#[derive(Clone)]
192struct GracePairSet {
193    remaining_steps: u32,
194    pairs: Vec<BodyPairKey>,
195}
196
197#[derive(Clone)]
198struct RuntimeHooks {
199    active_pairs: Arc<HashSet<BodyPairKey>>,
200    filtered_pairs: Arc<AtomicUsize>,
201}
202
203impl PhysicsHooks for RuntimeHooks {
204    fn filter_contact_pair(&self, context: &PairFilterContext) -> Option<SolverFlags> {
205        let Some(body1) = context.rigid_body1 else {
206            return Some(SolverFlags::default());
207        };
208        let Some(body2) = context.rigid_body2 else {
209            return Some(SolverFlags::default());
210        };
211        let Some(pair) = canonical_body_pair_key(body1, body2) else {
212            return Some(SolverFlags::default());
213        };
214        if self.active_pairs.contains(&pair) {
215            self.filtered_pairs.fetch_add(1, Ordering::Relaxed);
216            None
217        } else {
218            Some(SolverFlags::default())
219        }
220    }
221
222    fn filter_intersection_pair(&self, context: &PairFilterContext) -> bool {
223        let Some(body1) = context.rigid_body1 else {
224            return true;
225        };
226        let Some(body2) = context.rigid_body2 else {
227            return true;
228        };
229        let Some(pair) = canonical_body_pair_key(body1, body2) else {
230            return true;
231        };
232        if self.active_pairs.contains(&pair) {
233            self.filtered_pairs.fetch_add(1, Ordering::Relaxed);
234            false
235        } else {
236            true
237        }
238    }
239}
240
241#[derive(Clone, Copy)]
242struct BufferedContactForceEvent {
243    dt: Real,
244    collider1: ColliderHandle,
245    collider2: ColliderHandle,
246    total_force_magnitude: Real,
247}
248
249#[derive(Default)]
250struct BufferedPassEvents {
251    collisions: Vec<CollisionEvent>,
252    contact_forces: Vec<BufferedContactForceEvent>,
253}
254
255pub struct PassAdapter<'a, H: PhysicsHooks + ?Sized, E: EventHandler + ?Sized> {
256    runtime: RuntimeHooks,
257    user_hooks: &'a H,
258    user_events: &'a E,
259    buffered_events: Arc<Mutex<BufferedPassEvents>>,
260}
261
262impl<H, E> PhysicsHooks for PassAdapter<'_, H, E>
263where
264    H: PhysicsHooks + ?Sized,
265    E: EventHandler + ?Sized,
266{
267    fn filter_contact_pair(&self, context: &PairFilterContext) -> Option<SolverFlags> {
268        self.runtime
269            .filter_contact_pair(context)
270            .and_then(|_| self.user_hooks.filter_contact_pair(context))
271    }
272
273    fn filter_intersection_pair(&self, context: &PairFilterContext) -> bool {
274        self.runtime.filter_intersection_pair(context)
275            && self.user_hooks.filter_intersection_pair(context)
276    }
277
278    fn modify_solver_contacts(&self, context: &mut ContactModificationContext) {
279        self.user_hooks.modify_solver_contacts(context);
280    }
281}
282
283impl<H, E> EventHandler for PassAdapter<'_, H, E>
284where
285    H: PhysicsHooks + Sync + ?Sized,
286    E: EventHandler + ?Sized,
287{
288    fn handle_collision_event(
289        &self,
290        _bodies: &RigidBodySet,
291        _colliders: &ColliderSet,
292        event: CollisionEvent,
293        _contact_pair: Option<&ContactPair>,
294    ) {
295        self.buffered_events
296            .lock()
297            .expect("buffered pass events mutex poisoned")
298            .collisions
299            .push(event);
300    }
301
302    fn handle_contact_force_event(
303        &self,
304        dt: Real,
305        _bodies: &RigidBodySet,
306        _colliders: &ColliderSet,
307        contact_pair: &ContactPair,
308        total_force_magnitude: Real,
309    ) {
310        self.buffered_events
311            .lock()
312            .expect("buffered pass events mutex poisoned")
313            .contact_forces
314            .push(BufferedContactForceEvent {
315                dt,
316                collider1: contact_pair.collider1,
317                collider2: contact_pair.collider2,
318                total_force_magnitude,
319            });
320    }
321}
322
323#[derive(Clone, Copy, Debug, Default)]
324struct GraceStepStats {
325    active_cohorts: usize,
326    active_bodies: usize,
327    active_pairs: usize,
328    filtered_pairs: usize,
329}
330
331struct GraceState {
332    sibling_steps: u32,
333    impact_source_steps: u32,
334    cohorts: Vec<GraceCohort>,
335    pair_sets: Vec<GracePairSet>,
336    active_pairs: Arc<HashSet<BodyPairKey>>,
337    filtered_pairs: Arc<AtomicUsize>,
338}
339
340impl GraceState {
341    fn new(options: GracePeriodOptions) -> Self {
342        Self {
343            sibling_steps: options.sibling_steps,
344            impact_source_steps: options.impact_source_steps,
345            cohorts: Vec::new(),
346            pair_sets: Vec::new(),
347            active_pairs: Arc::new(HashSet::new()),
348            filtered_pairs: Arc::new(AtomicUsize::new(0)),
349        }
350    }
351
352    fn set_options(&mut self, options: GracePeriodOptions) {
353        self.sibling_steps = options.sibling_steps;
354        self.impact_source_steps = options.impact_source_steps;
355        self.cohorts.clear();
356        self.pair_sets.clear();
357        self.rebuild_pairs();
358    }
359
360    fn register_split_cohorts(&mut self, split_cohorts: &[SplitCohort]) {
361        if self.sibling_steps == 0 {
362            return;
363        }
364        for cohort in split_cohorts {
365            let unique_bodies: Vec<_> = cohort
366                .target_bodies
367                .iter()
368                .copied()
369                .collect::<HashSet<_>>()
370                .into_iter()
371                .collect();
372            if unique_bodies.len() > 1 {
373                self.cohorts.push(GraceCohort {
374                    remaining_steps: self.sibling_steps,
375                    bodies: unique_bodies,
376                });
377            }
378        }
379        self.rebuild_pairs();
380    }
381
382    fn register_impact_fracture_pairs(
383        &mut self,
384        impact_sources: &HashSet<RigidBodyHandle>,
385        impacted_bodies: &HashSet<RigidBodyHandle>,
386        split_cohorts: &[SplitCohort],
387    ) {
388        if self.impact_source_steps == 0
389            || impact_sources.is_empty()
390            || impacted_bodies.is_empty()
391            || split_cohorts.is_empty()
392        {
393            return;
394        }
395
396        let matching_targets: Vec<Vec<RigidBodyHandle>> = split_cohorts
397            .iter()
398            .filter(|cohort| {
399                cohort
400                    .source_bodies
401                    .iter()
402                    .any(|body| impacted_bodies.contains(body))
403            })
404            .map(|cohort| {
405                cohort
406                    .target_bodies
407                    .iter()
408                    .chain(cohort.source_bodies.iter())
409                    .copied()
410                    .collect()
411            })
412            .collect();
413
414        for &source_body in impact_sources {
415            let mut pairs = Vec::new();
416            let mut seen = HashSet::new();
417            for targets in &matching_targets {
418                for &target_body in targets {
419                    let Some(pair) = canonical_body_pair_key(source_body, target_body) else {
420                        continue;
421                    };
422                    if seen.insert(pair) {
423                        pairs.push(pair);
424                    }
425                }
426            }
427            if !pairs.is_empty() {
428                self.pair_sets.push(GracePairSet {
429                    remaining_steps: self.impact_source_steps,
430                    pairs,
431                });
432            }
433        }
434        self.rebuild_pairs();
435    }
436
437    fn begin_step(&self) -> (RuntimeHooks, GraceStepStats) {
438        self.filtered_pairs.store(0, Ordering::Relaxed);
439        let unique_bodies = self
440            .cohorts
441            .iter()
442            .flat_map(|cohort| cohort.bodies.iter().copied())
443            .collect::<HashSet<_>>()
444            .len();
445        (
446            RuntimeHooks {
447                active_pairs: Arc::clone(&self.active_pairs),
448                filtered_pairs: Arc::clone(&self.filtered_pairs),
449            },
450            GraceStepStats {
451                active_cohorts: self.cohorts.len(),
452                active_bodies: unique_bodies,
453                active_pairs: self.active_pairs.len(),
454                filtered_pairs: 0,
455            },
456        )
457    }
458
459    fn finish_step(&mut self, mut stats: GraceStepStats) -> GraceStepStats {
460        stats.filtered_pairs = self.filtered_pairs.swap(0, Ordering::Relaxed);
461        for cohort in &mut self.cohorts {
462            cohort.remaining_steps = cohort.remaining_steps.saturating_sub(1);
463        }
464        for pair_set in &mut self.pair_sets {
465            pair_set.remaining_steps = pair_set.remaining_steps.saturating_sub(1);
466        }
467        self.cohorts.retain(|cohort| cohort.remaining_steps > 0);
468        self.pair_sets
469            .retain(|pair_set| pair_set.remaining_steps > 0);
470        self.rebuild_pairs();
471        stats
472    }
473
474    fn rebuild_pairs(&mut self) {
475        let mut active_pairs = HashSet::new();
476        for cohort in &self.cohorts {
477            for i in 0..cohort.bodies.len() {
478                for j in (i + 1)..cohort.bodies.len() {
479                    if let Some(pair) = canonical_body_pair_key(cohort.bodies[i], cohort.bodies[j])
480                    {
481                        active_pairs.insert(pair);
482                    }
483                }
484            }
485        }
486        for pair_set in &self.pair_sets {
487            for &pair in &pair_set.pairs {
488                active_pairs.insert(pair);
489            }
490        }
491        self.active_pairs = Arc::new(active_pairs);
492    }
493}
494
495#[derive(Clone)]
496struct ImpactCandidate {
497    cooldown_key: ImpactCooldownKey,
498    node_index: u32,
499    target_body: RigidBodyHandle,
500    total_impulse: f32,
501    closing_speed: f32,
502    local_point: SolverVec3,
503    local_force: Vector<Real>,
504    is_internal: bool,
505}
506
507#[derive(Clone, Copy)]
508struct BodyMotionSnapshot {
509    position: Isometry<Real>,
510    linvel: Vector<Real>,
511    angvel: AngVector<Real>,
512}
513
514#[derive(Default)]
515struct PassAnalysis {
516    support_contacts: HashSet<RigidBodyHandle>,
517    impact_candidates: Vec<ImpactCandidate>,
518    impact_sources: HashSet<RigidBodyHandle>,
519    impacted_bodies: HashSet<RigidBodyHandle>,
520    contact_pairs: usize,
521    active_contact_pairs: usize,
522    contact_manifolds: usize,
523}
524
525struct ActiveFrameState {
526    now_secs: f32,
527    dt: f32,
528    remaining_resim_passes: usize,
529    snapshot: Option<BodySnapshots>,
530    pre_step_motion: HashMap<RigidBodyHandle, BodyMotionSnapshot>,
531    impact_sources: HashSet<RigidBodyHandle>,
532    pending_grace_stats: Option<GraceStepStats>,
533    result: FrameResult,
534}
535
536pub struct DestructionRuntime {
537    destructible: DestructibleSet,
538    options: DestructionRuntimeOptions,
539    cooldowns: HashMap<ImpactCooldownKey, f32>,
540    grace: GraceState,
541    frame: Option<ActiveFrameState>,
542}
543
544impl DestructionRuntime {
545    /// Creates a runtime around an existing low-level [`DestructibleSet`].
546    pub fn new(destructible: DestructibleSet, options: DestructionRuntimeOptions) -> Self {
547        let mut runtime = Self {
548            destructible,
549            options,
550            cooldowns: HashMap::new(),
551            grace: GraceState::new(options.grace),
552            frame: None,
553        };
554        runtime.apply_destructible_options(options.destructible);
555        runtime
556    }
557
558    /// Builds a high-level runtime directly from a scenario description.
559    pub fn from_scenario(
560        scenario: &ScenarioDesc,
561        settings: SolverSettings,
562        gravity: Vec3,
563        fracture_policy: FracturePolicy,
564        options: DestructionRuntimeOptions,
565    ) -> Option<Self> {
566        let destructible =
567            DestructibleSet::from_scenario(scenario, settings, gravity, fracture_policy)?;
568        Some(Self::new(destructible, options))
569    }
570
571    pub fn options(&self) -> DestructionRuntimeOptions {
572        self.options
573    }
574
575    pub fn set_options(&mut self, options: DestructionRuntimeOptions) {
576        self.options = options;
577        self.grace.set_options(options.grace);
578        self.apply_destructible_options(options.destructible);
579    }
580
581    pub fn into_inner(self) -> DestructibleSet {
582        self.destructible
583    }
584
585    pub fn initialize(
586        &mut self,
587        bodies: &mut RigidBodySet,
588        colliders: &mut ColliderSet,
589    ) -> Vec<RigidBodyHandle> {
590        self.destructible.initialize(bodies, colliders)
591    }
592
593    pub fn set_ground_body_handle(&mut self, handle: Option<RigidBodyHandle>) {
594        self.destructible.set_ground_body_handle(handle);
595    }
596
597    pub fn refresh_collision_groups(&mut self, bodies: &RigidBodySet, colliders: &mut ColliderSet) {
598        self.destructible
599            .refresh_collision_groups(bodies, colliders);
600    }
601
602    pub fn set_sleep_thresholds(&mut self, options: SleepThresholdOptions) {
603        self.destructible.set_sleep_thresholds(options);
604    }
605
606    pub fn set_small_body_damping(&mut self, options: SmallBodyDampingOptions) {
607        self.destructible.set_small_body_damping(options);
608    }
609
610    pub fn set_debris_cleanup(&mut self, options: DebrisCleanupOptions) {
611        self.destructible.set_debris_cleanup(options);
612    }
613
614    pub fn set_debris_collision_mode(&mut self, mode: DebrisCollisionMode) {
615        self.destructible.set_debris_collision_mode(mode);
616    }
617
618    pub fn set_resimulation_options(&mut self, options: super::resimulation::ResimulationOptions) {
619        self.destructible.set_resimulation_options(options);
620    }
621
622    pub fn set_dynamic_body_ccd_enabled(&mut self, enabled: bool) {
623        self.destructible.set_dynamic_body_ccd_enabled(enabled);
624    }
625
626    pub fn set_split_child_recentering_enabled(&mut self, enabled: bool) {
627        self.destructible
628            .set_split_child_recentering_enabled(enabled);
629    }
630
631    pub fn set_split_child_velocity_fit_enabled(&mut self, enabled: bool) {
632        self.destructible
633            .set_split_child_velocity_fit_enabled(enabled);
634    }
635
636    pub fn set_skip_single_bodies(&mut self, enabled: bool) {
637        self.destructible.set_skip_single_bodies(enabled);
638    }
639
640    /// Begins an advanced multi-pass frame.
641    ///
642    /// Most consumers should prefer [`Self::step_frame`], which wraps the full
643    /// destruction-aware Rapier loop for a frame.
644    pub fn begin_frame(&mut self, now_secs: f32, dt: f32, bodies: &RigidBodySet) {
645        let resimulation = self.destructible.resimulation_options();
646        let mut result = FrameResult::default();
647        let snapshot = if self.destructible.needs_resimulation_snapshot() {
648            let snapshot_started_at = Instant::now();
649            let captured = self.destructible.capture_resimulation_snapshot(bodies);
650            result.resim_snapshot_ms +=
651                snapshot_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
652            Some(captured)
653        } else {
654            None
655        };
656        self.frame = Some(ActiveFrameState {
657            now_secs,
658            dt,
659            remaining_resim_passes: if resimulation.enabled {
660                resimulation.max_passes
661            } else {
662                0
663            },
664            snapshot,
665            pre_step_motion: capture_body_motion(bodies),
666            impact_sources: HashSet::new(),
667            pending_grace_stats: None,
668            result,
669        });
670    }
671
672    pub fn begin_pass<'a, H, E>(
673        &mut self,
674        user_hooks: &'a H,
675        user_events: &'a E,
676    ) -> PassAdapter<'a, H, E>
677    where
678        H: PhysicsHooks + ?Sized,
679        E: EventHandler + ?Sized,
680    {
681        let (runtime_hooks, stats) = self.grace.begin_step();
682        if let Some(frame) = self.frame.as_mut() {
683            frame.pending_grace_stats = Some(stats);
684        }
685        PassAdapter {
686            runtime: runtime_hooks,
687            user_hooks,
688            user_events,
689            buffered_events: Arc::new(Mutex::new(BufferedPassEvents::default())),
690        }
691    }
692
693    /// Completes one speculative Rapier pass started by [`Self::begin_frame`].
694    pub fn finish_pass<H, E>(
695        &mut self,
696        pass: PassAdapter<'_, H, E>,
697        world: &mut RapierWorldAccess<'_>,
698    ) -> FrameDirective
699    where
700        H: PhysicsHooks + Sync + ?Sized,
701        E: EventHandler + ?Sized,
702    {
703        let mut frame = self
704            .frame
705            .take()
706            .expect("begin_frame must be called before finish_pass");
707        frame.result.rapier_passes += 1;
708
709        self.cooldowns.retain(|_, until| *until > frame.now_secs);
710        let buffered_events = pass
711            .buffered_events
712            .lock()
713            .expect("buffered pass events mutex poisoned");
714        let analysis =
715            self.collect_pass_analysis(frame.dt, &frame.pre_step_motion, &buffered_events, world);
716        frame
717            .impact_sources
718            .extend(analysis.impact_sources.iter().copied());
719        frame.result.contact_pairs = analysis.contact_pairs;
720        frame.result.active_contact_pairs = analysis.active_contact_pairs;
721        frame.result.contact_manifolds = analysis.contact_manifolds;
722
723        self.apply_impact_candidates(frame.now_secs, &analysis, &mut frame.result);
724
725        let solver_result = self.destructible.step_with_time(
726            frame.now_secs,
727            world.bodies,
728            world.colliders,
729            world.island_manager,
730            world.impulse_joints,
731            world.multibody_joints,
732        );
733        accumulate_step_result(&mut frame.result, solver_result.clone());
734
735        if let Some(pending_stats) = frame.pending_grace_stats.take() {
736            let grace_stats = self.grace.finish_step(pending_stats);
737            frame.result.sibling_grace_cohorts = frame
738                .result
739                .sibling_grace_cohorts
740                .max(grace_stats.active_cohorts);
741            frame.result.sibling_grace_bodies = frame
742                .result
743                .sibling_grace_bodies
744                .max(grace_stats.active_bodies);
745            frame.result.sibling_grace_pairs = frame
746                .result
747                .sibling_grace_pairs
748                .max(grace_stats.active_pairs);
749            frame.result.sibling_grace_filtered_pairs += grace_stats.filtered_pairs;
750        }
751
752        if !solver_result.split_cohorts.is_empty() {
753            self.grace
754                .register_split_cohorts(&solver_result.split_cohorts);
755            self.grace.register_impact_fracture_pairs(
756                &analysis.impact_sources,
757                &analysis.impacted_bodies,
758                &solver_result.split_cohorts,
759            );
760        }
761
762        let fractured = solver_result.split_events > 0 || solver_result.new_bodies > 0;
763        if fractured && frame.remaining_resim_passes > 0 {
764            if let Some(snapshot) = frame.snapshot.as_ref() {
765                let restore_started_at = Instant::now();
766                snapshot.restore(world.bodies);
767                snapshot.restore_split_children(world.bodies, &solver_result.split_cohorts);
768                frame.result.resim_restore_ms +=
769                    restore_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
770            }
771            frame.remaining_resim_passes = frame.remaining_resim_passes.saturating_sub(1);
772            if frame.remaining_resim_passes > 0 {
773                let snapshot_started_at = Instant::now();
774                frame.snapshot = Some(
775                    self.destructible
776                        .capture_resimulation_snapshot(world.bodies),
777                );
778                frame.result.resim_snapshot_ms +=
779                    snapshot_started_at.elapsed().as_secs_f64() as f32 * 1_000.0;
780                frame.pre_step_motion = capture_body_motion(world.bodies);
781            }
782            self.frame = Some(frame);
783            return FrameDirective::Resimulate;
784        }
785
786        for body_handle in analysis.support_contacts {
787            if self.destructible.mark_body_support_contact(
788                body_handle,
789                frame.now_secs,
790                world.bodies,
791                world.colliders,
792            ) {
793                frame.result.support_contacts += 1;
794            }
795        }
796
797        if frame.result.rapier_passes > 1 {
798            self.restore_unblocked_impact_source_motion(
799                &frame.impact_sources,
800                &frame.pre_step_motion,
801                world,
802            );
803        }
804
805        frame.result.optimization = self.destructible.process_optimizations(
806            frame.now_secs,
807            world.bodies,
808            world.colliders,
809            world.island_manager,
810            world.impulse_joints,
811            world.multibody_joints,
812        );
813
814        flush_buffered_events(pass.user_events, &buffered_events, world);
815        self.frame = None;
816        FrameDirective::Done(frame.result)
817    }
818
819    /// Recommended high-level integration for existing Rapier apps.
820    ///
821    /// The consumer still owns `PhysicsPipeline::step(...)`. This helper wraps
822    /// the destruction-specific orchestration around that step: it buffers
823    /// speculative events, analyzes Rapier contacts, injects accepted impacts,
824    /// applies fractures and split edits, and resimulates the frame when body
825    /// topology changes.
826    pub fn step_frame<H, E, S>(
827        &mut self,
828        now_secs: f32,
829        dt: f32,
830        world: &mut RapierWorldAccess<'_>,
831        user_hooks: &H,
832        user_events: &E,
833        mut step: S,
834    ) -> FrameResult
835    where
836        H: PhysicsHooks + Sync + ?Sized,
837        E: EventHandler + ?Sized,
838        S: FnMut(&PassAdapter<'_, H, E>, &mut RapierWorldAccess<'_>),
839    {
840        self.begin_frame(now_secs, dt, world.bodies);
841        loop {
842            let pass = self.begin_pass(user_hooks, user_events);
843            step(&pass, world);
844            match self.finish_pass(pass, world) {
845                FrameDirective::Resimulate => continue,
846                FrameDirective::Done(result) => return result,
847            }
848        }
849    }
850
851    fn collect_pass_analysis(
852        &self,
853        dt: f32,
854        pre_step_motion: &HashMap<RigidBodyHandle, BodyMotionSnapshot>,
855        buffered_events: &BufferedPassEvents,
856        world: &RapierWorldAccess<'_>,
857    ) -> PassAnalysis {
858        let mut analysis = PassAnalysis::default();
859        let dt = dt.max(1.0e-6);
860
861        for pair in world.narrow_phase.contact_pairs() {
862            analysis.contact_pairs += 1;
863            analysis.contact_manifolds += pair.manifolds.len();
864            if !pair.has_any_active_contact {
865                continue;
866            }
867            analysis.active_contact_pairs += 1;
868
869            let Some(collider1) = world.colliders.get(pair.collider1) else {
870                continue;
871            };
872            let Some(collider2) = world.colliders.get(pair.collider2) else {
873                continue;
874            };
875            let Some(body1) = collider1.parent() else {
876                continue;
877            };
878            let Some(body2) = collider2.parent() else {
879                continue;
880            };
881
882            let Some(_body_pair_key) = canonical_body_pair_key(body1, body2) else {
883                continue;
884            };
885
886            let node1 = self.destructible.collider_node(pair.collider1);
887            let node2 = self.destructible.collider_node(pair.collider2);
888            if node1.is_none() && node2.is_none() {
889                continue;
890            }
891
892            if !self.options.contact_impacts.enabled
893                || self.options.contact_impacts.force_scale <= 0.0
894            {
895                continue;
896            }
897
898            let pair_is_destructible_on_both_sides = node1.is_some() && node2.is_some();
899
900            if let Some(candidate) = self.build_external_candidate(
901                pair,
902                dt,
903                pair.collider1,
904                body1,
905                body2,
906                node1,
907                false,
908                pair_is_destructible_on_both_sides,
909                pre_step_motion,
910                world,
911            ) {
912                analysis.impact_candidates.push(candidate);
913                analysis.impact_sources.insert(body2);
914                analysis.impacted_bodies.insert(body1);
915            }
916            if let Some(candidate) = self.build_external_candidate(
917                pair,
918                dt,
919                pair.collider2,
920                body2,
921                body1,
922                node2,
923                true,
924                pair_is_destructible_on_both_sides,
925                pre_step_motion,
926                world,
927            ) {
928                analysis.impact_candidates.push(candidate);
929                analysis.impact_sources.insert(body1);
930                analysis.impacted_bodies.insert(body2);
931            }
932        }
933
934        for &event in &buffered_events.collisions {
935            let CollisionEvent::Started(collider1_handle, collider2_handle, _) = event else {
936                continue;
937            };
938            let Some(collider1) = world.colliders.get(collider1_handle) else {
939                continue;
940            };
941            let Some(collider2) = world.colliders.get(collider2_handle) else {
942                continue;
943            };
944            let Some(body1) = collider1.parent() else {
945                continue;
946            };
947            let Some(body2) = collider2.parent() else {
948                continue;
949            };
950            if let Some(node_index) = self.destructible.collider_node(collider1_handle) {
951                if self.support_contact_for_pair(node_index, body1, collider2_handle, body2, world)
952                {
953                    analysis.support_contacts.insert(body1);
954                }
955            }
956            if let Some(node_index) = self.destructible.collider_node(collider2_handle) {
957                if self.support_contact_for_pair(node_index, body2, collider1_handle, body1, world)
958                {
959                    analysis.support_contacts.insert(body2);
960                }
961            }
962        }
963
964        analysis
965    }
966
967    fn build_external_candidate(
968        &self,
969        pair: &ContactPair,
970        dt: f32,
971        target_collider: ColliderHandle,
972        target_body: RigidBodyHandle,
973        other_body: RigidBodyHandle,
974        target_node: Option<u32>,
975        target_is_second: bool,
976        is_internal: bool,
977        pre_step_motion: &HashMap<RigidBodyHandle, BodyMotionSnapshot>,
978        world: &RapierWorldAccess<'_>,
979    ) -> Option<ImpactCandidate> {
980        let target_node = target_node?;
981        if self.destructible.is_support(target_node) {
982            return None;
983        }
984
985        let (world_point, force_world, total_impulse) =
986            self.contact_geometry(pair, target_collider, target_is_second, dt, world)?;
987        let closing_speed = self.closing_speed(
988            world_point,
989            force_world,
990            target_body,
991            other_body,
992            pre_step_motion,
993            world,
994        );
995        let Some(target_rb) = world.bodies.get(target_body) else {
996            return None;
997        };
998        let local_point = target_rb.position().inverse_transform_point(&world_point);
999        let local_force = target_rb.position().rotation.inverse() * force_world;
1000
1001        Some(ImpactCandidate {
1002            cooldown_key: ImpactCooldownKey {
1003                pair: canonical_body_pair_key(target_body, other_body)?,
1004                target_body: BodyKey::from(target_body),
1005            },
1006            node_index: target_node,
1007            target_body,
1008            total_impulse,
1009            closing_speed,
1010            local_point: SolverVec3::new(local_point.x, local_point.y, local_point.z),
1011            local_force,
1012            is_internal,
1013        })
1014    }
1015
1016    fn contact_geometry(
1017        &self,
1018        pair: &ContactPair,
1019        target_collider: ColliderHandle,
1020        target_is_second: bool,
1021        dt: f32,
1022        world: &RapierWorldAccess<'_>,
1023    ) -> Option<(Point<Real>, Vector<Real>, f32)> {
1024        let total_impulse = pair.total_impulse();
1025        let total_impulse_mag = pair.total_impulse_magnitude();
1026        if total_impulse_mag <= 1.0e-6 {
1027            return None;
1028        }
1029
1030        let (manifold, contact) = pair.find_deepest_contact()?;
1031        let target_local_point = if target_is_second {
1032            contact.local_p2
1033        } else {
1034            contact.local_p1
1035        };
1036        let collider = world.colliders.get(target_collider)?;
1037        let world_point = collider.position() * target_local_point;
1038        let force_world = if target_is_second {
1039            total_impulse / dt
1040        } else {
1041            -total_impulse / dt
1042        };
1043        let force_world = if force_world.norm_squared() > 1.0e-12 {
1044            force_world
1045        } else {
1046            let normal = if target_is_second {
1047                manifold.data.normal
1048            } else {
1049                -manifold.data.normal
1050            };
1051            normal * (total_impulse_mag / dt)
1052        };
1053        Some((world_point, force_world, total_impulse_mag))
1054    }
1055
1056    fn closing_speed(
1057        &self,
1058        world_point: Point<Real>,
1059        force_world: Vector<Real>,
1060        target_body: RigidBodyHandle,
1061        other_body: RigidBodyHandle,
1062        pre_step_motion: &HashMap<RigidBodyHandle, BodyMotionSnapshot>,
1063        world: &RapierWorldAccess<'_>,
1064    ) -> f32 {
1065        if let (Some(target_motion), Some(other_motion)) = (
1066            pre_step_motion.get(&target_body),
1067            pre_step_motion.get(&other_body),
1068        ) {
1069            let target_velocity = motion_velocity_at_point(target_motion, world_point);
1070            let other_velocity = motion_velocity_at_point(other_motion, world_point);
1071            return relative_speed_along_force(force_world, other_velocity - target_velocity);
1072        }
1073
1074        let Some(target_rb) = world.bodies.get(target_body) else {
1075            return 0.0;
1076        };
1077        let Some(other_rb) = world.bodies.get(other_body) else {
1078            return 0.0;
1079        };
1080        let target_velocity = target_rb.velocity_at_point(&world_point);
1081        let other_velocity = other_rb.velocity_at_point(&world_point);
1082        relative_speed_along_force(force_world, other_velocity - target_velocity)
1083    }
1084
1085    fn support_contact_for_pair(
1086        &self,
1087        node_index: u32,
1088        body_handle: RigidBodyHandle,
1089        other_collider: ColliderHandle,
1090        other_body: RigidBodyHandle,
1091        world: &RapierWorldAccess<'_>,
1092    ) -> bool {
1093        if self.destructible.is_support(node_index) {
1094            return false;
1095        }
1096        if other_body == body_handle {
1097            return false;
1098        }
1099        world
1100            .bodies
1101            .get(other_body)
1102            .map(|body| body.is_fixed())
1103            .unwrap_or(false)
1104            || self
1105                .destructible
1106                .collider_node(other_collider)
1107                .map(|other_node| self.destructible.is_support(other_node))
1108                .unwrap_or(false)
1109            || self.destructible.body_has_support(other_body)
1110    }
1111
1112    fn apply_impact_candidates(
1113        &mut self,
1114        now_secs: f32,
1115        analysis: &PassAnalysis,
1116        result: &mut FrameResult,
1117    ) {
1118        let mut candidates = analysis.impact_candidates.clone();
1119        candidates.sort_by(|a, b| {
1120            b.total_impulse
1121                .partial_cmp(&a.total_impulse)
1122                .unwrap_or(std::cmp::Ordering::Equal)
1123        });
1124
1125        for candidate in candidates {
1126            let min_speed = if candidate.is_internal {
1127                self.options.contact_impacts.min_internal_speed
1128            } else {
1129                self.options.contact_impacts.min_external_speed
1130            };
1131            if candidate.total_impulse < self.options.contact_impacts.min_total_impulse {
1132                result.rejected_below_impulse += 1;
1133                continue;
1134            }
1135            if candidate.closing_speed < min_speed {
1136                result.rejected_below_speed += 1;
1137                continue;
1138            }
1139            if let Some(until) = self.cooldowns.get(&candidate.cooldown_key) {
1140                if *until > now_secs {
1141                    result.rejected_cooldown += 1;
1142                    continue;
1143                }
1144            }
1145
1146            let mut local_force = candidate.local_force;
1147            let mut force_scale = self.options.contact_impacts.force_scale
1148                * if candidate.is_internal {
1149                    self.options.contact_impacts.internal_contact_scale
1150                } else {
1151                    1.0
1152                };
1153            let scaled_mag = local_force.norm() * force_scale;
1154            if self.options.contact_impacts.max_force_magnitude.is_finite()
1155                && self.options.contact_impacts.max_force_magnitude > 0.0
1156                && scaled_mag > self.options.contact_impacts.max_force_magnitude
1157            {
1158                force_scale *= self.options.contact_impacts.max_force_magnitude / scaled_mag;
1159            }
1160            local_force *= force_scale;
1161            if local_force.norm_squared() <= 1.0e-12 {
1162                continue;
1163            }
1164
1165            self.apply_localized_force(
1166                candidate.target_body,
1167                candidate.node_index,
1168                candidate.local_point,
1169                local_force,
1170            );
1171
1172            self.cooldowns.insert(
1173                candidate.cooldown_key,
1174                now_secs + self.options.contact_impacts.cooldown_secs.max(0.0),
1175            );
1176            result.accepted_impacts += 1;
1177        }
1178    }
1179
1180    fn apply_localized_force(
1181        &mut self,
1182        body_handle: RigidBodyHandle,
1183        node_index: u32,
1184        hit_local: SolverVec3,
1185        local_force: Vector<Real>,
1186    ) {
1187        let splash_radius = self.options.contact_impacts.splash_radius.max(0.0);
1188        let splash_exp = self
1189            .options
1190            .contact_impacts
1191            .splash_falloff_exponent
1192            .max(0.01);
1193
1194        let impacted_nodes: Vec<(u32, SolverVec3, f32)> = self
1195            .destructible
1196            .body_nodes_slice(body_handle)
1197            .iter()
1198            .copied()
1199            .filter_map(|other_node| {
1200                let other_local = self.destructible.node_local_offset(other_node)?;
1201                let dx = other_local.x - hit_local.x;
1202                let dy = other_local.y - hit_local.y;
1203                let dz = other_local.z - hit_local.z;
1204                let dist = (dx * dx + dy * dy + dz * dz).sqrt();
1205                let weight = if other_node == node_index {
1206                    1.0
1207                } else if splash_radius <= 0.0 || dist > splash_radius {
1208                    0.0
1209                } else {
1210                    (1.0 - dist / splash_radius).max(0.0).powf(splash_exp)
1211                };
1212                (weight > 0.0).then_some((other_node, other_local, weight))
1213            })
1214            .collect();
1215
1216        for (other_node, other_local, weight) in impacted_nodes {
1217            self.destructible.add_force(
1218                other_node,
1219                other_local,
1220                SolverVec3::new(
1221                    local_force.x * weight,
1222                    local_force.y * weight,
1223                    local_force.z * weight,
1224                ),
1225            );
1226        }
1227    }
1228
1229    fn apply_destructible_options(&mut self, options: DestructibleRuntimeOptions) {
1230        if let Some(value) = options.sleep_thresholds {
1231            self.destructible.set_sleep_thresholds(value);
1232        }
1233        if let Some(value) = options.small_body_damping {
1234            self.destructible.set_small_body_damping(value);
1235        }
1236        if let Some(value) = options.debris_cleanup {
1237            self.destructible.set_debris_cleanup(value);
1238        }
1239        if let Some(value) = options.debris_collision_mode {
1240            self.destructible.set_debris_collision_mode(value);
1241        }
1242        if let Some(value) = options.split_child_recentering_enabled {
1243            self.destructible.set_split_child_recentering_enabled(value);
1244        }
1245        if let Some(value) = options.split_child_velocity_fit_enabled {
1246            self.destructible
1247                .set_split_child_velocity_fit_enabled(value);
1248        }
1249        if let Some(value) = options.skip_single_bodies {
1250            self.destructible.set_skip_single_bodies(value);
1251        }
1252    }
1253
1254    fn restore_unblocked_impact_source_motion(
1255        &self,
1256        impact_sources: &HashSet<RigidBodyHandle>,
1257        pre_step_motion: &HashMap<RigidBodyHandle, BodyMotionSnapshot>,
1258        world: &mut RapierWorldAccess<'_>,
1259    ) {
1260        for &body_handle in impact_sources {
1261            if !self.destructible.body_nodes_slice(body_handle).is_empty() {
1262                continue;
1263            }
1264            if source_has_active_destructible_contact(body_handle, world, &self.destructible) {
1265                continue;
1266            }
1267            let Some(motion) = pre_step_motion.get(&body_handle) else {
1268                continue;
1269            };
1270            let Some(body) = world.bodies.get_mut(body_handle) else {
1271                continue;
1272            };
1273            body.set_linvel(motion.linvel, true);
1274            body.set_angvel(motion.angvel, true);
1275            body.wake_up(true);
1276        }
1277    }
1278}
1279
1280impl Deref for DestructionRuntime {
1281    type Target = DestructibleSet;
1282
1283    fn deref(&self) -> &Self::Target {
1284        &self.destructible
1285    }
1286}
1287
1288impl DerefMut for DestructionRuntime {
1289    fn deref_mut(&mut self) -> &mut Self::Target {
1290        &mut self.destructible
1291    }
1292}
1293
1294fn accumulate_step_result(result: &mut FrameResult, step: StepResult) {
1295    result.fractures += step.fractures;
1296    result.new_bodies += step.new_bodies;
1297    result.split_events += step.split_events;
1298    result.split_edits.plan_ms += step.split_edits.plan_ms;
1299    result.split_edits.apply_ms += step.split_edits.apply_ms;
1300    result.split_edits.child_pose_ms += step.split_edits.child_pose_ms;
1301    result.split_edits.velocity_fit_ms += step.split_edits.velocity_fit_ms;
1302    result.split_edits.sleep_init_ms += step.split_edits.sleep_init_ms;
1303    result.split_edits.body_create_ms += step.split_edits.body_create_ms;
1304    result.split_edits.collider_move_ms += step.split_edits.collider_move_ms;
1305    result.split_edits.collider_insert_ms += step.split_edits.collider_insert_ms;
1306    result.split_edits.body_retire_ms += step.split_edits.body_retire_ms;
1307    result.split_edits.reused_bodies += step.split_edits.reused_bodies;
1308    result.split_edits.recycled_bodies += step.split_edits.recycled_bodies;
1309    result.split_edits.created_bodies += step.split_edits.created_bodies;
1310    result.split_edits.retired_bodies += step.split_edits.retired_bodies;
1311    result.split_edits.body_type_flips += step.split_edits.body_type_flips;
1312    result.split_edits.moved_colliders += step.split_edits.moved_colliders;
1313    result.split_edits.inserted_colliders += step.split_edits.inserted_colliders;
1314    result.split_edits.removed_colliders += step.split_edits.removed_colliders;
1315    result.split_sanitize_ms += step.split_sanitize_ms;
1316    result.split_estimate_ms += step.split_estimate_ms;
1317    result.split_cohorts.extend(step.split_cohorts);
1318}
1319
1320fn capture_body_motion(set: &RigidBodySet) -> HashMap<RigidBodyHandle, BodyMotionSnapshot> {
1321    set.iter()
1322        .map(|(handle, body)| {
1323            (
1324                handle,
1325                BodyMotionSnapshot {
1326                    position: *body.position(),
1327                    linvel: *body.linvel(),
1328                    angvel: *body.angvel(),
1329                },
1330            )
1331        })
1332        .collect()
1333}
1334
1335fn source_has_active_destructible_contact(
1336    body_handle: RigidBodyHandle,
1337    world: &RapierWorldAccess<'_>,
1338    destructible: &DestructibleSet,
1339) -> bool {
1340    let Some(body) = world.bodies.get(body_handle) else {
1341        return false;
1342    };
1343
1344    for &collider_handle in body.colliders() {
1345        for pair in world.narrow_phase.contact_pairs_with(collider_handle) {
1346            if !pair.has_any_active_contact {
1347                continue;
1348            }
1349            let other_collider = if pair.collider1 == collider_handle {
1350                pair.collider2
1351            } else {
1352                pair.collider1
1353            };
1354            if destructible.collider_node(other_collider).is_some() {
1355                return true;
1356            }
1357        }
1358    }
1359    false
1360}
1361
1362fn motion_velocity_at_point(
1363    snapshot: &BodyMotionSnapshot,
1364    world_point: Point<Real>,
1365) -> Vector<Real> {
1366    let offset = world_point.coords - snapshot.position.translation.vector;
1367    snapshot.linvel + snapshot.angvel.cross(&offset)
1368}
1369
1370fn relative_speed_along_force(force_world: Vector<Real>, relative_velocity: Vector<Real>) -> f32 {
1371    if force_world.norm_squared() <= 1.0e-12 {
1372        return relative_velocity.norm();
1373    }
1374    let direction = force_world.normalize();
1375    let projected = relative_velocity.dot(&direction).abs();
1376    if projected > 1.0e-5 {
1377        projected
1378    } else {
1379        relative_velocity.norm()
1380    }
1381}
1382
1383fn flush_buffered_events<E: EventHandler + ?Sized>(
1384    user_events: &E,
1385    buffered_events: &BufferedPassEvents,
1386    world: &RapierWorldAccess<'_>,
1387) {
1388    for &event in &buffered_events.collisions {
1389        let contact_pair = match event {
1390            CollisionEvent::Started(collider1, collider2, _)
1391            | CollisionEvent::Stopped(collider1, collider2, _) => {
1392                world.narrow_phase.contact_pair(collider1, collider2)
1393            }
1394        };
1395        user_events.handle_collision_event(world.bodies, world.colliders, event, contact_pair);
1396    }
1397
1398    for event in &buffered_events.contact_forces {
1399        let Some(contact_pair) = world
1400            .narrow_phase
1401            .contact_pair(event.collider1, event.collider2)
1402        else {
1403            continue;
1404        };
1405        user_events.handle_contact_force_event(
1406            event.dt,
1407            world.bodies,
1408            world.colliders,
1409            contact_pair,
1410            event.total_force_magnitude,
1411        );
1412    }
1413}