Skip to main content

rapier3d/geometry/narrow_phase/soft_contacts/
soft_contacts_volume.rs

1//! The volume patches of crossed closed surfaces (soft-soft and soft-rigid) and their binning into volume constraints.
2
3use crate::alloc_prelude::*;
4
5use crate::dynamics::{IntegrationParameters, RigidBodySet, SoftBody, SoftCollisionMesh};
6use crate::geometry::Collider;
7use crate::math::{DIM, Real, Vector};
8use crate::utils::{DotProduct, OrthonormalBasis};
9use parry::utils::hashmap::HashMap;
10#[cfg(not(feature = "std"))]
11#[allow(unused_imports)]
12use simba::scalar::{ComplexField as _, RealField as _};
13
14use super::soft_contacts_classify::{
15    classify_inside, classify_skin, classify_skin_rigid, ring_depths,
16};
17use super::{Side, SoftDetectionCtx, SoftRigidPatch, SoftVertexPass, SoftVolumePatch, body_frozen};
18
19/// The multi-volume grid's cells per tangent axis (see `overlap_multi_volume`).
20pub(super) fn volume_split(params: &IntegrationParameters) -> u32 {
21    if params.soft_bodies.recovery.overlap_multi_volume {
22        params.soft_bodies.recovery.overlap_split.max(1)
23    } else {
24        1
25    }
26}
27
28/// The depth of the vertices `inside` the other collider: their distance to its surface
29/// (`NEG_INFINITY` for the others). Returns whether any vertex is inside.
30pub(super) fn fill_depths(
31    mesh: &SoftCollisionMesh,
32    body: &SoftBody,
33    inside: &[bool],
34    co: &Collider,
35    depth: &mut Vec<Real>,
36) -> bool {
37    depth.clear();
38    depth.resize(mesh.vertex_count(), Real::NEG_INFINITY);
39    let mut any = false;
40    for v in 0..mesh.vertex_count() {
41        if inside[v] {
42            let p = mesh.vertex(body, v);
43            let proj = co.shape().project_point(co.position(), p, false);
44            depth[v] = (proj.point - p).length();
45            any = true;
46        }
47    }
48    any
49}
50
51/// Whether a closed mesh is self-crossed this step (part of its winding is mirrored, so its
52/// volume gradient points the wrong way there): its volume constraints sit out.
53fn self_tangled(m: &SoftCollisionMesh) -> bool {
54    m.orientation_unreliable || m.crossed_partners.contains(&m.collider())
55}
56
57/// The volume patches of a crossed pair of closed surfaces (see `SoftOverlapConstraint`, Allard et
58/// al. 2010): both intruding patches, on the skin-dilated surfaces under `overlap_skin_volume`,
59/// binned from `pass` (the vertices of `vb` against the surface of `eb`, the bins' own side).
60pub(super) fn detect_volume_patch(
61    pass: &SoftVertexPass,
62    (eb, eb_mesh, eb_handle, eb_co): Side<'_>,
63    (vb, vb_mesh, _, vb_co): Side<'_>,
64    ctx: &SoftDetectionCtx,
65) -> Option<SoftVolumePatch> {
66    let params = ctx.params;
67    let recovery = &params.soft_bodies.recovery;
68    let closed_pair = vb_mesh.is_solid() && eb_mesh.is_solid();
69    let crossed = !pass.cross_pairs.is_empty() && !pass.cross_tangled_vb_elements.is_empty();
70    if !recovery.overlap_constraints
71        || !closed_pair
72        || !crossed
73        || (recovery.overlap_skip_self_tangled && (self_tangled(vb_mesh) || self_tangled(eb_mesh)))
74    {
75        return None;
76    }
77    let skins = vb_co.contact_skin() + eb_co.contact_skin();
78    let mut depth_vb = Vec::new();
79    let mut depth_eb = Vec::new();
80    let band = params.prediction_distance() + ctx.motion_margin(eb_co) + ctx.motion_margin(vb_co);
81    let any = if recovery.overlap_skin_volume {
82        let rest = (1.0 - recovery.overlap_kept_depth.clamp(0.0, 1.0)) * skins;
83        let reach = skins + band;
84        let (a1, _) = classify_skin(
85            vb_mesh,
86            vb,
87            &pass.cross_tangled_vb_elements,
88            (eb_mesh, eb, eb_co),
89            reach,
90            rest,
91            &mut depth_vb,
92        );
93        let (a2, _) = classify_skin(
94            eb_mesh,
95            eb,
96            &pass.cross_tangled_elements,
97            (vb_mesh, vb, vb_co),
98            reach,
99            rest,
100            &mut depth_eb,
101        );
102        a1 || a2
103    } else {
104        // The geometric intersection alone: the inside vertices, at their distance to the
105        // other surface.
106        let mut inside_vb = Vec::new();
107        let mut inside_eb = Vec::new();
108        classify_inside(
109            vb_mesh,
110            vb,
111            &pass.cross_tangled_vb_elements,
112            (eb_mesh, eb),
113            &mut inside_vb,
114        );
115        classify_inside(
116            eb_mesh,
117            eb,
118            &pass.cross_tangled_elements,
119            (vb_mesh, vb),
120            &mut inside_eb,
121        );
122        let a1 = fill_depths(vb_mesh, vb, &inside_vb, eb_co, &mut depth_vb);
123        let a2 = fill_depths(eb_mesh, eb, &inside_eb, vb_co, &mut depth_eb);
124        if a1 {
125            ring_depths(vb_mesh, vb, eb_co, 0.0, &mut depth_vb);
126        }
127        if a2 {
128            ring_depths(eb_mesh, eb, vb_co, 0.0, &mut depth_eb);
129        }
130        a1 || a2
131    };
132    if !any {
133        return None;
134    }
135    let band = if recovery.overlap_skin_volume {
136        band
137    } else {
138        0.0
139    };
140    let own = VolumeSide {
141        body: eb,
142        mesh: eb_mesh,
143        vertices: patch_vertices(eb_mesh, eb, &depth_eb, band),
144    };
145    let other = VolumeSide {
146        body: vb,
147        mesh: vb_mesh,
148        vertices: patch_vertices(vb_mesh, vb, &depth_vb, band),
149    };
150    Some(SoftVolumePatch {
151        own: eb_handle,
152        bins: volume_bins(&own, Some(&other), volume_split(params)),
153        slot_offset: u32::MAX,
154    })
155}
156
157/// The volume patch of a closed surface against a rigid collider (see `SoftOverlapConstraint`):
158/// the surface vertices inside the rigid shape (plus its skin layer under the skin models), found
159/// independently of the pair's manifolds (a shape fully inside the surface has no contact).
160pub(super) fn detect_rigid_patch(
161    (sb, mesh, handle, surface_co): Side<'_>,
162    other_co: &Collider,
163    bodies: &RigidBodySet,
164    ctx: &SoftDetectionCtx,
165) -> Option<SoftRigidPatch> {
166    let params = ctx.params;
167    let recovery = &params.soft_bodies.recovery;
168    if !recovery.overlap_constraints
169        || !recovery.overlap_rigid
170        || body_frozen(sb)
171        || !mesh.collision_enabled()
172        || other_co.is_sensor()
173    {
174        return None;
175    }
176    // A shell (unoriented mesh) holds intruders instead; a self-crossed mesh sits out: part of
177    // its winding is mirrored.
178    if !mesh.is_solid()
179        || (recovery.overlap_skip_self_tangled
180            && (mesh.orientation_unreliable || mesh.crossed_partners.contains(&handle)))
181    {
182        return None;
183    }
184    let skin = recovery.overlap_skin_volume;
185    let other_rb = other_co.parent().and_then(|h| bodies.get(h));
186    let skins = surface_co.contact_skin() + other_co.contact_skin();
187    let rest = if skin {
188        (1.0 - recovery.overlap_kept_depth.clamp(0.0, 1.0)) * skins
189    } else {
190        0.0
191    };
192    let band = params.prediction_distance() + ctx.motion_margin(surface_co);
193    let reach = skin.then_some(skins + band);
194    let mut depth = Vec::new();
195    let (any, _) = classify_skin_rigid(mesh, sb, other_co, reach, rest, &mut depth);
196    if !any {
197        return None;
198    }
199    let own = VolumeSide {
200        body: sb,
201        mesh,
202        vertices: patch_vertices(mesh, sb, &depth, if skin { band } else { 0.0 }),
203    };
204    let bins = volume_bins(&own, None, volume_split(params));
205    Some(SoftRigidPatch {
206        bins,
207        depth: if recovery.overlap_patch_constraints != crate::dynamics::SoftPatchConstraints::Keep
208        {
209            depth
210        } else {
211            Vec::new()
212        },
213        com: other_rb.map_or(other_co.translation(), |rb| rb.center_of_mass()),
214        rot: other_co.position().rotation,
215    })
216}
217
218/// Accumulates, per vertex of `mesh`, the gradient of the enclosed area/volume over the elements
219/// `keep` accepts (weighted as it returns); translation-invariant per element, so a subset of a
220/// closed boundary is fine. `sign` flips the gradient (a mirrored lobe raises its negative area).
221fn partial_volume_gradients(
222    mesh: &SoftCollisionMesh,
223    body: &SoftBody,
224    sign: Real,
225    mut keep: impl FnMut(usize) -> Option<Real>,
226    grads: &mut [Vector],
227) {
228    for (f, el) in mesh.indices().iter().enumerate() {
229        let Some(w) = keep(f) else {
230            continue;
231        };
232        #[cfg(feature = "dim2")]
233        {
234            let (a, b) = (
235                mesh.vertex(body, el[0] as usize),
236                mesh.vertex(body, el[1] as usize),
237            );
238            let d = b - a;
239            let g = Vector::new(d.y, -d.x) * (0.5 * w * sign);
240            grads[el[0] as usize] += g;
241            grads[el[1] as usize] += g;
242        }
243        #[cfg(feature = "dim3")]
244        {
245            let (a, b, c) = (
246                mesh.vertex(body, el[0] as usize),
247                mesh.vertex(body, el[1] as usize),
248                mesh.vertex(body, el[2] as usize),
249            );
250            let n = (b - a).cross(c - a);
251            let g = n * (w * sign / 6.0);
252            for &v in el {
253                grads[v as usize] += g;
254            }
255        }
256    }
257}
258
259/// A vertex of a volume contact's patch: its share of the (partial) volume gradient and its
260/// depth in the other side's volume (the skin-dilated one under the skin models, where it
261/// is negative in the speculative band and above the kept layer).
262#[derive(Copy, Clone)]
263pub(super) struct PatchVertex {
264    vertex: u32,
265    grad: Vector,
266    depth: Real,
267}
268
269/// One side of a volume contact: its patch vertices.
270pub(super) struct VolumeSide<'a> {
271    pub(super) body: &'a SoftBody,
272    pub(super) mesh: &'a SoftCollisionMesh,
273    pub(super) vertices: Vec<PatchVertex>,
274}
275
276/// One volume constraint of a pair (see `SoftOverlapConstraint`): the whole patch, or a cell of
277/// the multi-volume grid (Allard et al. 2010, section 5), as particle entries (particle,
278/// gradient) per side.
279#[derive(Clone, Debug)]
280pub struct VolumeBin {
281    /// The own side's entries: (particle, volume gradient).
282    pub own: Vec<(u32, Vector)>,
283    /// The other side's entries: (particle, volume gradient); empty against a rigid collider.
284    pub other: Vec<(u32, Vector)>,
285    /// The signed volume estimate, `sum depth |g|` over both sides (negative: slack).
286    pub volume: Real,
287    /// The contact normal, from the own side into the other (zero: degenerate).
288    pub normal: Vector,
289    /// The area-weighted center of the bin's vertices.
290    pub center: Vector,
291    /// Whether the solver builds a constraint for it (the hook clears it to drop the constraint).
292    pub enabled: bool,
293    /// The impulse the solver applied through it at the last step (its multiplier along
294    /// the own side's gradients).
295    pub impulse: Real,
296}
297
298/// The fraction of an element lying in the volume where the depth (offset by the band,
299/// see `patch_vertices`) is positive, the zero crossing interpolated linearly along its
300/// edges: exact for a segment, the standard triangle clip in 3D.
301fn clipped_fraction(s: &[Real]) -> Real {
302    let inside: Vec<usize> = (0..s.len()).filter(|&i| s[i] >= 0.0).collect();
303    if inside.len() == s.len() {
304        return 1.0;
305    }
306    if inside.is_empty() {
307        return 0.0;
308    }
309    // The inside vertex's share along an edge to an outside vertex (half the edge when
310    // that vertex has no depth: the member-fraction rule).
311    let share = |a: usize, b: usize| -> Real {
312        if s[b] == Real::NEG_INFINITY {
313            0.5
314        } else {
315            (s[a] / (s[a] - s[b])).clamp(0.0, 1.0)
316        }
317    };
318    #[cfg(feature = "dim2")]
319    {
320        let a = inside[0];
321        share(a, 1 - a)
322    }
323    #[cfg(feature = "dim3")]
324    {
325        if inside.len() == 1 {
326            let a = inside[0];
327            let (b, c) = ((a + 1) % 3, (a + 2) % 3);
328            share(a, b) * share(a, c)
329        } else {
330            let o = (0..3).find(|i| !inside.contains(i)).unwrap();
331            let (b, c) = ((o + 1) % 3, (o + 2) % 3);
332            let out_share = |i: usize| {
333                if s[o] == Real::NEG_INFINITY {
334                    0.5
335                } else {
336                    (s[i] / (s[i] - s[o])).clamp(0.0, 1.0)
337                }
338            };
339            1.0 - (1.0 - out_share(b)) * (1.0 - out_share(c))
340        }
341    }
342}
343
344/// The patch of `mesh` made of its member vertices (`depth[v]` finite): every element weighs the
345/// fraction of it within reach (depth offset by the speculative slack `band`, interpolated along
346/// its edges), and each of its vertices gets its share of the volume gradient (none if depthless).
347pub(super) fn patch_vertices(
348    mesh: &SoftCollisionMesh,
349    body: &SoftBody,
350    depth: &[Real],
351    band: Real,
352) -> Vec<PatchVertex> {
353    let member = |v: usize| depth[v] > Real::NEG_INFINITY;
354    let mut grads = vec![Vector::ZERO; mesh.vertex_count()];
355    // The gradient follows the geometric orientation, not the raw winding: a loop turned
356    // inside out (crushed against a wall, no self-crossing) would otherwise be inflated
357    // out of the wall instead of shrunk.
358    partial_volume_gradients(
359        mesh,
360        body,
361        mesh.winding_sign(),
362        |f| {
363            let el = mesh.element(f);
364            let s: [Real; DIM] = core::array::from_fn(|k| {
365                el.get(k).map_or(Real::NEG_INFINITY, |&v| {
366                    let d = depth[v as usize];
367                    if d == Real::NEG_INFINITY { d } else { d + band }
368                })
369            });
370            let w = clipped_fraction(&s[..el.len().min(DIM)]);
371            (w > 0.0).then_some(w)
372        },
373        &mut grads,
374    );
375    // A vertex beyond the reach (a ring vertex, whose depth only places the clip) bounds
376    // the patch without adding volume.
377    grads
378        .iter()
379        .enumerate()
380        .filter(|(_, g)| **g != Vector::ZERO)
381        .map(|(v, g)| PatchVertex {
382            vertex: v as u32,
383            grad: *g,
384            depth: if member(v) && depth[v] >= -band {
385                depth[v]
386            } else {
387                0.0
388            },
389        })
390        .collect()
391}
392
393/// The particle-level entries of a side's patch vertices (a skinned vertex spread over its
394/// cell), sorted by particle.
395fn side_entries(side: &VolumeSide, picked: &[usize]) -> Vec<(u32, Vector)> {
396    let mut by_particle: HashMap<u32, Vector> = HashMap::default();
397    for &i in picked {
398        let pv = side.vertices[i];
399        let (anchors, weights) = side.mesh.vertex_anchors(side.body, pv.vertex as usize);
400        for (p, w) in anchors.iter().zip(&weights) {
401            if *p != u32::MAX && *w != 0.0 {
402                *by_particle.entry(*p).or_insert(Vector::ZERO) += pv.grad * *w;
403            }
404        }
405    }
406    let mut entries: Vec<(u32, Vector)> = by_particle.into_iter().collect();
407    entries.sort_by_key(|e| e.0);
408    entries
409}
410
411/// Splits a pair's patches into the volume constraints: one bin (mono-volume), or `split` cells
412/// per tangent axis of a regular grid over the patches, aligned with the pair's mean normal
413/// (multi-volume). A cell missing one side joins the nearest two-sided cell; with none, one bin.
414pub(super) fn volume_bins(
415    own: &VolumeSide,
416    other: Option<&VolumeSide>,
417    split: u32,
418) -> Vec<VolumeBin> {
419    let n_own = own.vertices.len();
420    let n_other = other.map_or(0, |o| o.vertices.len());
421    if n_own + n_other == 0 {
422        return Vec::new();
423    }
424    // Every vertex of both sides: position, gradient (the other side's negated, so the
425    // sum points from the own side into the other), and its side index.
426    let pos = |i: usize| -> Vector {
427        if i < n_own {
428            own.mesh.vertex(own.body, own.vertices[i].vertex as usize)
429        } else {
430            let o = other.unwrap();
431            o.mesh.vertex(o.body, o.vertices[i - n_own].vertex as usize)
432        }
433    };
434    let pv = |i: usize| -> PatchVertex {
435        if i < n_own {
436            own.vertices[i]
437        } else {
438            other.unwrap().vertices[i - n_own]
439        }
440    };
441    let total = n_own + n_other;
442    let mut normal_sum = Vector::ZERO;
443    for i in 0..total {
444        normal_sum += if i < n_own { pv(i).grad } else { -pv(i).grad };
445    }
446    let normal = normal_sum.try_normalize().unwrap_or(Vector::ZERO);
447    // The cells: each vertex's grid coordinates along the tangent axes.
448    let mut cells: Vec<Vec<usize>> = Vec::new();
449    let split = split.max(1) as usize;
450    if split > 1 && normal != Vector::ZERO && total >= 2 {
451        let tangents = normal.orthonormal_basis();
452        let mut center = Vector::ZERO;
453        for i in 0..total {
454            center += pos(i);
455        }
456        center /= total as Real;
457        let coords: Vec<[Real; DIM - 1]> = (0..total)
458            .map(|i| core::array::from_fn(|j| (pos(i) - center).gdot(tangents[j])))
459            .collect();
460        let mut lo = [Real::MAX; DIM - 1];
461        let mut hi = [-Real::MAX; DIM - 1];
462        for c in &coords {
463            for j in 0..DIM - 1 {
464                lo[j] = lo[j].min(c[j]);
465                hi[j] = hi[j].max(c[j]);
466            }
467        }
468        let cell_of = |c: &[Real; DIM - 1]| -> usize {
469            let mut idx = 0;
470            for j in 0..DIM - 1 {
471                let span = hi[j] - lo[j];
472                let k = if span > 0.0 {
473                    (((c[j] - lo[j]) / span) * split as Real) as usize
474                } else {
475                    0
476                };
477                idx = idx * split + k.min(split - 1);
478            }
479            idx
480        };
481        let n_cells = split.pow((DIM - 1) as u32);
482        cells.resize(n_cells, Vec::new());
483        for (i, c) in coords.iter().enumerate() {
484            cells[cell_of(c)].push(i);
485        }
486        let two_sided = |cell: &Vec<usize>| {
487            other.is_none() || (cell.iter().any(|&i| i < n_own) && cell.iter().any(|&i| i >= n_own))
488        };
489        let cell_center = |k: usize| -> [Real; DIM - 1] {
490            let mut c = [0.0; DIM - 1];
491            let mut idx = k;
492            for j in (0..DIM - 1).rev() {
493                c[j] = (idx % split) as Real;
494                idx /= split;
495            }
496            c
497        };
498        let keep: Vec<bool> = cells
499            .iter()
500            .map(|c| !c.is_empty() && two_sided(c))
501            .collect();
502        if keep.iter().any(|&k| k) {
503            for k in 0..n_cells {
504                if keep[k] || cells[k].is_empty() {
505                    continue;
506                }
507                let ck = cell_center(k);
508                let mut best = (Real::MAX, usize::MAX);
509                for t in 0..n_cells {
510                    if !keep[t] {
511                        continue;
512                    }
513                    let ct = cell_center(t);
514                    let d2: Real = (0..DIM - 1).map(|j| (ck[j] - ct[j]).powi(2)).sum();
515                    if d2 < best.0 {
516                        best = (d2, t);
517                    }
518                }
519                let moved = core::mem::take(&mut cells[k]);
520                cells[best.1].extend(moved);
521            }
522            cells.retain(|c| !c.is_empty());
523        } else {
524            cells.clear();
525        }
526    }
527    if cells.is_empty() {
528        cells.push((0..total).collect());
529    }
530    cells
531        .iter()
532        .map(|cell| {
533            let own_idx: Vec<usize> = cell.iter().copied().filter(|&i| i < n_own).collect();
534            let other_idx: Vec<usize> = cell
535                .iter()
536                .filter(|&&i| i >= n_own)
537                .map(|&i| i - n_own)
538                .collect();
539            let mut volume = 0.0;
540            let mut normal_sum = Vector::ZERO;
541            let mut center = Vector::ZERO;
542            let mut weight = 0.0;
543            for &i in cell {
544                let v = pv(i);
545                let a = v.grad.length();
546                volume += v.depth * a;
547                normal_sum += if i < n_own { v.grad } else { -v.grad };
548                center += pos(i) * a;
549                weight += a;
550            }
551            VolumeBin {
552                own: side_entries(own, &own_idx),
553                other: other.map_or_else(Vec::new, |o| side_entries(o, &other_idx)),
554                volume,
555                normal: normal_sum.try_normalize().unwrap_or(Vector::ZERO),
556                center: if weight > 0.0 {
557                    center / weight
558                } else {
559                    Vector::ZERO
560                },
561                enabled: true,
562                impulse: 0.0,
563            }
564        })
565        .collect()
566}