Skip to main content

alice_physics/
pressure.rs

1//! Pressure Simulation Modifier
2//!
3//! Contact forces and impacts accumulate pressure on the SDF surface:
4//! - **Crush**: High pressure compresses the surface inward (permanent dent)
5//! - **Bulge**: Internal pressure expands the surface outward
6//! - **Dent**: Impact creates localized depression
7//!
8//! # Physics Model
9//!
10//! Pressure field accumulates from collision contacts (force × area × time).
11//! Above yield threshold, permanent deformation occurs. Below threshold,
12//! elastic spring-back (optional).
13//!
14//! Author: Moroya Sakamoto
15
16use crate::math::Fix128;
17use crate::sim_field::ScalarField3D;
18use crate::sim_modifier::PhysicsModifier;
19use crate::sim_modifier::{observe_max, observe_sum, StateReader, StateWriter};
20use crate::world_participant::{
21    ObservationSink, Participant, ParticipantFault, ParticipantKind, StateError, SubstepCtx,
22};
23
24// ============================================================================
25// Configuration
26// ============================================================================
27
28/// Pressure modifier configuration
29#[derive(Clone, Copy, Debug, PartialEq)]
30pub struct PressureConfig {
31    /// Pressure diffusion rate (how fast pressure spreads)
32    pub diffusion_rate: f32,
33    /// Pressure decay rate (elastic recovery)
34    pub decay_rate: f32,
35    /// Yield threshold (permanent deformation begins above this)
36    pub yield_threshold: f32,
37    /// Deformation rate (surface recession per unit pressure per second)
38    pub deformation_rate: f32,
39    /// Maximum deformation depth
40    pub max_deformation: f32,
41    /// Internal pressure (positive = outward expansion, negative = inward contraction)
42    pub internal_pressure: f32,
43    /// Internal pressure expansion rate
44    pub expansion_rate: f32,
45}
46
47impl Default for PressureConfig {
48    fn default() -> Self {
49        Self {
50            diffusion_rate: 0.2,
51            decay_rate: 0.5,
52            yield_threshold: 10.0,
53            deformation_rate: 0.05,
54            max_deformation: 1.0,
55            internal_pressure: 0.0,
56            expansion_rate: 0.01,
57        }
58    }
59}
60
61// ============================================================================
62// Pressure Modifier
63// ============================================================================
64
65/// Pressure-driven SDF deformation
66pub struct PressureModifier {
67    /// Configuration
68    pub config: PressureConfig,
69    /// Current pressure field (transient)
70    pub pressure: ScalarField3D,
71    /// Accumulated permanent deformation (positive = surface pushed inward)
72    pub deformation: ScalarField3D,
73    /// Whether modifier is enabled
74    pub enabled: bool,
75}
76
77impl PressureModifier {
78    /// Create a new pressure modifier
79    ///
80    /// `resolution` is clamped to a minimum of 1: `ScalarField3D` computes
81    /// its cell size from `resolution - 1` and clamps grid reads to
82    /// `resolution - 1`, both of which underflow `usize` for a resolution
83    /// of 0. Clamping here keeps that invariant instead of deferring a
84    /// panic to the first call to [`Self::pressure_at`] /
85    /// [`Self::deformation_at`].
86    #[must_use]
87    pub fn new(
88        config: PressureConfig,
89        resolution: usize,
90        min: (f32, f32, f32),
91        max: (f32, f32, f32),
92    ) -> Self {
93        let resolution = resolution.max(1);
94        Self {
95            config,
96            pressure: ScalarField3D::new(resolution, resolution, resolution, min, max),
97            deformation: ScalarField3D::new(resolution, resolution, resolution, min, max),
98            enabled: true,
99        }
100    }
101
102    /// Apply pressure at a point (e.g., from collision contact)
103    ///
104    /// `force` = contact force magnitude, `radius` = contact patch radius
105    pub fn apply_pressure_at(&mut self, x: f32, y: f32, z: f32, force: f32, radius: f32) {
106        self.pressure.splat(x, y, z, force, radius);
107    }
108
109    /// Apply impact at a point (immediate dent)
110    ///
111    /// `impulse` = impact impulse, `radius` = impact area
112    pub fn apply_impact(&mut self, x: f32, y: f32, z: f32, impulse: f32, radius: f32) {
113        let dent_depth = impulse * self.config.deformation_rate;
114        let clamped = crate::det_math::min(dent_depth, self.config.max_deformation);
115        self.deformation.splat(x, y, z, clamped, radius);
116        // impacts add into the field: keep the sum within max_deformation now,
117        // not only at the next update
118        self.deformation.clamp(0.0, self.config.max_deformation);
119    }
120
121    /// Get pressure at a point
122    #[must_use]
123    pub fn pressure_at(&self, x: f32, y: f32, z: f32) -> f32 {
124        self.pressure.sample(x, y, z)
125    }
126
127    /// Get deformation at a point
128    #[must_use]
129    pub fn deformation_at(&self, x: f32, y: f32, z: f32) -> f32 {
130        self.deformation.sample(x, y, z)
131    }
132
133    /// Accumulate deformation from pressure above yield threshold
134    fn yield_deformation(&mut self, dt: f32) {
135        let threshold = self.config.yield_threshold;
136        let rate = self.config.deformation_rate;
137        let max_d = self.config.max_deformation;
138
139        let n = self.pressure.cell_count();
140        for i in 0..n {
141            let p = self.pressure.data[i];
142            if p > threshold {
143                let excess = p - threshold;
144                let delta = excess * rate * dt;
145                self.deformation.data[i] =
146                    crate::det_math::min(self.deformation.data[i] + delta, max_d);
147            }
148        }
149    }
150}
151
152impl PhysicsModifier for PressureModifier {
153    #[inline]
154    fn modify_distance(&self, x: f32, y: f32, z: f32, original_dist: f32) -> f32 {
155        if !self.enabled {
156            return original_dist;
157        }
158
159        let mut d = original_dist;
160
161        // Permanent deformation: push surface inward (increase distance)
162        let deform = self.deformation.sample(x, y, z);
163        if deform > 0.0 {
164            d += deform;
165        }
166
167        // Internal pressure: positive pushes the surface outward (decreases the
168        // distance), negative pulls it inward
169        d -= self.config.internal_pressure * self.config.expansion_rate;
170
171        d
172    }
173
174    fn update(&mut self, dt: f32) {
175        if !self.enabled {
176            return;
177        }
178
179        // 1. Yield deformation from high pressure
180        self.yield_deformation(dt);
181
182        // 2. Diffuse pressure
183        self.pressure.diffuse(dt, self.config.diffusion_rate);
184
185        // 3. Decay pressure (elastic recovery for transient pressure)
186        self.pressure.decay(self.config.decay_rate, dt);
187
188        // 4. Clamp deformation
189        self.deformation.clamp(0.0, self.config.max_deformation);
190    }
191
192    fn name(&self) -> &'static str {
193        "pressure"
194    }
195
196    fn is_active(&self) -> bool {
197        self.enabled
198    }
199}
200
201// ============================================================================
202// World participant
203// ============================================================================
204
205impl PressureModifier {
206    /// Snapshot tag of this type as a world participant: `"PRES"`, the four
207    /// ASCII bytes read big endian. Never changes.
208    pub const PARTICIPANT_KIND: ParticipantKind =
209        ParticipantKind::new(u32::from_be_bytes(*b"PRES"));
210
211    fn decode_state(bytes: &[u8]) -> Result<Self, StateError> {
212        let mut r = StateReader::new(bytes)?;
213        let config = PressureConfig {
214            diffusion_rate: r.f32()?,
215            decay_rate: r.f32()?,
216            yield_threshold: r.f32()?,
217            deformation_rate: r.f32()?,
218            max_deformation: r.f32()?,
219            internal_pressure: r.f32()?,
220            expansion_rate: r.f32()?,
221        };
222        let enabled = r.bool()?;
223        let pressure = r.field()?;
224        let deformation = r.field()?;
225        r.finish()?;
226        Ok(Self {
227            config,
228            pressure,
229            deformation,
230            enabled,
231        })
232    }
233}
234
235/// One `update(h)` per world substep (`h` converted with
236/// [`Fix128::to_f32`]); see the module documentation of
237/// [`crate::sim_modifier`] for the payload and what is not coupled yet.
238///
239/// Observations: channel 0 the highest cell pressure (absent for a grid
240/// without cells), channel 1 the total permanent deformation.
241impl Participant for PressureModifier {
242    fn kind(&self) -> ParticipantKind {
243        Self::PARTICIPANT_KIND
244    }
245
246    fn substep(&mut self, _ctx: &mut SubstepCtx<'_>, h: Fix128) -> Result<(), ParticipantFault> {
247        PhysicsModifier::update(self, h.to_f32());
248        Ok(())
249    }
250
251    fn observe(&self, out: &mut ObservationSink) {
252        observe_max(out, 0, &self.pressure);
253        observe_sum(out, 1, &self.deformation);
254    }
255
256    fn write_state(&self, out: &mut Vec<u8>) {
257        let mut w = StateWriter::new(out);
258        let c = &self.config;
259        for v in [
260            c.diffusion_rate,
261            c.decay_rate,
262            c.yield_threshold,
263            c.deformation_rate,
264            c.max_deformation,
265            c.internal_pressure,
266            c.expansion_rate,
267        ] {
268            w.f32(v);
269        }
270        w.bool(self.enabled);
271        w.field(&self.pressure);
272        w.field(&self.deformation);
273    }
274
275    fn check_state(&self, bytes: &[u8]) -> Result<(), StateError> {
276        Self::decode_state(bytes).map(drop)
277    }
278
279    fn read_state(&mut self, bytes: &[u8]) {
280        match Self::decode_state(bytes) {
281            Ok(state) => *self = state,
282            Err(e) => panic!("read_state called with a payload check_state refuses: {e:?}"),
283        }
284    }
285}
286
287// ============================================================================
288// Tests
289// ============================================================================
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use crate::sdf_collider::{ClosureSdf, SdfField};
295    use crate::sim_modifier::SingleModifiedSdf;
296
297    fn unit_sphere() -> ClosureSdf {
298        ClosureSdf::new(
299            |x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
300            |x, y, z| {
301                let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
302                if len < 1e-10 {
303                    (0.0, 1.0, 0.0)
304                } else {
305                    (x / len, y / len, z / len)
306                }
307            },
308        )
309    }
310
311    #[test]
312    fn test_pressure_no_force() {
313        let config = PressureConfig::default();
314        let modifier = PressureModifier::new(config, 8, (-2.0, -2.0, -2.0), (2.0, 2.0, 2.0));
315        let modified = SingleModifiedSdf::new(Box::new(unit_sphere()), modifier);
316
317        let d = modified.distance(2.0, 0.0, 0.0);
318        assert!(
319            (d - 1.0).abs() < 0.01,
320            "No pressure should not change SDF, got {d}"
321        );
322    }
323
324    #[test]
325    fn test_pressure_dent() {
326        let config = PressureConfig {
327            deformation_rate: 0.1,
328            max_deformation: 2.0,
329            ..Default::default()
330        };
331        let mut modifier = PressureModifier::new(config, 8, (-2.0, -2.0, -2.0), (2.0, 2.0, 2.0));
332
333        // Impact at surface point
334        modifier.apply_impact(1.0, 0.0, 0.0, 5.0, 0.5);
335
336        let deform = modifier.deformation_at(1.0, 0.0, 0.0);
337        assert!(
338            deform > 0.0,
339            "Impact should create deformation, got {deform}"
340        );
341
342        // Distance should increase (surface pushed inward)
343        let d = modifier.modify_distance(1.0, 0.0, 0.0, 0.0);
344        assert!(d > 0.0, "Dent should increase distance, got {d}");
345    }
346
347    #[test]
348    fn test_pressure_yield() {
349        let config = PressureConfig {
350            yield_threshold: 5.0,
351            deformation_rate: 0.1,
352            decay_rate: 0.0, // No recovery
353            ..Default::default()
354        };
355        let mut modifier = PressureModifier::new(config, 8, (-2.0, -2.0, -2.0), (2.0, 2.0, 2.0));
356
357        // Apply high pressure (large radius to cover grid cells)
358        modifier.apply_pressure_at(0.0, 0.0, 0.0, 200.0, 1.5);
359
360        // Simulate
361        for _ in 0..10 {
362            modifier.update(0.016);
363        }
364
365        // Deformation should have accumulated
366        let deform = modifier.deformation_at(0.0, 0.0, 0.0);
367        assert!(
368            deform > 0.0,
369            "High pressure should cause permanent deformation, got {deform}"
370        );
371    }
372
373    #[test]
374    fn test_pressure_decay() {
375        let config = PressureConfig {
376            decay_rate: 5.0,          // Fast decay
377            yield_threshold: 10000.0, // Won't yield
378            ..Default::default()
379        };
380        let mut modifier = PressureModifier::new(config, 8, (-2.0, -2.0, -2.0), (2.0, 2.0, 2.0));
381
382        modifier.apply_pressure_at(0.0, 0.0, 0.0, 100.0, 0.5);
383        let before = modifier.pressure_at(0.0, 0.0, 0.0);
384
385        for _ in 0..20 {
386            modifier.update(0.016);
387        }
388
389        let after = modifier.pressure_at(0.0, 0.0, 0.0);
390        assert!(
391            after < before * 0.5,
392            "Pressure should decay, before={before}, after={after}"
393        );
394    }
395
396    fn sample_modifier() -> PressureModifier {
397        let mut m = PressureModifier::new(
398            PressureConfig {
399                diffusion_rate: 0.125,
400                decay_rate: 0.25,
401                yield_threshold: 3.5,
402                deformation_rate: 0.75,
403                max_deformation: 1.5,
404                internal_pressure: 2.0,
405                expansion_rate: 0.0625,
406            },
407            2,
408            (-1.0, -2.0, -3.0),
409            (1.0, 2.0, 3.0),
410        );
411        for (i, v) in m.pressure.data.iter_mut().enumerate() {
412            *v = i as f32 * 0.5;
413        }
414        m.deformation.data[5] = 0.375;
415        m.enabled = false;
416        m
417    }
418
419    /// oracle: an internal pressure `p` with expansion rate `k` moves the
420    /// surface out by `p·k` (`2 · 1/16 = 1/8`); a disabled modifier leaves
421    /// the distance and its fields as they are.
422    #[test]
423    fn internal_pressure_and_disabled_modifier() {
424        let mut m = PressureModifier::new(
425            PressureConfig {
426                internal_pressure: 2.0,
427                expansion_rate: 0.0625,
428                ..Default::default()
429            },
430            1,
431            (0.0, 0.0, 0.0),
432            (1.0, 1.0, 1.0),
433        );
434        assert_eq!(m.modify_distance(0.5, 0.5, 0.5, 1.0), 0.875);
435        m.pressure.data[0] = 50.0;
436        m.enabled = false;
437        assert!(!m.is_active());
438        assert_eq!(m.modify_distance(0.5, 0.5, 0.5, 1.0), 1.0);
439        m.update(1.0);
440        assert_eq!(m.pressure.data[0], 50.0);
441        assert_eq!(m.deformation.data[0], 0.0);
442    }
443
444    /// oracle: the participant kind is `"PRES"` read big endian; a payload
445    /// written by `write_state` is accepted and read back into a different
446    /// modifier, which then has the original config, flag and fields and
447    /// writes the same bytes.
448    #[test]
449    fn participant_state_round_trip() {
450        assert_eq!(
451            PressureModifier::PARTICIPANT_KIND,
452            ParticipantKind::new(0x5052_4553)
453        );
454        let src = sample_modifier();
455        assert_eq!(Participant::kind(&src), PressureModifier::PARTICIPANT_KIND);
456        let mut bytes = Vec::new();
457        src.write_state(&mut bytes);
458        assert_eq!(src.check_state(&bytes), Ok(()));
459        let mut dst = PressureModifier::new(
460            PressureConfig::default(),
461            1,
462            (0.0, 0.0, 0.0),
463            (1.0, 1.0, 1.0),
464        );
465        dst.read_state(&bytes);
466        assert_eq!(dst.config, src.config);
467        assert!(!dst.enabled);
468        assert_eq!(dst.pressure.data, src.pressure.data);
469        assert_eq!(dst.deformation.data, src.deformation.data);
470        let mut again = Vec::new();
471        dst.write_state(&mut again);
472        assert_eq!(again, bytes);
473    }
474
475    /// oracle: the enabled flag is the byte after the version (4) and seven
476    /// `f32`s (28), offset 32; a value other than 0/1 there is refused, as
477    /// is a payload one byte short or one byte long.
478    #[test]
479    fn participant_state_refusals() {
480        let src = sample_modifier();
481        let mut bytes = Vec::new();
482        src.write_state(&mut bytes);
483        assert_eq!(bytes[32], 0);
484        let mut bad = bytes.clone();
485        bad[32] = 2;
486        assert_eq!(src.check_state(&bad), Err(StateError::InvalidValue));
487        assert!(matches!(
488            src.check_state(&bytes[..bytes.len() - 1]),
489            Err(StateError::Length { .. })
490        ));
491        let mut long = bytes;
492        long.push(0);
493        assert!(matches!(
494            src.check_state(&long),
495            Err(StateError::Length { .. })
496        ));
497    }
498
499    #[test]
500    #[should_panic(expected = "read_state called with a payload check_state refuses")]
501    fn participant_read_state_panics_on_a_refused_payload() {
502        let mut m = sample_modifier();
503        m.read_state(&[]);
504    }
505
506    /// oracle: in a world of one substep `dt = 1/4`, a one-cell modifier with
507    /// pressure 3 above the yield threshold 1, deformation rate 1, no
508    /// diffusion or decay, yields `(3 − 1)·1·(1/4) = 1/2` of deformation;
509    /// the pressure stays 3. Channel 0 is the highest pressure (3), channel
510    /// 1 the total deformation (1/2).
511    #[test]
512    fn participant_substep_in_a_world() {
513        let mut m = PressureModifier::new(
514            PressureConfig {
515                diffusion_rate: 0.0,
516                decay_rate: 0.0,
517                yield_threshold: 1.0,
518                deformation_rate: 1.0,
519                max_deformation: 10.0,
520                ..Default::default()
521            },
522            1,
523            (0.0, 0.0, 0.0),
524            (1.0, 1.0, 1.0),
525        );
526        m.pressure.data[0] = 3.0;
527        let mut world = crate::solver::PhysicsWorld::new(crate::solver::SolverConfig {
528            substeps: 1,
529            ..Default::default()
530        });
531        world.add_participant(Box::new(m)).expect("register");
532        world.step(Fix128::from_ratio(1, 4));
533        let Some(crate::world_participant::Observed::Exact(sink)) = world.observe_participant(0)
534        else {
535            panic!("observation");
536        };
537        assert_eq!(
538            sink.values(),
539            &[(0, Fix128::from_int(3)), (1, Fix128::from_ratio(1, 2))]
540        );
541    }
542}