ferravex_dynamics/
world.rs

1use crate::{collision::*, math::*, *};
2
3struct Slot<const D: usize> {
4    generation: u32,
5    body: Option<Body<D>>,
6}
7#[derive(Clone, Copy)]
8struct Proxy<const D: usize> {
9    index: usize,
10    min: [f32; D],
11    max: [f32; D],
12}
13#[derive(Clone, Copy)]
14struct Motion<const D: usize> {
15    index: usize,
16    position: [f32; D],
17    velocity: [f32; D],
18}
19#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
20pub struct StepStats {
21    pub bodies: usize,
22    pub broadphase_tests: usize,
23    pub candidate_pairs: usize,
24    pub contacts: usize,
25    pub ccd_impacts: usize,
26    /// Remaining movement was discarded to stay conservative after this body's impact budget.
27    pub ccd_budget_stops: usize,
28}
29
30/// Reuses bounded scratch buffers and returns an error rather than dropping
31/// overflowing collision pairs. Failed steps restore all body motion and forces.
32/// Buffers grow lazily to the configured bounds, then reuse their storage.
33pub struct PhysicsWorld<const D: usize> {
34    config: WorldConfig<D>,
35    slots: Vec<Slot<D>>,
36    free: Vec<usize>,
37    len: usize,
38    proxies: Vec<Proxy<D>>,
39    pairs: Vec<(usize, usize)>,
40    backup: Vec<Motion<D>>,
41    contacts: Vec<Contact<D>>,
42    next_contacts: Vec<Contact<D>>,
43    events: Vec<ContactEvent<D>>,
44    pending_stops: Vec<ContactEvent<D>>,
45}
46impl<const D: usize> PhysicsWorld<D> {
47    /// Construct a world with default finite work/memory limits.
48    /// Panics for dimensions other than 2/3 or invalid gravity; use `with_config`
49    /// for fallible construction from untrusted settings.
50    pub fn new(gravity: [f32; D]) -> Self {
51        Self::with_config(WorldConfig::new(gravity)).expect("valid physics world configuration")
52    }
53    pub fn with_config(config: WorldConfig<D>) -> Result<Self, PhysicsError> {
54        if D != 2 && D != 3 {
55            return Err(PhysicsError::InvalidDimension);
56        }
57        if !valid(config.gravity)
58            || config.max_bodies == 0
59            || config.max_bodies > 1_000_000
60            || config.max_pairs == 0
61            || config.max_pairs > 10_000_000
62            || config.max_pair_tests == 0
63            || config.max_pair_tests > 100_000_000
64            || !(1..=64).contains(&config.solver_iterations)
65            || !(1..=16).contains(&config.ccd_iterations)
66            || !config.contact_slop.is_finite()
67            || !(1e-6..=0.1).contains(&config.contact_slop)
68        {
69            return Err(PhysicsError::InvalidConfiguration);
70        }
71        Ok(Self {
72            config,
73            slots: Vec::new(),
74            free: Vec::new(),
75            len: 0,
76            proxies: Vec::new(),
77            pairs: Vec::new(),
78            backup: Vec::new(),
79            contacts: Vec::new(),
80            next_contacts: Vec::new(),
81            events: Vec::new(),
82            pending_stops: Vec::new(),
83        })
84    }
85    pub fn config(&self) -> WorldConfig<D> {
86        self.config
87    }
88    pub fn len(&self) -> usize {
89        self.len
90    }
91    pub fn is_empty(&self) -> bool {
92        self.len == 0
93    }
94    pub fn set_gravity(&mut self, gravity: [f32; D]) -> Result<(), PhysicsError> {
95        if !valid(gravity) {
96            return Err(PhysicsError::InvalidConfiguration);
97        }
98        self.config.gravity = gravity;
99        Ok(())
100    }
101    pub fn insert(&mut self, desc: BodyDesc<D>) -> Result<BodyHandle, PhysicsError> {
102        if !desc.valid() || desc.kind == BodyKind::Static && desc.velocity != [0.; D] {
103            return Err(PhysicsError::InvalidBody);
104        }
105        if self.len >= self.config.max_bodies {
106            return Err(PhysicsError::BodyCapacity);
107        }
108        let body = Body {
109            desc,
110            force: [0.; D],
111        };
112        let index = if let Some(index) = self.free.pop() {
113            self.slots[index].body = Some(body);
114            index
115        } else {
116            // Retired generation-overflow slots count against storage capacity.
117            if self.slots.len() >= self.config.max_bodies {
118                return Err(PhysicsError::BodyCapacity);
119            }
120            self.slots.push(Slot {
121                generation: 0,
122                body: Some(body),
123            });
124            self.slots.len() - 1
125        };
126        self.len += 1;
127        Ok(self.handle(index))
128    }
129    fn handle(&self, index: usize) -> BodyHandle {
130        BodyHandle {
131            index: index as u32,
132            generation: self.slots[index].generation,
133        }
134    }
135    pub fn body(&self, handle: BodyHandle) -> Option<&Body<D>> {
136        let slot = self.slots.get(handle.index as usize)?;
137        (slot.generation == handle.generation)
138            .then_some(slot.body.as_ref())
139            .flatten()
140    }
141    pub fn body_mut(&mut self, handle: BodyHandle) -> Option<&mut Body<D>> {
142        let slot = self.slots.get_mut(handle.index as usize)?;
143        (slot.generation == handle.generation)
144            .then_some(slot.body.as_mut())
145            .flatten()
146    }
147    pub fn bodies(&self) -> impl Iterator<Item = (BodyHandle, &Body<D>)> {
148        self.slots
149            .iter()
150            .enumerate()
151            .filter_map(|(i, s)| s.body.as_ref().map(|b| (self.handle(i), b)))
152    }
153    /// Removal invalidates the handle. Its outstanding contacts generate Stopped
154    /// events on the next successful step, once each, including sensor contacts.
155    pub fn remove(&mut self, handle: BodyHandle) -> Option<Body<D>> {
156        let slot = self.slots.get_mut(handle.index as usize)?;
157        if slot.generation != handle.generation {
158            return None;
159        }
160        let body = slot.body.take()?;
161        if slot.generation != u32::MAX {
162            slot.generation += 1;
163            self.free.push(handle.index as usize);
164        }
165        self.len -= 1;
166        self.contacts.retain(|contact| {
167            if contact.a == handle || contact.b == handle {
168                self.pending_stops.push(ContactEvent {
169                    kind: ContactEventKind::Stopped,
170                    contact: *contact,
171                });
172                false
173            } else {
174                true
175            }
176        });
177        Some(body)
178    }
179    pub fn contacts(&self) -> &[Contact<D>] {
180        &self.contacts
181    }
182    /// Started/Stopped transitions from the last successful step. Persistent
183    /// overlaps are available through `contacts`, not repeated Started events.
184    pub fn events(&self) -> &[ContactEvent<D>] {
185        &self.events
186    }
187
188    /// Advance exactly one timestep in (0, 0.1] seconds. Use a fixed dt, commonly
189    /// 1/120, from the caller's fixed clock. Forces last for one successful step.
190    pub fn step(&mut self, dt: f32) -> Result<StepStats, PhysicsError> {
191        if !dt.is_finite() || dt <= 0. || dt > 0.1 {
192            return Err(PhysicsError::InvalidTimestep);
193        }
194        self.backup.clear();
195        for (index, slot) in self.slots.iter().enumerate() {
196            if let Some(body) = &slot.body {
197                self.backup.push(Motion {
198                    index,
199                    position: body.desc.position,
200                    velocity: body.desc.velocity,
201                });
202            }
203        }
204        let result = self.advance(dt);
205        if result.is_err() {
206            for m in &self.backup {
207                let b = self.slots[m.index].body.as_mut().unwrap();
208                b.desc.position = m.position;
209                b.desc.velocity = m.velocity;
210            }
211        }
212        result
213    }
214    fn advance(&mut self, dt: f32) -> Result<StepStats, PhysicsError> {
215        let mut stats = StepStats {
216            bodies: self.len,
217            ..StepStats::default()
218        };
219        for slot in &mut self.slots {
220            let Some(body) = &mut slot.body else {
221                continue;
222            };
223            if body.inv_mass() > 0. {
224                let acceleration = add(
225                    mul(self.config.gravity, body.desc.gravity_scale),
226                    mul(body.force, body.inv_mass()),
227                );
228                body.desc.velocity = mul(
229                    add(body.desc.velocity, mul(acceleration, dt)),
230                    1. / (1. + body.desc.linear_damping * dt),
231                );
232            }
233            if !valid(body.desc.velocity) {
234                return Err(PhysicsError::NumericalLimit);
235            }
236            if body.desc.kind != BodyKind::Static
237                && !(body.desc.ccd
238                    && body.inv_mass() > 0.
239                    && matches!(body.desc.shape, Shape::Ball { .. })
240                    && !body.desc.sensor)
241            {
242                body.desc.position = add(body.desc.position, mul(body.desc.velocity, dt));
243            }
244        }
245        for index in 0..self.slots.len() {
246            let Some(body) = self.slots[index].body.as_ref() else {
247                continue;
248            };
249            if !body.desc.ccd || body.desc.sensor || body.inv_mass() == 0. {
250                continue;
251            }
252            let Shape::Ball { radius } = body.desc.shape else {
253                continue;
254            };
255            let desc = body.desc;
256            let mut p = desc.position;
257            let mut v = desc.velocity;
258            let mut remaining = dt;
259            let skin = self.config.contact_slop * 0.2;
260            for _ in 0..self.config.ccd_iterations {
261                let displacement = mul(v, remaining);
262                let distance = length(displacement);
263                if distance < 1e-8 {
264                    remaining = 0.;
265                    break;
266                }
267                let direction = mul(displacement, distance.recip());
268                let filter = QueryFilter {
269                    exclude: Some(self.handle(index)),
270                    layers: desc.layers,
271                    only_static: true,
272                    ..QueryFilter::default()
273                };
274                let Some(hit) = self.cast_ball(p, radius, direction, distance, filter) else {
275                    p = add(p, displacement);
276                    remaining = 0.;
277                    break;
278                };
279                stats.ccd_impacts += 1;
280                p = add(p, mul(direction, (hit.distance - skin).max(0.)));
281                let target = self.body(hit.body).unwrap();
282                let normal_speed = dot(v, hit.normal);
283                if normal_speed < 0. {
284                    let bounce = if normal_speed < -1. {
285                        desc.restitution.max(target.desc.restitution)
286                    } else {
287                        0.
288                    };
289                    let normal_change = -(1. + bounce) * normal_speed;
290                    v = add(v, mul(hit.normal, normal_change));
291                    let tangent = sub(v, mul(hit.normal, dot(v, hit.normal)));
292                    let tangent_speed = length(tangent);
293                    if tangent_speed > 1e-8 {
294                        let friction = (desc.friction * target.desc.friction).sqrt();
295                        v = sub(
296                            v,
297                            mul(tangent, (friction * normal_change / tangent_speed).min(1.)),
298                        );
299                    }
300                }
301                remaining *= 1. - (hit.distance / distance).clamp(0., 1.);
302                // A body initially embedded in terrain is left for the discrete
303                // penetration solver; repeated zero-time impacts cannot loop.
304                if hit.distance < skin && dot(v, hit.normal) <= 1e-6 {
305                    break;
306                }
307            }
308            if remaining > 1e-8 {
309                stats.ccd_budget_stops += 1;
310            }
311            let body = self.slots[index].body.as_mut().unwrap();
312            body.desc.position = p;
313            body.desc.velocity = v;
314        }
315        if self
316            .slots
317            .iter()
318            .filter_map(|s| s.body.as_ref())
319            .any(|b| !valid(b.desc.position))
320        {
321            return Err(PhysicsError::NumericalLimit);
322        }
323        self.broadphase(&mut stats)?;
324        let margin = self.config.contact_slop * 2.;
325        // Sequential impulses plus iterative mass-weighted penetration correction.
326        for _ in 0..self.config.solver_iterations {
327            for &(a, b) in &self.pairs {
328                let (left, right) = self.slots.split_at_mut(b);
329                let a = left[a].body.as_mut().unwrap();
330                let b = right[0].body.as_mut().unwrap();
331                if a.desc.sensor || b.desc.sensor {
332                    continue;
333                }
334                let Some(m) = contact(
335                    a.desc.shape,
336                    a.desc.position,
337                    b.desc.shape,
338                    b.desc.position,
339                    margin,
340                ) else {
341                    continue;
342                };
343                solve(a, b, m, self.config.contact_slop);
344            }
345        }
346        if self
347            .slots
348            .iter()
349            .filter_map(|s| s.body.as_ref())
350            .any(|b| !valid(b.desc.position) || !valid(b.desc.velocity))
351        {
352            return Err(PhysicsError::NumericalLimit);
353        }
354        self.next_contacts.clear();
355        for &(a, b) in &self.pairs {
356            let aa = self.slots[a].body.as_ref().unwrap();
357            let bb = self.slots[b].body.as_ref().unwrap();
358            if let Some(m) = contact(
359                aa.desc.shape,
360                aa.desc.position,
361                bb.desc.shape,
362                bb.desc.position,
363                if aa.desc.sensor || bb.desc.sensor {
364                    0.
365                } else {
366                    margin
367                },
368            ) {
369                self.next_contacts.push(Contact {
370                    a: self.handle(a),
371                    b: self.handle(b),
372                    normal: m.normal,
373                    penetration: m.penetration,
374                    sensor: aa.desc.sensor || bb.desc.sensor,
375                });
376            }
377        }
378        self.next_contacts.sort_unstable_by_key(|c| (c.a, c.b));
379        self.events.clear();
380        self.events.append(&mut self.pending_stops);
381        let mut old = 0;
382        let mut new = 0;
383        while old < self.contacts.len() || new < self.next_contacts.len() {
384            let a = self.contacts.get(old);
385            let b = self.next_contacts.get(new);
386            let order = match (a, b) {
387                (Some(a), Some(b)) => (a.a, a.b).cmp(&(b.a, b.b)),
388                (Some(_), None) => std::cmp::Ordering::Less,
389                (None, Some(_)) => std::cmp::Ordering::Greater,
390                (None, None) => unreachable!(),
391            };
392            match order {
393                std::cmp::Ordering::Less => {
394                    self.events.push(ContactEvent {
395                        kind: ContactEventKind::Stopped,
396                        contact: *a.unwrap(),
397                    });
398                    old += 1;
399                }
400                std::cmp::Ordering::Greater => {
401                    self.events.push(ContactEvent {
402                        kind: ContactEventKind::Started,
403                        contact: *b.unwrap(),
404                    });
405                    new += 1;
406                }
407                std::cmp::Ordering::Equal => {
408                    old += 1;
409                    new += 1;
410                }
411            }
412        }
413        std::mem::swap(&mut self.contacts, &mut self.next_contacts);
414        for body in self.slots.iter_mut().filter_map(|s| s.body.as_mut()) {
415            body.clear_forces();
416        }
417        stats.contacts = self.contacts.len();
418        Ok(stats)
419    }
420    fn broadphase(&mut self, stats: &mut StepStats) -> Result<(), PhysicsError> {
421        self.proxies.clear();
422        self.pairs.clear();
423        let margin = self.config.contact_slop * 2.;
424        for (index, slot) in self.slots.iter().enumerate() {
425            if let Some(b) = &slot.body {
426                let half = b.desc.shape.half().map(|v| v + margin);
427                self.proxies.push(Proxy {
428                    index,
429                    min: sub(b.desc.position, half),
430                    max: add(b.desc.position, half),
431                });
432            }
433        }
434        // Sweep along the axis with greatest center variance to avoid the common
435        // degenerate case of a vertical stack whose every X interval overlaps.
436        let mut sum = [0.; D];
437        let mut squared = [0.; D];
438        for p in &self.proxies {
439            for axis in 0..D {
440                let c = (p.min[axis] + p.max[axis]) * 0.5;
441                sum[axis] += c;
442                squared[axis] += c * c;
443            }
444        }
445        let count = self.len.max(1) as f32;
446        let axis = (0..D)
447            .max_by(|&a, &b| {
448                (squared[a] - sum[a] * sum[a] / count)
449                    .total_cmp(&(squared[b] - sum[b] * sum[b] / count))
450            })
451            .unwrap();
452        self.proxies.sort_unstable_by(|a, b| {
453            a.min[axis]
454                .total_cmp(&b.min[axis])
455                .then(a.index.cmp(&b.index))
456        });
457        for i in 0..self.proxies.len() {
458            let a = self.proxies[i];
459            for b in &self.proxies[i + 1..] {
460                if b.min[axis] > a.max[axis] {
461                    break;
462                }
463                stats.broadphase_tests += 1;
464                if stats.broadphase_tests > self.config.max_pair_tests {
465                    return Err(PhysicsError::PairCapacity);
466                }
467                if (0..D).any(|axis| a.min[axis] > b.max[axis] || b.min[axis] > a.max[axis]) {
468                    continue;
469                }
470                let aa = self.slots[a.index].body.as_ref().unwrap();
471                let bb = self.slots[b.index].body.as_ref().unwrap();
472                if !aa.desc.layers.allows(bb.desc.layers) {
473                    continue;
474                }
475                if aa.desc.kind == BodyKind::Static
476                    && bb.desc.kind == BodyKind::Static
477                    && !aa.desc.sensor
478                    && !bb.desc.sensor
479                {
480                    continue;
481                }
482                if self.pairs.len() == self.config.max_pairs {
483                    return Err(PhysicsError::PairCapacity);
484                }
485                self.pairs
486                    .push((a.index.min(b.index), a.index.max(b.index)));
487            }
488        }
489        // Stable handle order makes solver order independent of the sweep axis.
490        self.pairs.sort_unstable();
491        stats.candidate_pairs = self.pairs.len();
492        Ok(())
493    }
494    fn query_allows(&self, handle: BodyHandle, body: &Body<D>, filter: QueryFilter) -> bool {
495        filter.exclude != Some(handle)
496            && filter.layers.allows(body.desc.layers)
497            && (filter.include_sensors || !body.desc.sensor)
498            && (!filter.only_static || body.desc.kind == BodyKind::Static)
499    }
500    /// Nearest ray hit, reflecting current body transforms immediately. Direction
501    /// is normalized, so `max_distance` and hit distances are in world units.
502    pub fn raycast(
503        &self,
504        origin: [f32; D],
505        direction: [f32; D],
506        max_distance: f32,
507        filter: QueryFilter,
508    ) -> Option<RayHit<D>> {
509        if !valid(origin)
510            || !valid(direction)
511            || !max_distance.is_finite()
512            || !(0. ..=1_000_000.).contains(&max_distance)
513        {
514            return None;
515        }
516        let direction = normalize(direction)?;
517        let mut max = max_distance;
518        let mut result = None;
519        for (handle, body) in self.bodies() {
520            if !self.query_allows(handle, body, filter) {
521                continue;
522            }
523            let hit = match body.desc.shape {
524                Shape::Ball { radius } => {
525                    ray_ball(origin, direction, body.desc.position, radius, max)
526                }
527                Shape::Box { half_extents } => {
528                    ray_box(origin, direction, body.desc.position, half_extents, max)
529                }
530            };
531            if let Some((distance, normal)) = hit {
532                if distance == max && result.is_some() {
533                    continue;
534                }
535                max = distance;
536                result = Some(RayHit {
537                    body: handle,
538                    distance,
539                    point: add(origin, mul(direction, distance)),
540                    normal,
541                });
542            }
543        }
544        result
545    }
546    /// Cast a circle/sphere continuously. `point` is the moving ball's center at
547    /// contact, not its surface point. Exact against boxes' rounded Minkowski
548    /// boundaries. Sensors are excluded by default; filtering is explicit.
549    pub fn cast_ball(
550        &self,
551        origin: [f32; D],
552        radius: f32,
553        direction: [f32; D],
554        max_distance: f32,
555        filter: QueryFilter,
556    ) -> Option<RayHit<D>> {
557        if !valid(origin)
558            || !valid(direction)
559            || !Shape::<D>::ball(radius).valid()
560            || !max_distance.is_finite()
561            || !(0. ..=1_000_000.).contains(&max_distance)
562        {
563            return None;
564        }
565        let direction = normalize(direction)?;
566        let mut max = max_distance;
567        let mut result = None;
568        for (handle, body) in self.bodies() {
569            if !self.query_allows(handle, body, filter) {
570                continue;
571            }
572            let hit = match body.desc.shape {
573                Shape::Ball {
574                    radius: target_radius,
575                } => {
576                    let hit = ray_ball(
577                        origin,
578                        direction,
579                        body.desc.position,
580                        radius + target_radius,
581                        max,
582                    );
583                    hit.filter(|(distance, normal)| {
584                        *distance > 1e-6
585                            || dot(direction, *normal) < 0.
586                            || length(sub(origin, body.desc.position))
587                                < radius + target_radius - 1e-5
588                    })
589                }
590                Shape::Box { half_extents } => sweep_ball_box(
591                    origin,
592                    radius,
593                    direction,
594                    body.desc.position,
595                    half_extents,
596                    max,
597                ),
598            };
599            if let Some((distance, normal)) = hit {
600                if distance == max && result.is_some() {
601                    continue;
602                }
603                max = distance;
604                result = Some(RayHit {
605                    body: handle,
606                    distance,
607                    point: add(origin, mul(direction, distance)),
608                    normal,
609                });
610            }
611        }
612        result
613    }
614}
615
616fn solve<const D: usize>(a: &mut Body<D>, b: &mut Body<D>, m: Manifold<D>, slop: f32) {
617    let ma = a.inv_mass();
618    let mb = b.inv_mass();
619    let inverse_mass = ma + mb;
620    if inverse_mass == 0. {
621        return;
622    }
623    let relative = sub(b.desc.velocity, a.desc.velocity);
624    let speed = dot(relative, m.normal);
625    if speed < 0. {
626        // Low-speed contacts must not bounce forever under gravity.
627        let restitution = if speed < -1. {
628            a.desc.restitution.max(b.desc.restitution)
629        } else {
630            0.
631        };
632        let impulse = -(1. + restitution) * speed / inverse_mass;
633        a.desc.velocity = sub(a.desc.velocity, mul(m.normal, impulse * ma));
634        b.desc.velocity = add(b.desc.velocity, mul(m.normal, impulse * mb));
635        let tangent = sub(relative, mul(m.normal, speed));
636        let tangent_speed = length(tangent);
637        if tangent_speed > 1e-8 {
638            let friction = (a.desc.friction * b.desc.friction).sqrt();
639            let friction_impulse = (tangent_speed / inverse_mass).min(friction * impulse);
640            let friction_vector = mul(tangent, friction_impulse / tangent_speed);
641            a.desc.velocity = add(a.desc.velocity, mul(friction_vector, ma));
642            b.desc.velocity = sub(b.desc.velocity, mul(friction_vector, mb));
643        }
644    }
645    let correction = (m.penetration - slop).max(0.) * 0.75 / inverse_mass;
646    a.desc.position = sub(a.desc.position, mul(m.normal, correction * ma));
647    b.desc.position = add(b.desc.position, mul(m.normal, correction * mb));
648}