use rapier3d::prelude as rp;
use serde::{Deserialize, Serialize};
use super::{CollisionGroups, CollisionMesh, Physics3D, pose_to_rp, to_rp};
use crate::{Entity, Transform3D, Vec2, Vec3, World};
#[derive(Clone)]
pub struct CollisionMeshData {
pub(super) shape: rp::SharedShape,
pub(super) offset: Vec3,
}
impl std::fmt::Debug for CollisionMeshData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CollisionMeshData")
.field("shape", &self.shape.shape_type())
.field("offset", &self.offset)
.finish()
}
}
impl CollisionMeshData {
#[track_caller]
#[must_use]
pub fn heightfield(heights: &[f32], size: f32) -> Self {
Self::heightfield_named(heights, size, "CollisionMeshData::heightfield")
}
#[track_caller]
pub(super) fn heightfield_named(heights: &[f32], size: f32, name: &str) -> Self {
let n = (heights.len() as f64).sqrt() as usize;
assert!(
n >= 2 && n * n == heights.len(),
"{name}: {} heights isn't a square grid of at least 2 x 2",
heights.len()
);
assert!(
size > 0.0 && size.is_finite(),
"{name}: size must be above 0 (got {size})"
);
let mut data = vec![0.0; n * n];
for z in 0..n {
for x in 0..n {
let h = heights[z * n + x];
data[z + x * n] = if h.is_finite() { h } else { 0.0 };
}
}
let grid = rapier3d::parry::utils::Array2::new(n, n, data);
let mut field = rapier3d::parry::shape::HeightField::with_flags(
grid,
rp::Vector::new(size, 1.0, size),
rapier3d::parry::shape::HeightFieldFlags::FIX_INTERNAL_EDGES,
);
for z in 0..n - 1 {
for x in 0..n - 1 {
let corners = [(x, z), (x + 1, z), (x, z + 1), (x + 1, z + 1)];
if corners.iter().any(|&(x, z)| !heights[z * n + x].is_finite()) {
field.set_cell_status(z, x, rapier3d::parry::shape::HeightFieldCellStatus::CELL_REMOVED);
}
}
}
let half = size * 0.5;
Self {
shape: rp::SharedShape::new(field),
offset: Vec3::new(half, 0.0, half),
}
}
#[must_use]
pub fn convex_hull(points: &[Vec3]) -> Option<Self> {
if points.len() < 4 || !points.iter().all(|p| p.is_finite()) {
return None;
}
let points: Vec<rp::Vector> = points.iter().map(|&p| to_rp(p)).collect();
let shape = rp::SharedShape::convex_hull(&points)?;
let mass = shape.mass_properties(1.0).mass();
if !(mass > 0.0 && mass.is_finite()) {
return None;
}
Some(Self {
shape,
offset: Vec3::ZERO,
})
}
#[must_use]
pub fn trimesh(vertices: &[Vec3], indices: &[[u32; 3]]) -> Option<Self> {
if indices.is_empty()
|| !vertices.iter().all(|p| p.is_finite())
|| indices.iter().flatten().any(|&i| i as usize >= vertices.len())
{
return None;
}
let vertices: Vec<rp::Vector> = vertices.iter().map(|&p| to_rp(p)).collect();
let shape =
rp::SharedShape::trimesh_with_flags(vertices, indices.to_vec(), rp::TriMeshFlags::FIX_INTERNAL_EDGES)
.ok()?;
Some(Self {
shape,
offset: Vec3::ZERO,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerrainCollider {
pub mesh: CollisionMesh,
pub friction: f32,
pub restitution: f32,
pub groups: CollisionGroups,
}
impl TerrainCollider {
#[must_use]
pub fn new(mesh: CollisionMesh) -> Self {
Self {
mesh,
friction: 0.5,
restitution: 0.0,
groups: CollisionGroups::ALL,
}
}
#[must_use]
pub fn friction(self, friction: f32) -> Self {
Self { friction, ..self }
}
#[must_use]
pub fn restitution(self, restitution: f32) -> Self {
Self { restitution, ..self }
}
#[must_use]
pub fn groups(self, groups: CollisionGroups) -> Self {
Self { groups, ..self }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[repr(C)] pub struct ColliderFocus {
pub radius: f32,
pub keep_radius: f32,
}
impl Default for ColliderFocus {
fn default() -> Self {
Self::new(100.0, 150.0)
}
}
impl ColliderFocus {
#[must_use]
pub fn new(radius: f32, keep_radius: f32) -> Self {
Self { radius, keep_radius }
}
}
pub(super) type Rect = [f32; 4];
fn away(r: Rect, p: Vec2) -> f32 {
let dx = (r[0] - p.x).max(p.x - r[2]).max(0.0);
let dz = (r[1] - p.y).max(p.y - r[3]).max(0.0);
(dx * dx + dz * dz).sqrt()
}
fn overlaps(a: Rect, b: Rect) -> bool {
a[0] <= b[2] && b[0] <= a[2] && a[1] <= b[3] && b[1] <= a[3]
}
const HOLD_MARGIN: f32 = 1.0;
#[derive(Default)]
pub(super) struct Scratch {
focus: Vec<(Vec2, f32, f32)>,
holders: Vec<Rect>,
wanted: Vec<(f32, u32)>,
}
impl Physics3D {
pub fn stream_colliders(&mut self, max_new: Option<usize>) {
self.streaming = max_new;
}
#[must_use]
pub fn collider_streaming(&self) -> Option<usize> {
self.streaming
}
#[must_use]
pub fn is_collider_active(&self, e: Entity) -> bool {
self.link(e)
.and_then(|l| self.world.colliders.get(l.collider))
.is_some_and(rp::Collider::is_enabled)
}
pub fn add_mesh(&mut self, data: &CollisionMeshData) -> CollisionMesh {
self.add_shape(data.shape.clone(), data.offset)
}
pub(super) fn rect_of(&self, collider: rp::ColliderHandle, t: &Transform3D) -> Rect {
let Some(c) = self.world.colliders.get(collider) else {
return [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY];
};
let local = c.position_wrt_parent().copied().unwrap_or_default();
let aabb = c.shape().compute_aabb(&(pose_to_rp(t.position, t.rotation) * local));
[aabb.mins.x, aabb.mins.z, aabb.maxs.x, aabb.maxs.z]
}
pub(super) fn set_active(&mut self, i: usize, active: bool) {
let Some(link) = self.links[i].as_mut() else { return };
link.active = active;
if let Some(c) = self.world.colliders.get_mut(link.collider) {
c.set_enabled(active);
}
}
}
pub(super) fn scaled(shape: &rp::SharedShape, scale: Vec3) -> rp::SharedShape {
let scale = to_rp(scale.abs().max(Vec3::splat(1e-6)));
if let Some(field) = shape.as_heightfield() {
rp::SharedShape::new(field.clone().scaled(scale))
} else if let Some(mesh) = shape.as_trimesh() {
rp::SharedShape::new(mesh.clone().scaled(scale))
} else {
shape.clone()
}
}
pub(super) fn stream(physics: &mut Physics3D, world: &mut World) {
physics.moving();
let Some(max_new) = physics.streaming else {
if physics.streamed_out {
physics.streamed_out = false;
for k in 0..physics.statics.len() {
let i = physics.statics[k] as usize;
if physics.links[i].is_some_and(|l| !l.active) {
physics.set_active(i, true);
}
}
}
return;
};
physics.streamed_out = true;
let mut scratch = std::mem::take(&mut physics.stream_scratch);
scratch.focus.clear();
world.query::<(&Transform3D, &ColliderFocus)>().for_each(|(t, f)| {
let at = Vec2::new(t.position.x, t.position.z);
scratch.focus.push((at, f.radius, f.keep_radius.max(f.radius)));
});
scratch.wanted.clear();
let mut holders_found = false;
for k in 0..physics.statics.len() {
let i = physics.statics[k];
let Some(link) = physics.links[i as usize] else {
continue;
};
if link.active {
if scratch.focus.iter().any(|&(at, _, keep)| away(link.rect, at) <= keep) {
continue;
}
if !holders_found {
holders_found = true;
find_holders(physics, &mut scratch.holders);
}
if scratch.holders.iter().any(|&h| overlaps(h, link.rect)) {
continue;
}
physics.set_active(i as usize, false);
} else {
let nearest = scratch
.focus
.iter()
.map(|&(at, radius, _)| (away(link.rect, at), radius))
.filter(|&(d, radius)| d <= radius)
.map(|(d, _)| d)
.reduce(f32::min);
if let Some(d) = nearest {
scratch.wanted.push((d, i));
}
}
}
if scratch.wanted.len() > max_new {
scratch
.wanted
.sort_unstable_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
}
for &(_, i) in scratch.wanted.iter().take(max_new) {
physics.set_active(i as usize, true);
}
physics.stream_scratch = scratch;
}
fn find_holders(physics: &mut Physics3D, holders: &mut Vec<Rect>) {
holders.clear();
let n = physics.moving().len();
for k in 0..n {
let i = physics.moving[k] as usize;
let Some(link) = physics.links[i] else { continue };
let Some(c) = physics.world.colliders.get(link.collider) else {
continue;
};
let aabb = c.compute_aabb();
holders.push([
aabb.mins.x - HOLD_MARGIN,
aabb.mins.z - HOLD_MARGIN,
aabb.maxs.x + HOLD_MARGIN,
aabb.maxs.z + HOLD_MARGIN,
]);
}
}