Skip to main content

box2d_rust/
mover.rs

1// Port of mover.c: the plane solver for character movers. Collision planes
2// gathered by `world_collide_mover` are fed to `solve_planes` to find a
3// translation that satisfies them, then `clip_vector` removes velocity into
4// the touched planes.
5//
6// SPDX-FileCopyrightText: 2025 Erin Catto
7// SPDX-License-Identifier: MIT
8
9use crate::constants::linear_slop;
10use crate::math_functions::{
11    abs_float, clamp_float, dot, min_float, mul_add, mul_sub, plane_separation, Plane, Vec2,
12};
13
14/// A collision plane that can be fed to [`solve_planes`]. Normally assembled
15/// by the user from the plane results of `world_collide_mover`.
16/// (b2CollisionPlane)
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct CollisionPlane {
19    /// The collision plane between the mover and some shape
20    pub plane: Plane,
21
22    /// Setting this to f32::MAX makes the plane as rigid as possible. Lower
23    /// values can make the plane collision soft. Usually in meters.
24    pub push_limit: f32,
25
26    /// The push on the mover determined by [`solve_planes`]. Usually in
27    /// meters.
28    pub push: f32,
29
30    /// Indicates if [`clip_vector`] should clip against this plane. Should be
31    /// false for soft collision.
32    pub clip_velocity: bool,
33}
34
35/// Result returned by [`solve_planes`]. (b2PlaneSolverResult)
36#[derive(Debug, Clone, Copy, PartialEq, Default)]
37pub struct PlaneSolverResult {
38    /// The translation of the mover
39    pub translation: Vec2,
40
41    /// The number of iterations used by the plane solver. For diagnostics.
42    pub iteration_count: i32,
43}
44
45/// Solves the position of a mover that satisfies the given collision planes.
46///
47/// `target_delta` is the desired movement from the position used to generate
48/// the collision planes. (b2SolvePlanes)
49pub fn solve_planes(target_delta: Vec2, planes: &mut [CollisionPlane]) -> PlaneSolverResult {
50    for plane in planes.iter_mut() {
51        plane.push = 0.0;
52    }
53
54    let mut delta = target_delta;
55    let tolerance = linear_slop();
56
57    let mut iteration = 0;
58    while iteration < 20 {
59        let mut total_push = 0.0;
60        for plane in planes.iter_mut() {
61            // Add slop to prevent jitter
62            let separation = plane_separation(plane.plane, delta) + linear_slop();
63
64            let push = -separation;
65
66            // Clamp accumulated push
67            let accumulated_push = plane.push;
68            plane.push = clamp_float(plane.push + push, 0.0, plane.push_limit);
69            let push = plane.push - accumulated_push;
70            delta = mul_add(delta, push, plane.plane.normal);
71
72            // Track maximum push for convergence
73            total_push += abs_float(push);
74        }
75
76        if total_push < tolerance {
77            break;
78        }
79
80        iteration += 1;
81    }
82
83    PlaneSolverResult {
84        translation: delta,
85        iteration_count: iteration,
86    }
87}
88
89/// Clips the velocity against the given planes so the mover doesn't keep
90/// pushing into what it already touched. (b2ClipVector)
91pub fn clip_vector(vector: Vec2, planes: &[CollisionPlane]) -> Vec2 {
92    let mut v = vector;
93
94    for plane in planes.iter() {
95        if plane.push == 0.0 || !plane.clip_velocity {
96            continue;
97        }
98
99        v = mul_sub(
100            v,
101            min_float(0.0, dot(v, plane.plane.normal)),
102            plane.plane.normal,
103        );
104    }
105
106    v
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::math_functions::normalize;
113
114    // A mover pushed toward a floor plane must slide along it, and clipping
115    // must remove the velocity component into the plane.
116    #[test]
117    fn solve_and_clip_against_floor() {
118        // Floor with normal +y, mover 0.05 m above it (separation offset).
119        let floor = Plane {
120            normal: Vec2 { x: 0.0, y: 1.0 },
121            offset: -0.05,
122        };
123        let mut planes = [CollisionPlane {
124            plane: floor,
125            push_limit: f32::MAX,
126            push: 0.0,
127            clip_velocity: true,
128        }];
129
130        // Try to move diagonally down into the floor.
131        let target = Vec2 { x: 1.0, y: -1.0 };
132        let result = solve_planes(target, &mut planes);
133
134        // Horizontal motion survives; vertical penetration is pushed out to
135        // roughly the plane surface (within slop).
136        assert!((result.translation.x - 1.0).abs() < 1e-6);
137        assert!(result.translation.y > -0.1);
138        assert!(planes[0].push > 0.0);
139
140        // Velocity into the plane is removed, tangential velocity kept.
141        let velocity = Vec2 { x: 2.0, y: -3.0 };
142        let clipped = clip_vector(velocity, &planes);
143        assert!((clipped.x - 2.0).abs() < 1e-6);
144        assert!(clipped.y.abs() < 1e-6);
145
146        // A soft plane (clip_velocity = false) leaves velocity alone.
147        planes[0].clip_velocity = false;
148        let unclipped = clip_vector(velocity, &planes);
149        assert_eq!(unclipped, velocity);
150    }
151
152    // The iterative solver converges for a wedge of two planes.
153    #[test]
154    fn solve_planes_wedge() {
155        let mut planes = [
156            CollisionPlane {
157                plane: Plane {
158                    normal: normalize(Vec2 { x: 1.0, y: 1.0 }),
159                    offset: 0.0,
160                },
161                push_limit: f32::MAX,
162                push: 0.0,
163                clip_velocity: true,
164            },
165            CollisionPlane {
166                plane: Plane {
167                    normal: normalize(Vec2 { x: -1.0, y: 1.0 }),
168                    offset: 0.0,
169                },
170                push_limit: f32::MAX,
171                push: 0.0,
172                clip_velocity: true,
173            },
174        ];
175
176        // Push straight down into the wedge: the solved translation must not
177        // penetrate either plane by more than the slop tolerance.
178        let result = solve_planes(Vec2 { x: 0.0, y: -1.0 }, &mut planes);
179        for plane in planes.iter() {
180            assert!(plane_separation(plane.plane, result.translation) > -0.02);
181        }
182        assert!(result.iteration_count < 20);
183    }
184}