use std::sync::Mutex;
use rapier3d::prelude as rp;
use serde::{Deserialize, Serialize};
use crate::collision::{BodyKind, CollisionStarted, CollisionStopped};
use crate::{Ctx, Entity, Quat, Transform3D, Vec3, World};
mod character;
mod contacts;
pub use contacts::{ContactModifier3D, ContactPair3D};
mod dominance;
mod files;
mod joints;
pub use joints::{Joint3D, JointAxis3D, JointDrive3D, JointMotion3D};
mod query;
mod ragdoll;
mod streaming;
mod vehicle;
pub use ragdoll::{Ragdoll3D, RagdollBone};
pub(crate) use ragdoll::{blend_back, pose_ragdolls};
pub use streaming::{ColliderFocus, CollisionMeshData, TerrainCollider};
pub use character::Character3D;
pub use query::{Hit3D, QueryFilter3D};
pub use vehicle::{FrictionCurve, Vehicle3D, Wheel3D};
pub const PHYSICS3D_HZ: u32 = 50;
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct RigidBody3D {
pub kind: BodyKind,
pub dominance: i8,
pub gravity_scale: f32,
pub linear_damping: f32,
pub angular_damping: f32,
pub lock_rotation: bool,
pub ccd: bool,
pub mass: Option<f32>,
pub center_of_mass: Option<Vec3>,
}
impl Default for RigidBody3D {
fn default() -> Self {
Self::dynamic()
}
}
impl RigidBody3D {
#[must_use]
pub const fn dominance(self, dominance: i8) -> Self {
Self { dominance, ..self }
}
#[must_use]
pub const fn dynamic() -> Self {
Self {
kind: BodyKind::Dynamic,
dominance: 0,
gravity_scale: 1.0,
linear_damping: 0.0,
angular_damping: 0.0,
lock_rotation: false,
ccd: false,
mass: None,
center_of_mass: None,
}
}
#[must_use]
pub const fn fixed() -> Self {
Self {
kind: BodyKind::Fixed,
..Self::dynamic()
}
}
#[must_use]
pub const fn kinematic() -> Self {
Self {
kind: BodyKind::Kinematic,
..Self::dynamic()
}
}
#[must_use]
pub const fn mass(self, kg: f32) -> Self {
Self { mass: Some(kg), ..self }
}
#[must_use]
pub const fn center_of_mass(self, center: Vec3) -> Self {
Self {
center_of_mass: Some(center),
..self
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub struct CollisionMesh {
index: u32,
generation: u32,
}
impl CollisionMesh {
#[doc(hidden)]
#[must_use]
pub fn to_bits(self) -> u64 {
u64::from(self.generation) << 32 | u64::from(self.index)
}
#[doc(hidden)]
#[must_use]
pub fn from_bits(bits: u64) -> Self {
Self {
index: bits as u32,
generation: (bits >> 32) as u32,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Shape3D {
Sphere {
radius: f32,
},
Cuboid {
size: Vec3,
},
Capsule {
height: f32,
radius: f32,
},
Mesh(CollisionMesh),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(C)]
pub struct CollisionGroups {
pub memberships: u32,
pub filter: u32,
}
impl Default for CollisionGroups {
fn default() -> Self {
Self::ALL
}
}
impl CollisionGroups {
pub const ALL: Self = Self::new(u32::MAX, u32::MAX);
#[must_use]
pub const fn new(memberships: u32, filter: u32) -> Self {
Self { memberships, filter }
}
#[must_use]
pub const fn touches(self, other: Self) -> bool {
self.memberships & other.filter != 0 && other.memberships & self.filter != 0
}
fn to_rp(self) -> rp::InteractionGroups {
rp::InteractionGroups::new(
rp::Group::from_bits_retain(self.memberships),
rp::Group::from_bits_retain(self.filter),
rp::InteractionTestMode::And,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Collider3D {
pub shape: Shape3D,
#[serde(default = "water")]
pub density: f32,
#[serde(default = "half")]
pub friction: f32,
#[serde(default)]
pub restitution: f32,
#[serde(default)]
pub sensor: bool,
#[serde(default)]
pub offset: Vec3,
#[serde(default)]
pub groups: CollisionGroups,
}
fn water() -> f32 {
1000.0
}
fn half() -> f32 {
0.5
}
impl Collider3D {
fn of(shape: Shape3D) -> Self {
Self {
shape,
density: water(),
friction: half(),
restitution: 0.0,
sensor: false,
offset: Vec3::ZERO,
groups: CollisionGroups::ALL,
}
}
#[must_use]
pub fn sphere(radius: f32) -> Self {
Self::of(Shape3D::Sphere { radius })
}
#[must_use]
pub fn cuboid(size: Vec3) -> Self {
Self::of(Shape3D::Cuboid { size })
}
#[must_use]
pub fn capsule(height: f32, radius: f32) -> Self {
Self::of(Shape3D::Capsule { height, radius })
}
#[must_use]
pub fn mesh(mesh: CollisionMesh) -> Self {
Self::of(Shape3D::Mesh(mesh))
}
#[must_use]
pub fn density(self, density: f32) -> Self {
Self { density, ..self }
}
#[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 sensor(self) -> Self {
Self { sensor: true, ..self }
}
#[must_use]
pub fn offset(self, offset: Vec3) -> Self {
Self { offset, ..self }
}
#[must_use]
pub fn groups(self, groups: CollisionGroups) -> Self {
Self { groups, ..self }
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
#[repr(C)] pub struct Velocity3D {
pub linear: Vec3,
pub angular: Vec3,
}
impl Velocity3D {
#[must_use]
pub const fn linear(linear: Vec3) -> Self {
Self {
linear,
angular: Vec3::ZERO,
}
}
}
#[derive(Clone, Copy, PartialEq)]
struct Pose {
position: Vec3,
rotation: Quat,
}
impl Pose {
fn of(t: &Transform3D) -> Self {
Self {
position: t.position,
rotation: t.rotation,
}
}
}
#[derive(Clone, Copy)]
struct Link {
entity: Entity,
body: rp::RigidBodyHandle,
collider: rp::ColliderHandle,
active: bool,
rect: streaming::Rect,
kind: LinkKind,
velocity: Velocity3D,
written: Transform3D,
prev: Pose,
curr: Pose,
seen: u64,
}
#[derive(Clone, Copy, PartialEq)]
enum LinkKind {
Body(RigidBody3D, Collider3D),
Character(character::Capsule),
Terrain(TerrainCollider, Vec3),
}
impl LinkKind {
fn moves(self) -> bool {
match self {
LinkKind::Body(rigid, _) => rigid.kind != BodyKind::Fixed,
LinkKind::Character(_) => true,
LinkKind::Terrain(..) => false,
}
}
}
struct MeshSlot {
shape: Option<rp::SharedShape>,
offset: Vec3,
generation: u32,
}
#[derive(Default)]
struct Collector(Mutex<Vec<rp::CollisionEvent>>);
impl rp::EventHandler for Collector {
fn handle_collision_event(
&self,
_: &rp::RigidBodySet,
_: &rp::ColliderSet,
event: rp::CollisionEvent,
_: Option<&rp::ContactPair>,
) {
self.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(event);
}
fn handle_contact_force_event(
&self,
_: f32,
_: &rp::RigidBodySet,
_: &rp::ColliderSet,
_: &rp::ContactPair,
_: f32,
) {
}
fn handle_soft_body_tear_event(&self, _: &rp::SoftBodySet, _: &rp::SoftBodyTearEvent) {}
}
pub struct Physics3D {
world: rp::PhysicsWorld,
joints: Vec<joints::Link>,
links: Vec<Option<Link>>,
moving: Vec<u32>,
moving_dirty: bool,
statics: Vec<u32>,
streaming: Option<usize>,
streamed_out: bool,
stream_scratch: streaming::Scratch,
dominance: bool,
gravity: Vec3,
step: u64,
collector: Collector,
events: Vec<rp::CollisionEvent>,
meshes: Vec<MeshSlot>,
free_meshes: Vec<u32>,
character: character::Scratch,
pub(crate) ragdolls: Vec<Entity>,
contact_table: Vec<Option<(Entity, ContactModifier3D)>>,
contact_hooked: Vec<(Entity, ContactModifier3D)>,
contact_spare: Vec<(Entity, ContactModifier3D)>,
contact_hook: Option<contacts::ContactHook>,
}
impl Default for Physics3D {
fn default() -> Self {
Self::new(Vec3::ZERO)
}
}
pub(crate) fn to_rp(v: Vec3) -> rp::Vector {
rp::Vector::new(v.x, v.y, v.z)
}
pub(crate) fn from_rp(v: rp::Vector) -> Vec3 {
Vec3::new(v.x, v.y, v.z)
}
fn rot_to_rp(q: Quat) -> rp::Rotation {
let q = q.normalize();
if q.is_finite() {
rp::Rotation::from_xyzw(q.x, q.y, q.z, q.w)
} else {
rp::Rotation::IDENTITY
}
}
fn rot_from_rp(q: rp::Rotation) -> Quat {
Quat::from_xyzw(q.x, q.y, q.z, q.w)
}
pub(crate) fn pose_to_rp(position: Vec3, rotation: Quat) -> rp::Pose {
rp::Pose::from_parts(to_rp(position), rot_to_rp(rotation))
}
impl Physics3D {
#[must_use]
pub fn new(gravity: Vec3) -> Self {
Self {
world: rp::PhysicsWorld::new(),
joints: Vec::new(),
links: Vec::new(),
moving: Vec::new(),
moving_dirty: false,
statics: Vec::new(),
streaming: None,
streamed_out: false,
stream_scratch: streaming::Scratch::default(),
dominance: false,
gravity,
step: 0,
collector: Collector::default(),
events: Vec::new(),
meshes: Vec::new(),
free_meshes: Vec::new(),
character: character::Scratch::default(),
ragdolls: Vec::new(),
contact_table: Vec::new(),
contact_hooked: Vec::new(),
contact_spare: Vec::new(),
contact_hook: None,
}
}
pub fn set_contact_hook(&mut self, hook: impl Fn(&mut ContactPair3D) + Send + Sync + 'static) {
self.contact_hook = Some(Box::new(hook));
}
pub fn clear_contact_hook(&mut self) {
self.contact_hook = None;
}
pub fn gravity(&self) -> Vec3 {
self.gravity
}
pub fn set_gravity(&mut self, gravity: Vec3) {
self.gravity = gravity;
}
pub fn body_count(&self) -> usize {
self.world.bodies.len()
}
fn entity_of(&self, collider: rp::ColliderHandle) -> Option<Entity> {
self.world
.colliders
.get(collider)
.map(|c| Entity::from_bits(c.user_data as u64))
}
fn link(&self, e: Entity) -> Option<&Link> {
self.links.get(e.index() as usize)?.as_ref().filter(|l| l.entity == e)
}
pub fn apply_impulse(&mut self, e: Entity, impulse: Vec3) -> bool {
let Some(link) = self.link(e) else { return false };
let handle = link.body;
match self.world.bodies.get_mut(handle) {
Some(body) => {
body.apply_impulse(to_rp(impulse), true);
true
}
None => false,
}
}
pub fn apply_impulse_at(&mut self, e: Entity, impulse: Vec3, point: Vec3) -> bool {
let Some(link) = self.link(e) else { return false };
let handle = link.body;
match self.world.bodies.get_mut(handle) {
Some(body) => {
body.apply_impulse_at_point(to_rp(impulse), to_rp(point), true);
true
}
None => false,
}
}
pub fn mass(&self, e: Entity) -> Option<f32> {
let link = self.link(e)?;
self.world.bodies.get(link.body).map(|b| b.mass())
}
pub fn center_of_mass(&self, e: Entity) -> Option<Vec3> {
let link = self.link(e)?;
self.world.bodies.get(link.body).map(|b| from_rp(b.center_of_mass()))
}
pub fn position(&self, e: Entity) -> Option<Vec3> {
self.link(e).map(|l| l.curr.position)
}
pub fn convex_hull(&mut self, points: &[Vec3]) -> Option<CollisionMesh> {
Some(self.add_mesh(&CollisionMeshData::convex_hull(points)?))
}
pub fn trimesh(&mut self, vertices: &[Vec3], indices: &[[u32; 3]]) -> Option<CollisionMesh> {
Some(self.add_mesh(&CollisionMeshData::trimesh(vertices, indices)?))
}
#[track_caller]
pub fn heightfield(&mut self, heights: &[f32], size: f32) -> CollisionMesh {
self.add_mesh(&CollisionMeshData::heightfield_named(
heights,
size,
"Physics3D::heightfield",
))
}
pub fn free_mesh(&mut self, mesh: CollisionMesh) {
if let Some(slot) = self.meshes.get_mut(mesh.index as usize)
&& slot.generation == mesh.generation
&& slot.shape.take().is_some()
{
slot.generation = slot.generation.wrapping_add(1);
self.free_meshes.push(mesh.index);
}
}
pub fn is_mesh_live(&self, mesh: CollisionMesh) -> bool {
self.mesh(mesh).is_some()
}
fn mesh(&self, mesh: CollisionMesh) -> Option<&MeshSlot> {
self.meshes
.get(mesh.index as usize)
.filter(|s| s.generation == mesh.generation && s.shape.is_some())
}
fn add_shape(&mut self, shape: rp::SharedShape, offset: Vec3) -> CollisionMesh {
if let Some(index) = self.free_meshes.pop() {
let slot = &mut self.meshes[index as usize];
slot.shape = Some(shape);
slot.offset = offset;
return CollisionMesh {
index,
generation: slot.generation,
};
}
self.meshes.push(MeshSlot {
shape: Some(shape),
offset,
generation: 0,
});
CollisionMesh {
index: (self.meshes.len() - 1) as u32,
generation: 0,
}
}
fn collider_of(&self, c: &Collider3D, e: Entity) -> rp::Collider {
self.scaled_collider_of(c, e, Vec3::ONE)
}
fn terrain_collider_of(&self, t: &TerrainCollider, scale: Vec3, e: Entity) -> rp::Collider {
let c = Collider3D {
friction: t.friction,
restitution: t.restitution,
groups: t.groups,
..Collider3D::mesh(t.mesh)
};
self.scaled_collider_of(&c, e, scale)
}
fn scaled_collider_of(&self, c: &Collider3D, e: Entity, scale: Vec3) -> rp::Collider {
let tiny = f32::EPSILON;
let (shape, offset) = match c.shape {
Shape3D::Sphere { radius } => (rp::SharedShape::ball(radius.max(tiny)), Vec3::ZERO),
Shape3D::Cuboid { size } => {
let h = (size * 0.5).max(Vec3::splat(tiny));
(rp::SharedShape::cuboid(h.x, h.y, h.z), Vec3::ZERO)
}
Shape3D::Capsule { height, radius } => (
rp::SharedShape::capsule_y((height * 0.5).max(0.0), radius.max(tiny)),
Vec3::ZERO,
),
Shape3D::Mesh(m) => match self.mesh(m) {
Some(slot) if scale == Vec3::ONE => (slot.shape.clone().expect("live slot"), slot.offset),
Some(slot) => (
streaming::scaled(slot.shape.as_ref().expect("live slot"), scale),
slot.offset * scale.abs(),
),
None => (rp::SharedShape::ball(tiny), Vec3::ZERO),
},
};
let mut types = rp::ActiveCollisionTypes::default();
if c.sensor {
types |= rp::ActiveCollisionTypes::KINEMATIC_FIXED | rp::ActiveCollisionTypes::KINEMATIC_KINEMATIC;
}
let groups = match c.shape {
Shape3D::Mesh(m) if self.mesh(m).is_none() => CollisionGroups::new(0, 0),
_ => c.groups,
};
rp::ColliderBuilder::new(shape)
.density(c.density)
.friction(c.friction)
.restitution(c.restitution)
.sensor(c.sensor)
.translation(to_rp(c.offset + offset))
.collision_groups(groups.to_rp())
.active_collision_types(types)
.active_events(rp::ActiveEvents::COLLISION_EVENTS)
.user_data(u128::from(e.to_bits()))
.build()
}
fn moving(&mut self) -> &[u32] {
if self.moving_dirty {
self.moving_dirty = false;
self.moving.clear();
self.statics.clear();
let links = &self.links;
self.moving
.extend((0..links.len() as u32).filter(|&i| links[i as usize].is_some_and(|l| l.kind.moves())));
self.statics
.extend((0..links.len() as u32).filter(|&i| links[i as usize].is_some_and(|l| !l.kind.moves())));
self.dominance = links.iter().flatten().any(|l| {
matches!(l.kind, LinkKind::Body(rigid, _) if rigid.kind == BodyKind::Dynamic && rigid.dominance != 0)
});
}
&self.moving
}
fn uses_dominance(&mut self) -> bool {
self.moving();
self.dominance
}
fn insert_link(&mut self, e: Entity, t: &Transform3D, kind: LinkKind, velocity: Velocity3D) {
self.moving_dirty = true;
let active = kind.moves() || self.streaming.is_none();
let (body, collider) = match kind {
LinkKind::Body(rigid, shape) => {
let body = self.world.insert_body(body_of(t, rigid, velocity));
let mut collider = self.collider_of(&shape, e);
set_mass(&mut collider, rigid);
collider.set_enabled(active);
(body, self.world.insert_collider(collider, Some(body)))
}
LinkKind::Character(capsule) => {
let body = self.world.insert_body(
rp::RigidBodyBuilder::kinematic_position_based()
.pose(pose_to_rp(t.position, Quat::IDENTITY))
.build(),
);
(body, self.world.insert_collider(capsule.collider(e), Some(body)))
}
LinkKind::Terrain(terrain, scale) => {
let body = self
.world
.insert_body(body_of(t, RigidBody3D::fixed(), Velocity3D::default()));
let mut collider = self.terrain_collider_of(&terrain, scale, e);
collider.set_enabled(active);
(body, self.world.insert_collider(collider, Some(body)))
}
};
if !active {
self.streamed_out = true;
}
let rect = if kind.moves() {
[0.0; 4]
} else {
self.rect_of(collider, t)
};
let i = e.index() as usize;
if self.links.len() <= i {
self.links.resize(i + 1, None);
}
let pose = Pose::of(t);
self.links[i] = Some(Link {
entity: e,
body,
collider,
active,
rect,
kind,
velocity,
written: *t,
prev: pose,
curr: pose,
seen: self.step,
});
}
fn remove(&mut self, i: usize) {
self.moving_dirty = true;
if let Some(link) = self.links[i].take() {
self.world.remove_body(link.body);
}
}
}
fn set_mass(collider: &mut rp::Collider, rigid: RigidBody3D) {
let mass = rigid.mass.filter(|m| *m > 0.0 && m.is_finite());
let center = rigid.center_of_mass.filter(|c| c.is_finite());
if mass.is_none() && center.is_none() {
return;
}
let mut props = collider.mass_properties();
if let Some(mass) = mass {
if props.mass() > 0.0 {
props.set_mass(mass, true);
} else {
props = rp::MassProperties::new(props.local_com, mass, rp::Vector::splat(mass * 0.1));
}
}
if let Some(center) = center {
props.local_com = to_rp(center) - collider.position().translation;
}
collider.set_mass_properties(props);
}
fn body_of(t: &Transform3D, rigid: RigidBody3D, velocity: Velocity3D) -> rp::RigidBody {
let builder = match rigid.kind {
BodyKind::Fixed => rp::RigidBodyBuilder::fixed(),
BodyKind::Kinematic => rp::RigidBodyBuilder::kinematic_velocity_based(),
_ => rp::RigidBodyBuilder::dynamic(),
};
let mut builder = builder
.pose(pose_to_rp(t.position, t.rotation))
.linvel(to_rp(velocity.linear))
.angvel(to_rp(velocity.angular))
.dominance_group(rigid.dominance)
.gravity_scale(rigid.gravity_scale)
.linear_damping(rigid.linear_damping)
.angular_damping(rigid.angular_damping)
.ccd_enabled(rigid.ccd);
if rigid.lock_rotation {
builder = builder.lock_rotations();
}
builder.build()
}
pub(crate) fn start(ctx: &mut Ctx, gravity: Vec3) {
use crate::WorldReflect;
ctx.physics3d = Physics3D::new(gravity);
ctx.world.reflect::<RigidBody3D>();
crate::reflect::reflect_collider(&mut ctx.world);
ctx.world.reflect::<Velocity3D>();
ctx.world.reflect::<Character3D>();
ctx.world.reflect::<Vehicle3D>();
ctx.world.reflect::<ColliderFocus>();
}
pub fn step_physics3d(ctx: &mut Ctx) {
let dt = ctx.dt();
let physics = &mut ctx.physics3d;
physics.step += 1;
sync_into(physics, &mut ctx.world);
streaming::stream(physics, &mut ctx.world);
joints::sync(physics, &mut ctx.world);
contacts::sync(physics, &mut ctx.world);
physics.world.gravity = to_rp(physics.gravity);
physics.world.integration_parameters.dt = dt;
character::move_characters(physics, &mut ctx.world, dt);
vehicle::step_vehicles(physics, &mut ctx.world, dt);
let hooks = contacts::Hooks {
table: &physics.contact_table,
custom: physics.contact_hook.as_deref(),
};
physics.world.step_with_events(&hooks, &physics.collector);
if physics.uses_dominance() {
dominance::transfer_velocity(physics, dt);
}
write_back(physics, &mut ctx.world);
pose_ragdolls(&mut ctx.world, &mut physics.ragdolls);
std::mem::swap(
&mut physics.events,
&mut *physics
.collector
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
);
for i in 0..physics.events.len() {
let (a, b, flags, started) = match physics.events[i] {
rp::CollisionEvent::Started(a, b, f) => (a, b, f, true),
rp::CollisionEvent::Stopped(a, b, f) => (a, b, f, false),
};
let (Some(a), Some(b)) = (physics.entity_of(a), physics.entity_of(b)) else {
continue;
};
let sensor = flags.contains(rp::CollisionEventFlags::SENSOR);
if started {
ctx.events.send(CollisionStarted { a, b, sensor });
} else {
ctx.events.send(CollisionStopped { a, b, sensor });
}
}
physics.events.clear();
}
pub fn interpolate_physics3d(ctx: &mut Ctx) {
if ctx.headless {
return;
}
let alpha = ctx.fixed_alpha().clamp(0.0, 1.0);
let physics = &mut ctx.physics3d;
if physics.links.is_empty() {
return;
}
let n = physics.moving().len();
for k in 0..n {
let i = physics.moving[k] as usize;
let Some(link) = physics.links[i].as_mut() else {
continue;
};
let Some(t) = ctx.world.get_mut::<Transform3D>(link.entity) else {
continue;
};
if !written_matches(link, t) {
continue;
}
t.position = link.prev.position.lerp(link.curr.position, alpha);
if let LinkKind::Body(..) = link.kind {
t.rotation = link.prev.rotation.slerp(link.curr.rotation, alpha);
}
link.written = *t;
}
}
fn written_matches(link: &Link, t: &Transform3D) -> bool {
match link.kind {
LinkKind::Character(_) => link.written.position == t.position,
LinkKind::Body(..) | LinkKind::Terrain(..) => link.written == *t,
}
}
fn sync_into(physics: &mut Physics3D, world: &mut World) {
let step = physics.step;
world
.query::<(Entity, &Transform3D, &RigidBody3D, &Collider3D, Option<&Velocity3D>)>()
.without::<Character3D>()
.for_each(|(e, t, rigid, shape, velocity)| {
let kind = LinkKind::Body(*rigid, *shape);
sync_one(physics, e, t, kind, velocity.copied(), step);
});
world
.query::<(Entity, &Transform3D, &Character3D)>()
.for_each(|(e, t, c)| {
let kind = LinkKind::Character(character::Capsule::of(c));
sync_one(physics, e, t, kind, None, step);
});
world
.query::<(Entity, &Transform3D, &TerrainCollider)>()
.without::<RigidBody3D>()
.without::<Character3D>()
.for_each(|(e, t, terrain)| {
let kind = LinkKind::Terrain(*terrain, t.scale);
sync_one(physics, e, t, kind, None, step);
});
for i in 0..physics.links.len() {
if physics.links[i].is_some_and(|l| l.seen != step) {
physics.remove(i);
}
}
}
fn sync_one(
physics: &mut Physics3D,
e: Entity,
t: &Transform3D,
kind: LinkKind,
velocity: Option<Velocity3D>,
step: u64,
) {
let i = e.index() as usize;
let Some(link) = physics
.links
.get_mut(i)
.and_then(Option::as_mut)
.filter(|l| l.entity == e)
else {
if let Some(Some(old)) = physics.links.get(i).copied() {
physics.world.remove_body(old.body);
physics.links[i] = None;
physics.moving_dirty = true;
}
physics.insert_link(e, t, kind, velocity.unwrap_or_default());
return;
};
link.seen = step;
if link.kind != kind {
let (current, was_active) = (link.velocity, link.active);
physics.world.remove_body(link.body);
physics.insert_link(e, t, kind, velocity.unwrap_or(current));
if was_active && physics.links[i].is_some_and(|l| !l.active) {
physics.set_active(i, true);
}
return;
}
let mut moved_static = None;
if !written_matches(link, t)
&& let Some(body) = physics.world.bodies.get_mut(link.body)
{
let rotation = match kind {
LinkKind::Body(..) | LinkKind::Terrain(..) => t.rotation,
LinkKind::Character(_) => Quat::IDENTITY,
};
body.set_position(pose_to_rp(t.position, rotation), true);
link.written = *t;
link.curr = Pose::of(t);
link.prev = link.curr;
if !kind.moves() {
moved_static = Some(link.collider);
}
}
if let Some(v) = velocity
&& link.velocity != v
&& let Some(body) = physics.world.bodies.get_mut(link.body)
{
body.set_linvel(to_rp(v.linear), true);
body.set_angvel(to_rp(v.angular), true);
link.velocity = v;
}
if let Some(collider) = moved_static {
let rect = physics.rect_of(collider, t);
if let Some(link) = physics.links[i].as_mut() {
link.rect = rect;
}
}
}
fn write_back(physics: &mut Physics3D, world: &mut World) {
let n = physics.moving().len();
for k in 0..n {
let i = physics.moving[k] as usize;
let Some(link) = physics.links[i].as_mut() else {
continue;
};
let e = link.entity;
let Some(body) = physics.world.bodies.get(link.body) else {
continue;
};
if body.is_sleeping() {
link.prev = link.curr;
if let LinkKind::Body(..) = link.kind
&& link.velocity != Velocity3D::default()
&& world.get::<Transform3D>(e).is_some()
{
link.velocity = Velocity3D::default();
if let Some(velocity) = world.get_mut::<Velocity3D>(e) {
*velocity = link.velocity;
}
}
continue;
}
if world.get::<Transform3D>(e).is_none() {
continue;
}
link.prev = link.curr;
link.curr.position = from_rp(body.translation());
let moved_rotation = matches!(link.kind, LinkKind::Body(..)).then(|| rot_from_rp(*body.rotation()));
if let Some(rotation) = moved_rotation {
link.curr.rotation = rotation;
let v = Velocity3D {
linear: from_rp(body.linvel()),
angular: from_rp(body.angvel()),
};
link.velocity = v;
if let Some(velocity) = world.get_mut::<Velocity3D>(e) {
*velocity = v;
}
}
let Some(t) = world.get_mut::<Transform3D>(e) else {
continue;
};
t.position = link.curr.position;
if let Some(rotation) = moved_rotation {
t.rotation = rotation;
}
link.written = *t;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::time::FrameTiming;
use crate::{Vec2, vec3};
fn tick(ctx: &mut Ctx) {
ctx.set_in_sim(true);
step_physics3d(ctx);
ctx.set_in_sim(false);
}
fn frame(ctx: &mut Ctx, alpha: f32) {
ctx.begin_frame(FrameTiming {
dt: 0.01,
fixed_steps: 0,
alpha,
});
interpolate_physics3d(ctx);
}
#[test]
fn frames_between_steps_draw_a_blend_and_steps_continue_from_the_physics_pose() {
let mut ctx = Ctx::new(0.02, Vec2::ZERO);
start(&mut ctx, Vec3::ZERO);
let ball = ctx.world.spawn((
Transform3D::default(),
RigidBody3D::dynamic(),
Collider3D::sphere(0.5),
Velocity3D::linear(vec3(10.0, 0.0, 0.0)),
));
tick(&mut ctx);
tick(&mut ctx);
let x = |ctx: &mut Ctx| ctx.world.get::<Transform3D>(ball).unwrap().position.x;
assert!((x(&mut ctx) - 0.4).abs() < 1e-4, "two steps of 0.2 m");
frame(&mut ctx, 0.5);
assert!(
(x(&mut ctx) - 0.3).abs() < 1e-4,
"halfway between the last two: {}",
x(&mut ctx)
);
frame(&mut ctx, 0.75);
assert!((x(&mut ctx) - 0.35).abs() < 1e-4);
tick(&mut ctx);
assert!((x(&mut ctx) - 0.6).abs() < 1e-4, "{}", x(&mut ctx));
ctx.world.get_mut::<Transform3D>(ball).unwrap().position = vec3(-5.0, 0.0, 0.0);
frame(&mut ctx, 0.5);
assert_eq!(x(&mut ctx), -5.0, "left alone by the blend");
tick(&mut ctx);
assert!((x(&mut ctx) + 4.8).abs() < 1e-4, "{}", x(&mut ctx));
}
#[test]
fn headless_runs_draw_the_stepped_pose() {
let mut ctx = Ctx::new(0.02, Vec2::ZERO);
ctx.headless = true;
start(&mut ctx, Vec3::ZERO);
let ball = ctx.world.spawn((
Transform3D::default(),
RigidBody3D::dynamic(),
Collider3D::sphere(0.5),
Velocity3D::linear(vec3(10.0, 0.0, 0.0)),
));
tick(&mut ctx);
frame(&mut ctx, 0.5);
assert!((ctx.world.get::<Transform3D>(ball).unwrap().position.x - 0.2).abs() < 1e-4);
}
}