unlit3dx/physics.rs
1//! The physics threaded world: a fixed-step rapier simulation on its own thread.
2//!
3//! Gameplay sends jobs that build bodies; the thread steps the simulation at
4//! a fixed rate. A step drains rapier's contact events into
5//! [`PhysicsContacts`], the world's own component, and publishes nothing by
6//! itself: a caller that wants the body transforms or the contacts subscribes
7//! with a collector of its own. Rapier's math types are glam aliases, so
8//! whatever a collector copies crosses threads with no conversion.
9//!
10//! Contacts are events, not state. A step's contacts sit in
11//! [`PhysicsContacts`] until the next step overwrites them, so a caller that
12//! must see every one of them — bullet damage, say — has its collector queue
13//! them as they appear. A snapshot mailbox keeps only the newest, which is
14//! right for transforms and wrong for contacts.
15
16use std::sync::mpsc::{Receiver, channel};
17use std::time::Duration;
18
19use glam::{Quat, Vec3};
20use rapier3d::prelude::*;
21
22pub use rapier3d::prelude::{PhysicsWorld, RigidBodyHandle};
23use unlit_ecs::{Bundle, DynamicBundle, Entity, World};
24
25use crate::handle::WorldHandle;
26use crate::thread::{FixedClock, fixed_steps};
27use crate::world::WorldRunner;
28use unlit_ecs::bundle;
29
30/// The simulation's default step rate, in hertz.
31pub const PHYSICS_HZ: f64 = 60.0;
32
33/// How the physics world is set up before its first step.
34#[derive(Clone, Debug)]
35pub struct PhysicsConfig {
36 /// The step rate, in hertz.
37 pub hz: f64,
38 /// The gravity every dynamic body feels.
39 pub gravity: Vec3,
40 /// The command channel's capacity, in queued jobs.
41 pub command_capacity: usize,
42}
43
44impl Default for PhysicsConfig {
45 fn default() -> Self {
46 Self {
47 hz: PHYSICS_HZ,
48 gravity: Vec3::Y * -9.81,
49 command_capacity: 64,
50 }
51 }
52}
53
54/// The contacts rapier reported on the last step.
55///
56/// The component lives in the physics world and is rewritten by every step:
57/// it holds the contacts that began during that step, with the speed the pair
58/// was closing at, and those that ended. It is a reusable buffer, so a
59/// collector reads the contacts where they are instead of asking the world to
60/// build a copy it may not want.
61///
62/// A contact lives in exactly one step. A caller that must react to every one
63/// of them has its collector queue them as they appear; a caller that only
64/// wants the newest state can ignore them.
65#[derive(Clone, Debug, Default)]
66pub struct PhysicsContacts {
67 /// Contacts that began this step, with the closing speed of the pair.
68 pub started: Vec<(RigidBodyHandle, RigidBodyHandle, f32)>,
69 /// Contacts that ended this step.
70 pub stopped: Vec<(RigidBodyHandle, RigidBodyHandle)>,
71}
72
73/// The event plumbing a physics world carries between steps.
74///
75/// Rapier reports contacts through channels: the collector it is stepped
76/// with owns the sending ends, and this component keeps the collector and
77/// the receiving end of the collision channel alive across steps, so each
78/// step reuses them instead of allocating fresh channels.
79pub(crate) struct PhysicsEvents {
80 receiver: Receiver<CollisionEvent>,
81 collector: ChannelEventCollector,
82 /// The moving bodies' velocities, sampled before the step so a contact
83 /// can be reported with the speed the pair was closing at.
84 velocities: Vec<(RigidBodyHandle, Vec3)>,
85}
86
87impl PhysicsEvents {
88 fn new() -> Self {
89 let (collision_sender, receiver) = channel();
90 // The unused event kinds just vanish: a dropped receiver discards them.
91 let (force_sender, force_receiver) = channel::<ContactForceEvent>();
92 drop(force_receiver);
93 let (tear_sender, tear_receiver) = channel::<SoftBodyTearEvent>();
94 drop(tear_receiver);
95 Self {
96 receiver,
97 collector: ChannelEventCollector::new(collision_sender, force_sender, tear_sender),
98 velocities: Vec::new(),
99 }
100 }
101
102 /// Record the moving bodies' velocities before the step runs.
103 fn sample_velocities(&mut self, physics: &PhysicsWorld) {
104 self.velocities.clear();
105 self.velocities.extend(
106 physics
107 .bodies
108 .iter()
109 .filter(|(_, body)| body.is_dynamic() || body.is_kinematic())
110 .map(|(handle, body)| (handle, body.linvel())),
111 );
112 }
113
114 /// The velocity a body had before the step, or zero if it was still.
115 fn velocity(&self, body: RigidBodyHandle) -> Vec3 {
116 self.velocities
117 .iter()
118 .find(|(known, _)| *known == body)
119 .map_or(Vec3::ZERO, |(_, velocity)| *velocity)
120 }
121}
122
123/// A physics world, its event plumbing and the contacts its last step found.
124///
125/// The three are spawned together and read together: the collector is what
126/// lets a step report contacts at all, and the buffer is where those contacts
127/// land, so a world carrying only some of them either reports nothing or has
128/// nowhere to put what it found.
129pub struct Physics {
130 /// The rapier world the bodies live in.
131 ///
132 /// The one field a caller reaches into, because the body API takes it. The
133 /// other two are the plumbing [`step_once`] drives, and a caller reads the
134 /// contacts back as a component rather than through the bundle it spawned.
135 pub world: PhysicsWorld,
136 events: PhysicsEvents,
137 contacts: PhysicsContacts,
138}
139
140impl Physics {
141 /// A physics world under the given gravity, with empty event plumbing.
142 #[must_use]
143 pub fn new(gravity: Vec3) -> Self {
144 let mut world = PhysicsWorld::new();
145 world.gravity = gravity;
146 Self {
147 world,
148 events: PhysicsEvents::new(),
149 contacts: PhysicsContacts::default(),
150 }
151 }
152}
153
154impl Bundle for Physics {
155 fn into_bundle(self) -> DynamicBundle {
156 bundle![self.world, self.events, self.contacts]
157 }
158}
159
160/// The event plumbing a physics world carries, if it has any.
161fn physics_events(world: &World) -> Option<unlit_ecs::QueryOne<'_, &mut PhysicsEvents>> {
162 let entity = world
163 .root_scope()
164 .query::<(Entity, &PhysicsEvents)>()
165 .iter()
166 .next()
167 .map(|(entity, _events)| entity)?;
168 world
169 .root_scope()
170 .query_one::<&mut PhysicsEvents>(entity)
171 .ok()
172}
173
174/// The contact buffer a physics world carries, if it has one.
175fn physics_contacts(world: &World) -> Option<unlit_ecs::QueryOne<'_, &mut PhysicsContacts>> {
176 let entity = world
177 .root_scope()
178 .query::<(Entity, &PhysicsContacts)>()
179 .iter()
180 .next()
181 .map(|(entity, _contacts)| entity)?;
182 world
183 .root_scope()
184 .query_one::<&mut PhysicsContacts>(entity)
185 .ok()
186}
187
188/// The collider shape a dynamic body carries.
189#[derive(Clone, Copy, Debug)]
190pub enum BodyShape {
191 /// A sphere of the given radius, in metres.
192 Ball(f32),
193 /// An axis-aligned box of the given half extents, in metres.
194 Cuboid(Vec3),
195 /// A cylinder along the y axis.
196 Cylinder {
197 /// Half the height, in metres.
198 half_height: f32,
199 /// The radius, in metres.
200 radius: f32,
201 },
202 /// A cone pointing up the y axis.
203 Cone {
204 /// Half the height, in metres.
205 half_height: f32,
206 /// The base radius, in metres.
207 radius: f32,
208 },
209}
210
211/// A falling, bouncing body: the simulation the example drives.
212#[derive(Clone, Copy, Debug)]
213pub struct BodyDesc {
214 /// Where the body starts, in metres.
215 pub position: Vec3,
216 /// The velocity the body starts with, in metres per second.
217 pub velocity: Vec3,
218 /// How bouncy the body is, from zero to one.
219 pub restitution: f32,
220 /// The collider shape the body carries.
221 pub shape: BodyShape,
222}
223
224/// Spawn a dynamic body with a collision-event collider, returning it.
225///
226/// # Panics
227///
228/// Panics when the world carries no [`PhysicsWorld`]. The physics world is
229/// created before any job runs, so jobs sent through
230/// [`spawn_physics_world_with`] never see this.
231#[must_use]
232pub fn spawn_body(world: &mut World, desc: BodyDesc) -> RigidBodyHandle {
233 let mut physics = physics_world(world);
234 let body = physics.get().insert_body(
235 RigidBodyBuilder::dynamic()
236 .translation(desc.position)
237 .linvel(desc.velocity)
238 .build(),
239 );
240 let collider = match desc.shape {
241 BodyShape::Ball(radius) => ColliderBuilder::ball(radius),
242 BodyShape::Cuboid(half) => ColliderBuilder::cuboid(half.x, half.y, half.z),
243 BodyShape::Cylinder {
244 half_height,
245 radius,
246 } => ColliderBuilder::cylinder(half_height, radius),
247 BodyShape::Cone {
248 half_height,
249 radius,
250 } => ColliderBuilder::cone(half_height, radius),
251 };
252 physics.get().insert_collider(
253 collider
254 .restitution(desc.restitution)
255 .active_events(ActiveEvents::COLLISION_EVENTS),
256 Some(body),
257 );
258 body
259}
260
261/// Remove a body and its colliders from the physics world.
262///
263/// Returns whether the handle named a body. A caller that recycles a pool
264/// of bodies — a shape that has rested long enough to be replaced — calls
265/// this to give the simulation its memory back; the ground a scene builds
266/// once is left alone.
267///
268/// # Panics
269///
270/// Panics when the world carries no [`PhysicsWorld`].
271#[must_use]
272pub fn despawn_body(world: &World, body: RigidBodyHandle) -> bool {
273 physics_world(world).get().remove_body(body).is_some()
274}
275
276/// Spawn a fixed cuboid: a floor, a wall, whatever gameplay needs.
277///
278/// # Panics
279///
280/// Panics when the world carries no [`PhysicsWorld`].
281pub fn spawn_fixed_cuboid(world: &mut World, position: Vec3, half_extents: Vec3, friction: f32) {
282 let mut physics = physics_world(world);
283 let body = physics
284 .get()
285 .insert_body(RigidBodyBuilder::fixed().translation(position).build());
286 physics.get().insert_collider(
287 ColliderBuilder::cuboid(half_extents.x, half_extents.y, half_extents.z).friction(friction),
288 Some(body),
289 );
290}
291
292/// The physics world as a mutable guard, for jobs that build bodies.
293fn physics_world(world: &World) -> unlit_ecs::QueryOne<'_, &mut PhysicsWorld> {
294 let entity = world
295 .root_scope()
296 .query::<(Entity, &PhysicsWorld)>()
297 .iter()
298 .next()
299 .map(|(entity, _physics)| entity)
300 .expect("the physics world exists");
301 world
302 .root_scope()
303 .query_one::<&mut PhysicsWorld>(entity)
304 .expect("the physics world exists")
305}
306
307/// Spawn the physics world driven by `R`: on its own thread
308/// ([`Threaded`](crate::world::Threaded)), or stepped by the host
309/// ([`Stepped`](crate::world::Stepped)).
310///
311/// The world owns the rapier world as a [`PhysicsWorld`] component, and
312/// gameplay never touches it: jobs build bodies, and a subscriber reads the
313/// state back. The world is created before the first job runs, so a job that
314/// builds a body — the static geometry, usually — finds it ready.
315///
316/// Nothing is published until someone subscribes: the world runs a
317/// subscriber's collector once per step, after the step has drained its
318/// contacts. Subscribing with the handle's [`JobSender`](crate::job::JobSender)
319/// is how a host asks for transforms, contacts, or both.
320///
321/// Both runners share one tick body, so a stepped world simulates exactly
322/// what a threaded one does: the only difference is that its host calls
323/// [`crate::world::step_stepped_world`] once per frame instead of a thread
324/// pacing itself.
325pub fn spawn_physics_world_with<R: WorldRunner>(
326 host: &mut World,
327 config: PhysicsConfig,
328) -> WorldHandle {
329 let step_seconds = 1.0 / config.hz as f32;
330 let mut clock = FixedClock::new(Duration::from_secs_f32(step_seconds));
331 let mut debt = 0.0;
332 let mut started = false;
333 R::spawn(
334 host,
335 "physics",
336 config.command_capacity,
337 move |world| init_physics_world(world, &config),
338 move |world, control, (), delta_seconds| {
339 if control.stop_requested() {
340 return false;
341 }
342 if control.self_driven() {
343 let steps = fixed_steps(&mut debt, delta_seconds, step_seconds);
344 // The first turn steps immediately, before its first wait.
345 for _ in 0..steps.max(u32::from(!started)) {
346 step_once(world);
347 }
348 started = true;
349 clock.wait(control);
350 } else {
351 // A stepped host has already decided that one step is due.
352 step_once(world);
353 }
354 true
355 },
356 )
357}
358
359/// Create the physics world and its event plumbing in `world`, with
360/// `config` applied.
361///
362/// Thread-free callers that drive [`step_once`] on their own cadence —
363/// deterministic tests, offline rendering — call this to get the world
364/// [`spawn_physics_world_with`] builds, contact events included.
365pub fn init_physics_world(world: &mut World, config: &PhysicsConfig) {
366 let mut physics = Physics::new(config.gravity);
367 physics.world.integration_parameters.dt = 1.0 / config.hz as f32;
368 world.root_scope_mut().spawn(physics);
369}
370
371/// Step the rapier world once, draining its contacts into
372/// [`PhysicsContacts`].
373///
374/// Returns whether `world` carries a [`PhysicsWorld`] at all. A physics
375/// world's own tick calls this; thread-free callers — deterministic tests,
376/// offline rendering — drive the same simulation on a cadence of their own.
377pub fn step_once(world: &World) -> bool {
378 let mut physics_world_rows = world.root_scope().query::<&mut PhysicsWorld>();
379
380 let Some(physics) = physics_world_rows.iter().next() else {
381 return false;
382 };
383 // The contact events rapier reports this step describe contacts that
384 // began during it, and by the time the step returns the solver has
385 // already turned the approach into a bounce. Sampling the velocities
386 // before the step is what makes the reported speed the approach speed
387 // rather than the separation it left behind.
388 let mut events = physics_events(world);
389 if let Some(events) = events.as_mut() {
390 events.get().sample_velocities(physics);
391 }
392 match events.as_mut() {
393 Some(events) => physics.step_with_events(&(), &events.get().collector),
394 None => physics.step(),
395 }
396 let Some(mut events) = events else {
397 return true;
398 };
399 let Some(mut contacts) = physics_contacts(world) else {
400 return true;
401 };
402 contacts.get().started.clear();
403 contacts.get().stopped.clear();
404 let events = events.get();
405 while let Ok(event) = events.receiver.try_recv() {
406 let (one, two) = match event {
407 CollisionEvent::Started(one, two, _) => (one, two),
408 CollisionEvent::Stopped(one, two, _) => (one, two),
409 };
410 let Some((body_one, body_two)) = body_pair(physics, one, two) else {
411 continue;
412 };
413 match event {
414 CollisionEvent::Started(..) => {
415 let speed = contact_speed(physics, events, one, two);
416 contacts.get().started.push((body_one, body_two, speed));
417 }
418 _ => contacts.get().stopped.push((body_one, body_two)),
419 }
420 }
421 true
422}
423
424/// The speed two colliders close at along their contact normal, from the
425/// velocities sampled before the step that reported the contact.
426///
427/// Rapier reports the normal in the first collider's local frame, so it is
428/// rotated into world space before the relative velocity is projected onto
429/// it. The result is negative while the pair is separating.
430fn contact_speed(
431 physics: &PhysicsWorld,
432 events: &PhysicsEvents,
433 one: ColliderHandle,
434 two: ColliderHandle,
435) -> f32 {
436 // The pair stores its colliders in the broad phase's own order, which
437 // need not match the event's order: the manifold normal belongs to the
438 // pair's first collider, so project the pair's own relative velocity.
439 let Some(pair) = physics.contact_pair(one, two) else {
440 return 0.0;
441 };
442 let (first, second) = (pair.collider1, pair.collider2);
443 let normal = pair
444 .manifolds()
445 .first()
446 .map(|manifold| {
447 let rotation = physics
448 .colliders
449 .get(first)
450 .map_or(Quat::IDENTITY, |collider| collider.rotation());
451 rotation * manifold.local_n1
452 })
453 .unwrap_or(Vec3::ZERO);
454 let velocity = |handle: ColliderHandle| {
455 physics
456 .colliders
457 .get(handle)
458 .and_then(|collider| collider.parent())
459 .map_or(Vec3::ZERO, |body| events.velocity(body))
460 };
461 normal.dot(velocity(first) - velocity(second))
462}
463
464/// The bodies a collider pair belongs to, or nothing when either side is
465/// parentless.
466fn body_pair(
467 physics: &PhysicsWorld,
468 one: ColliderHandle,
469 two: ColliderHandle,
470) -> Option<(RigidBodyHandle, RigidBodyHandle)> {
471 let body = |handle: ColliderHandle| physics.colliders.get(handle)?.parent();
472 Some((body(one)?, body(two)?))
473}