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 = dent_depth.min(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] = (self.deformation.data[i] + delta).min(max_d);
146            }
147        }
148    }
149}
150
151impl PhysicsModifier for PressureModifier {
152    #[inline]
153    fn modify_distance(&self, x: f32, y: f32, z: f32, original_dist: f32) -> f32 {
154        if !self.enabled {
155            return original_dist;
156        }
157
158        let mut d = original_dist;
159
160        // Permanent deformation: push surface inward (increase distance)
161        let deform = self.deformation.sample(x, y, z);
162        if deform > 0.0 {
163            d += deform;
164        }
165
166        // Internal pressure: positive pushes the surface outward (decreases the
167        // distance), negative pulls it inward
168        d -= self.config.internal_pressure * self.config.expansion_rate;
169
170        d
171    }
172
173    fn update(&mut self, dt: f32) {
174        if !self.enabled {
175            return;
176        }
177
178        // 1. Yield deformation from high pressure
179        self.yield_deformation(dt);
180
181        // 2. Diffuse pressure
182        self.pressure.diffuse(dt, self.config.diffusion_rate);
183
184        // 3. Decay pressure (elastic recovery for transient pressure)
185        self.pressure.decay(self.config.decay_rate, dt);
186
187        // 4. Clamp deformation
188        self.deformation.clamp(0.0, self.config.max_deformation);
189    }
190
191    fn name(&self) -> &'static str {
192        "pressure"
193    }
194
195    fn is_active(&self) -> bool {
196        self.enabled
197    }
198}
199
200// ============================================================================
201// World participant
202// ============================================================================
203
204impl PressureModifier {
205    /// Snapshot tag of this type as a world participant: `"PRES"`, the four
206    /// ASCII bytes read big endian. Never changes.
207    pub const PARTICIPANT_KIND: ParticipantKind =
208        ParticipantKind::new(u32::from_be_bytes(*b"PRES"));
209
210    fn decode_state(bytes: &[u8]) -> Result<Self, StateError> {
211        let mut r = StateReader::new(bytes)?;
212        let config = PressureConfig {
213            diffusion_rate: r.f32()?,
214            decay_rate: r.f32()?,
215            yield_threshold: r.f32()?,
216            deformation_rate: r.f32()?,
217            max_deformation: r.f32()?,
218            internal_pressure: r.f32()?,
219            expansion_rate: r.f32()?,
220        };
221        let enabled = r.bool()?;
222        let pressure = r.field()?;
223        let deformation = r.field()?;
224        r.finish()?;
225        Ok(Self {
226            config,
227            pressure,
228            deformation,
229            enabled,
230        })
231    }
232}
233
234/// One `update(h)` per world substep (`h` converted with
235/// [`Fix128::to_f32`]); see the module documentation of
236/// [`crate::sim_modifier`] for the payload and what is not coupled yet.
237///
238/// Observations: channel 0 the highest cell pressure (absent for a grid
239/// without cells), channel 1 the total permanent deformation.
240impl Participant for PressureModifier {
241    fn kind(&self) -> ParticipantKind {
242        Self::PARTICIPANT_KIND
243    }
244
245    fn substep(&mut self, _ctx: &mut SubstepCtx<'_>, h: Fix128) -> Result<(), ParticipantFault> {
246        PhysicsModifier::update(self, h.to_f32());
247        Ok(())
248    }
249
250    fn observe(&self, out: &mut ObservationSink) {
251        observe_max(out, 0, &self.pressure);
252        observe_sum(out, 1, &self.deformation);
253    }
254
255    fn write_state(&self, out: &mut Vec<u8>) {
256        let mut w = StateWriter::new(out);
257        let c = &self.config;
258        for v in [
259            c.diffusion_rate,
260            c.decay_rate,
261            c.yield_threshold,
262            c.deformation_rate,
263            c.max_deformation,
264            c.internal_pressure,
265            c.expansion_rate,
266        ] {
267            w.f32(v);
268        }
269        w.bool(self.enabled);
270        w.field(&self.pressure);
271        w.field(&self.deformation);
272    }
273
274    fn check_state(&self, bytes: &[u8]) -> Result<(), StateError> {
275        Self::decode_state(bytes).map(drop)
276    }
277
278    fn read_state(&mut self, bytes: &[u8]) {
279        match Self::decode_state(bytes) {
280            Ok(state) => *self = state,
281            Err(e) => panic!("read_state called with a payload check_state refuses: {e:?}"),
282        }
283    }
284}
285
286// ============================================================================
287// Tests
288// ============================================================================
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use crate::sdf_collider::{ClosureSdf, SdfField};
294    use crate::sim_modifier::SingleModifiedSdf;
295
296    fn unit_sphere() -> ClosureSdf {
297        ClosureSdf::new(
298            |x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
299            |x, y, z| {
300                let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
301                if len < 1e-10 {
302                    (0.0, 1.0, 0.0)
303                } else {
304                    (x / len, y / len, z / len)
305                }
306            },
307        )
308    }
309
310    #[test]
311    fn test_pressure_no_force() {
312        let config = PressureConfig::default();
313        let modifier = PressureModifier::new(config, 8, (-2.0, -2.0, -2.0), (2.0, 2.0, 2.0));
314        let modified = SingleModifiedSdf::new(Box::new(unit_sphere()), modifier);
315
316        let d = modified.distance(2.0, 0.0, 0.0);
317        assert!(
318            (d - 1.0).abs() < 0.01,
319            "No pressure should not change SDF, got {d}"
320        );
321    }
322
323    #[test]
324    fn test_pressure_dent() {
325        let config = PressureConfig {
326            deformation_rate: 0.1,
327            max_deformation: 2.0,
328            ..Default::default()
329        };
330        let mut modifier = PressureModifier::new(config, 8, (-2.0, -2.0, -2.0), (2.0, 2.0, 2.0));
331
332        // Impact at surface point
333        modifier.apply_impact(1.0, 0.0, 0.0, 5.0, 0.5);
334
335        let deform = modifier.deformation_at(1.0, 0.0, 0.0);
336        assert!(
337            deform > 0.0,
338            "Impact should create deformation, got {deform}"
339        );
340
341        // Distance should increase (surface pushed inward)
342        let d = modifier.modify_distance(1.0, 0.0, 0.0, 0.0);
343        assert!(d > 0.0, "Dent should increase distance, got {d}");
344    }
345
346    #[test]
347    fn test_pressure_yield() {
348        let config = PressureConfig {
349            yield_threshold: 5.0,
350            deformation_rate: 0.1,
351            decay_rate: 0.0, // No recovery
352            ..Default::default()
353        };
354        let mut modifier = PressureModifier::new(config, 8, (-2.0, -2.0, -2.0), (2.0, 2.0, 2.0));
355
356        // Apply high pressure (large radius to cover grid cells)
357        modifier.apply_pressure_at(0.0, 0.0, 0.0, 200.0, 1.5);
358
359        // Simulate
360        for _ in 0..10 {
361            modifier.update(0.016);
362        }
363
364        // Deformation should have accumulated
365        let deform = modifier.deformation_at(0.0, 0.0, 0.0);
366        assert!(
367            deform > 0.0,
368            "High pressure should cause permanent deformation, got {deform}"
369        );
370    }
371
372    #[test]
373    fn test_pressure_decay() {
374        let config = PressureConfig {
375            decay_rate: 5.0,          // Fast decay
376            yield_threshold: 10000.0, // Won't yield
377            ..Default::default()
378        };
379        let mut modifier = PressureModifier::new(config, 8, (-2.0, -2.0, -2.0), (2.0, 2.0, 2.0));
380
381        modifier.apply_pressure_at(0.0, 0.0, 0.0, 100.0, 0.5);
382        let before = modifier.pressure_at(0.0, 0.0, 0.0);
383
384        for _ in 0..20 {
385            modifier.update(0.016);
386        }
387
388        let after = modifier.pressure_at(0.0, 0.0, 0.0);
389        assert!(
390            after < before * 0.5,
391            "Pressure should decay, before={before}, after={after}"
392        );
393    }
394
395    fn sample_modifier() -> PressureModifier {
396        let mut m = PressureModifier::new(
397            PressureConfig {
398                diffusion_rate: 0.125,
399                decay_rate: 0.25,
400                yield_threshold: 3.5,
401                deformation_rate: 0.75,
402                max_deformation: 1.5,
403                internal_pressure: 2.0,
404                expansion_rate: 0.0625,
405            },
406            2,
407            (-1.0, -2.0, -3.0),
408            (1.0, 2.0, 3.0),
409        );
410        for (i, v) in m.pressure.data.iter_mut().enumerate() {
411            *v = i as f32 * 0.5;
412        }
413        m.deformation.data[5] = 0.375;
414        m.enabled = false;
415        m
416    }
417
418    /// oracle: an internal pressure `p` with expansion rate `k` moves the
419    /// surface out by `p·k` (`2 · 1/16 = 1/8`); a disabled modifier leaves
420    /// the distance and its fields as they are.
421    #[test]
422    fn internal_pressure_and_disabled_modifier() {
423        let mut m = PressureModifier::new(
424            PressureConfig {
425                internal_pressure: 2.0,
426                expansion_rate: 0.0625,
427                ..Default::default()
428            },
429            1,
430            (0.0, 0.0, 0.0),
431            (1.0, 1.0, 1.0),
432        );
433        assert_eq!(m.modify_distance(0.5, 0.5, 0.5, 1.0), 0.875);
434        m.pressure.data[0] = 50.0;
435        m.enabled = false;
436        assert!(!m.is_active());
437        assert_eq!(m.modify_distance(0.5, 0.5, 0.5, 1.0), 1.0);
438        m.update(1.0);
439        assert_eq!(m.pressure.data[0], 50.0);
440        assert_eq!(m.deformation.data[0], 0.0);
441    }
442
443    /// oracle: the participant kind is `"PRES"` read big endian; a payload
444    /// written by `write_state` is accepted and read back into a different
445    /// modifier, which then has the original config, flag and fields and
446    /// writes the same bytes.
447    #[test]
448    fn participant_state_round_trip() {
449        assert_eq!(
450            PressureModifier::PARTICIPANT_KIND,
451            ParticipantKind::new(0x5052_4553)
452        );
453        let src = sample_modifier();
454        assert_eq!(Participant::kind(&src), PressureModifier::PARTICIPANT_KIND);
455        let mut bytes = Vec::new();
456        src.write_state(&mut bytes);
457        assert_eq!(src.check_state(&bytes), Ok(()));
458        let mut dst = PressureModifier::new(
459            PressureConfig::default(),
460            1,
461            (0.0, 0.0, 0.0),
462            (1.0, 1.0, 1.0),
463        );
464        dst.read_state(&bytes);
465        assert_eq!(dst.config, src.config);
466        assert!(!dst.enabled);
467        assert_eq!(dst.pressure.data, src.pressure.data);
468        assert_eq!(dst.deformation.data, src.deformation.data);
469        let mut again = Vec::new();
470        dst.write_state(&mut again);
471        assert_eq!(again, bytes);
472    }
473
474    /// oracle: the enabled flag is the byte after the version (4) and seven
475    /// `f32`s (28), offset 32; a value other than 0/1 there is refused, as
476    /// is a payload one byte short or one byte long.
477    #[test]
478    fn participant_state_refusals() {
479        let src = sample_modifier();
480        let mut bytes = Vec::new();
481        src.write_state(&mut bytes);
482        assert_eq!(bytes[32], 0);
483        let mut bad = bytes.clone();
484        bad[32] = 2;
485        assert_eq!(src.check_state(&bad), Err(StateError::InvalidValue));
486        assert!(matches!(
487            src.check_state(&bytes[..bytes.len() - 1]),
488            Err(StateError::Length { .. })
489        ));
490        let mut long = bytes;
491        long.push(0);
492        assert!(matches!(
493            src.check_state(&long),
494            Err(StateError::Length { .. })
495        ));
496    }
497
498    #[test]
499    #[should_panic(expected = "read_state called with a payload check_state refuses")]
500    fn participant_read_state_panics_on_a_refused_payload() {
501        let mut m = sample_modifier();
502        m.read_state(&[]);
503    }
504
505    /// oracle: in a world of one substep `dt = 1/4`, a one-cell modifier with
506    /// pressure 3 above the yield threshold 1, deformation rate 1, no
507    /// diffusion or decay, yields `(3 − 1)·1·(1/4) = 1/2` of deformation;
508    /// the pressure stays 3. Channel 0 is the highest pressure (3), channel
509    /// 1 the total deformation (1/2).
510    #[test]
511    fn participant_substep_in_a_world() {
512        let mut m = PressureModifier::new(
513            PressureConfig {
514                diffusion_rate: 0.0,
515                decay_rate: 0.0,
516                yield_threshold: 1.0,
517                deformation_rate: 1.0,
518                max_deformation: 10.0,
519                ..Default::default()
520            },
521            1,
522            (0.0, 0.0, 0.0),
523            (1.0, 1.0, 1.0),
524        );
525        m.pressure.data[0] = 3.0;
526        let mut world = crate::solver::PhysicsWorld::new(crate::solver::SolverConfig {
527            substeps: 1,
528            ..Default::default()
529        });
530        world.add_participant(Box::new(m)).expect("register");
531        world.step(Fix128::from_ratio(1, 4));
532        let Some(crate::world_participant::Observed::Exact(sink)) = world.observe_participant(0)
533        else {
534            panic!("observation");
535        };
536        assert_eq!(
537            sink.values(),
538            &[(0, Fix128::from_int(3)), (1, Fix128::from_ratio(1, 2))]
539        );
540    }
541}