ncollide3d/query/algorithms/
gjk.rs

1//! The Gilbert–Johnson–Keerthi distance algorithm.
2
3use na::{self, Unit};
4use simba::scalar::RealField;
5
6use crate::query::algorithms::{special_support_maps::ConstantOrigin, CSOPoint, VoronoiSimplex};
7use crate::shape::SupportMap;
8// use query::Proximity;
9use crate::math::{Isometry, Point, Vector, DIM};
10use crate::query::{self, Ray};
11
12/// Results of the GJK algorithm.
13#[derive(Clone, Debug, PartialEq)]
14pub enum GJKResult<N: RealField + Copy> {
15    /// Result of the GJK algorithm when the origin is inside of the polytope.
16    Intersection,
17    /// Result of the GJK algorithm when a projection of the origin on the polytope is found.
18    ClosestPoints(Point<N>, Point<N>, Unit<Vector<N>>),
19    /// Result of the GJK algorithm when the origin is too close to the polytope but not inside of it.
20    Proximity(Unit<Vector<N>>),
21    /// Result of the GJK algorithm when the origin is too far away from the polytope.
22    NoIntersection(Unit<Vector<N>>),
23}
24
25/// The absolute tolerence used by the GJK algorithm.
26pub fn eps_tol<N: RealField + Copy>() -> N {
27    let _eps = N::default_epsilon();
28    _eps * na::convert(10.0f64)
29}
30
31/// Projects the origin on the boundary of the given shape.
32///
33/// The origin is assumed to be outside of the shape. If it is inside,
34/// use the EPA algorithm instead.
35/// Return `None` if the origin is not inside of the shape or if
36/// the EPA algorithm failed to compute the projection.
37pub fn project_origin<N, G: ?Sized>(
38    m: &Isometry<N>,
39    g: &G,
40    simplex: &mut VoronoiSimplex<N>,
41) -> Option<Point<N>>
42where
43    N: RealField + Copy,
44    G: SupportMap<N>,
45{
46    match closest_points(
47        m,
48        g,
49        &Isometry::identity(),
50        &ConstantOrigin,
51        N::max_value().unwrap(),
52        true,
53        simplex,
54    ) {
55        GJKResult::Intersection => None,
56        GJKResult::ClosestPoints(p, _, _) => Some(p),
57        _ => unreachable!(),
58    }
59}
60
61/*
62 * Separating Axis GJK
63 */
64/// Projects the origin on a shape using the Separating Axis GJK algorithm.
65/// The algorithm will stop as soon as the polytope can be proven to be at least `max_dist` away
66/// from the origin.
67///
68/// # Arguments:
69/// * simplex - the simplex to be used by the GJK algorithm. It must be already initialized
70///             with at least one point on the shape boundary.
71/// * exact_dist - if `false`, the gjk will stop as soon as it can prove that the origin is at
72/// a distance smaller than `max_dist` but not inside of `shape`. In that case, it returns a
73/// `GJKResult::Proximity(sep_axis)` where `sep_axis` is a separating axis. If `false` the gjk will
74/// compute the exact distance and return `GJKResult::Projection(point)` if the origin is closer
75/// than `max_dist` but not inside `shape`.
76pub fn closest_points<N, G1: ?Sized, G2: ?Sized>(
77    m1: &Isometry<N>,
78    g1: &G1,
79    m2: &Isometry<N>,
80    g2: &G2,
81    max_dist: N,
82    exact_dist: bool,
83    simplex: &mut VoronoiSimplex<N>,
84) -> GJKResult<N>
85where
86    N: RealField + Copy,
87    G1: SupportMap<N>,
88    G2: SupportMap<N>,
89{
90    let _eps = N::default_epsilon();
91    let _eps_tol: N = eps_tol();
92    let _eps_rel: N = _eps_tol.sqrt();
93
94    // FIXME: reset the simplex if it is empty?
95    let mut proj = simplex.project_origin_and_reduce();
96
97    let mut old_dir;
98
99    if let Some(proj_dir) = Unit::try_new(proj.coords, N::zero()) {
100        old_dir = -proj_dir;
101    } else {
102        return GJKResult::Intersection;
103    }
104
105    let mut max_bound = N::max_value().unwrap();
106    let mut dir;
107    let mut niter = 0;
108
109    loop {
110        let old_max_bound = max_bound;
111
112        if let Some((new_dir, dist)) = Unit::try_new_and_get(-proj.coords, _eps_tol) {
113            dir = new_dir;
114            max_bound = dist;
115        } else {
116            // The origin is on the simplex.
117            return GJKResult::Intersection;
118        }
119
120        if max_bound >= old_max_bound {
121            if exact_dist {
122                let (p1, p2) = result(simplex, true);
123                return GJKResult::ClosestPoints(p1, p2, old_dir); // upper bounds inconsistencies
124            } else {
125                return GJKResult::Proximity(old_dir);
126            }
127        }
128
129        let cso_point = CSOPoint::from_shapes(m1, g1, m2, g2, &dir);
130        let min_bound = -dir.dot(&cso_point.point.coords);
131
132        assert!(min_bound == min_bound);
133
134        if min_bound > max_dist {
135            return GJKResult::NoIntersection(dir);
136        } else if !exact_dist && min_bound > na::zero() && max_bound <= max_dist {
137            return GJKResult::Proximity(old_dir);
138        } else if max_bound - min_bound <= _eps_rel * max_bound {
139            if exact_dist {
140                let (p1, p2) = result(simplex, false);
141                return GJKResult::ClosestPoints(p1, p2, dir); // the distance found has a good enough precision
142            } else {
143                return GJKResult::Proximity(dir);
144            }
145        }
146
147        if !simplex.add_point(cso_point) {
148            if exact_dist {
149                let (p1, p2) = result(simplex, false);
150                return GJKResult::ClosestPoints(p1, p2, dir);
151            } else {
152                return GJKResult::Proximity(dir);
153            }
154        }
155
156        old_dir = dir;
157        proj = simplex.project_origin_and_reduce();
158
159        if simplex.dimension() == DIM {
160            if min_bound >= _eps_tol {
161                if exact_dist {
162                    let (p1, p2) = result(simplex, true);
163                    return GJKResult::ClosestPoints(p1, p2, old_dir);
164                } else {
165                    // NOTE: previous implementation used old_proj here.
166                    return GJKResult::Proximity(old_dir);
167                }
168            } else {
169                return GJKResult::Intersection; // Point inside of the cso.
170            }
171        }
172        niter += 1;
173        if niter == 10000 {
174            return GJKResult::NoIntersection(Vector::x_axis());
175        }
176    }
177}
178
179/// Casts a ray on a support map using the GJK algorithm.
180pub fn cast_ray<N, G: ?Sized>(
181    m: &Isometry<N>,
182    shape: &G,
183    simplex: &mut VoronoiSimplex<N>,
184    ray: &Ray<N>,
185    max_toi: N,
186) -> Option<(N, Vector<N>)>
187where
188    N: RealField + Copy,
189    G: SupportMap<N>,
190{
191    let m2 = Isometry::identity();
192    let g2 = ConstantOrigin;
193    minkowski_ray_cast(m, shape, &m2, &g2, ray, max_toi, simplex)
194}
195
196/// Compute the normal and the distance that can travel `g1` along the direction
197/// `dir` so that `g1` and `g2` just touch.
198pub fn directional_distance<N, G1: ?Sized, G2: ?Sized>(
199    m1: &Isometry<N>,
200    g1: &G1,
201    m2: &Isometry<N>,
202    g2: &G2,
203    dir: &Vector<N>,
204    simplex: &mut VoronoiSimplex<N>,
205) -> Option<(N, Vector<N>, Point<N>, Point<N>)>
206where
207    N: RealField + Copy,
208    G1: SupportMap<N>,
209    G2: SupportMap<N>,
210{
211    let ray = Ray::new(Point::origin(), *dir);
212    minkowski_ray_cast(m1, g1, m2, g2, &ray, N::max_value().unwrap(), simplex).map(
213        |(toi, normal)| {
214            let witnesses = if !toi.is_zero() {
215                result(simplex, simplex.dimension() == DIM)
216            } else {
217                // If there is penetration, the witness points
218                // are undefined.
219                (Point::origin(), Point::origin())
220            };
221
222            (toi, normal, witnesses.0, witnesses.1)
223        },
224    )
225}
226
227// Ray-cast on the Minkowski Difference `m1 * g1 - m2 * g2`.
228fn minkowski_ray_cast<N, G1: ?Sized, G2: ?Sized>(
229    m1: &Isometry<N>,
230    g1: &G1,
231    m2: &Isometry<N>,
232    g2: &G2,
233    ray: &Ray<N>,
234    max_toi: N,
235    simplex: &mut VoronoiSimplex<N>,
236) -> Option<(N, Vector<N>)>
237where
238    N: RealField + Copy,
239    G1: SupportMap<N>,
240    G2: SupportMap<N>,
241{
242    let _eps = N::default_epsilon();
243    let _eps_tol: N = eps_tol();
244    let _eps_rel: N = _eps_tol.sqrt();
245
246    let ray_length = ray.dir.norm();
247
248    if relative_eq!(ray_length, N::zero()) {
249        return None;
250    }
251
252    let mut ltoi = N::zero();
253    let mut curr_ray = Ray::new(ray.origin, ray.dir / ray_length);
254    let dir = -curr_ray.dir;
255    let mut ldir = dir;
256
257    // Initialize the simplex.
258    let support_point = CSOPoint::from_shapes(m1, g1, m2, g2, &dir);
259    simplex.reset(support_point.translate(&-curr_ray.origin.coords));
260
261    // FIXME: reset the simplex if it is empty?
262    let mut proj = simplex.project_origin_and_reduce();
263    let mut max_bound = N::max_value().unwrap();
264    let mut dir;
265    let mut niter = 0;
266    let mut last_chance = false;
267
268    loop {
269        let old_max_bound = max_bound;
270
271        if let Some((new_dir, dist)) = Unit::try_new_and_get(-proj.coords, _eps_tol) {
272            dir = new_dir;
273            max_bound = dist;
274        } else {
275            return Some((ltoi / ray_length, ldir));
276        }
277
278        let support_point = if max_bound >= old_max_bound {
279            // Upper bounds inconsistencies. Consider the projection as a valid support point.
280            last_chance = true;
281            CSOPoint::single_point(proj + curr_ray.origin.coords)
282        } else {
283            CSOPoint::from_shapes(m1, g1, m2, g2, &dir)
284        };
285
286        if last_chance && ltoi > N::zero() {
287            // last_chance && ltoi > N::zero() && (support_point.point - curr_ray.origin).dot(&ldir) >= N::zero() {
288            return Some((ltoi / ray_length, ldir));
289        }
290
291        // Clip the ray on the support plane (None <=> t < 0)
292        // The configurations are:
293        //   dir.dot(curr_ray.dir)  |   t   |               Action
294        // −−−−−−−−−−−−−−−−−−−−-----+−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
295        //          < 0             |  < 0  | Continue.
296        //          < 0             |  > 0  | New lower bound, move the origin.
297        //          > 0             |  < 0  | Miss. No intersection.
298        //          > 0             |  > 0  | New higher bound.
299        match query::ray_toi_with_plane(&support_point.point, &dir, &curr_ray) {
300            Some(t) => {
301                if dir.dot(&curr_ray.dir) < na::zero() && t > N::zero() {
302                    // new lower bound
303                    ldir = *dir;
304                    ltoi += t;
305
306                    // NOTE: we divide by ray_length instead of doing max_toi * ray_length
307                    // because the multiplication may cause an overflow if max_toi is set
308                    // to N::max_value().unwrap() by users that want to have an infinite ray.
309                    if ltoi / ray_length > max_toi {
310                        return None;
311                    }
312
313                    let shift = curr_ray.dir * t;
314                    curr_ray.origin += shift;
315                    max_bound = N::max_value().unwrap();
316                    simplex.modify_pnts(&|pt| pt.translate_mut(&-shift));
317                    last_chance = false;
318                }
319            }
320            None => {
321                if dir.dot(&curr_ray.dir) > _eps_tol {
322                    // miss
323                    return None;
324                }
325            }
326        }
327
328        if last_chance {
329            return None;
330        }
331
332        let min_bound = -dir.dot(&(support_point.point.coords - curr_ray.origin.coords));
333
334        assert!(min_bound == min_bound);
335
336        if max_bound - min_bound <= _eps_rel * max_bound {
337            // This is needed when using fixed-points to avoid missing
338            // some castes.
339            // FIXME: I feel like we should always return `Some` in
340            // this case, even with floating-point numbers. Though it
341            // has not been sufficinetly tested with floats yet to be sure.
342            if cfg!(feature = "improved_fixed_point_support") {
343                return Some((ltoi / ray_length, ldir));
344            } else {
345                return None;
346            }
347        }
348
349        let _ = simplex.add_point(support_point.translate(&-curr_ray.origin.coords));
350        proj = simplex.project_origin_and_reduce();
351
352        if simplex.dimension() == DIM {
353            if min_bound >= _eps_tol {
354                return None;
355            } else {
356                return Some((ltoi / ray_length, ldir)); // Point inside of the cso.
357            }
358        }
359
360        niter += 1;
361        if niter == 10000 {
362            return None;
363        }
364    }
365}
366
367fn result<N: RealField + Copy>(simplex: &VoronoiSimplex<N>, prev: bool) -> (Point<N>, Point<N>) {
368    let mut res = (Point::origin(), Point::origin());
369    if prev {
370        for i in 0..simplex.prev_dimension() + 1 {
371            let coord = simplex.prev_proj_coord(i);
372            let point = simplex.prev_point(i);
373            res.0 += point.orig1.coords * coord;
374            res.1 += point.orig2.coords * coord;
375        }
376
377        res
378    } else {
379        for i in 0..simplex.dimension() + 1 {
380            let coord = simplex.proj_coord(i);
381            let point = simplex.point(i);
382            res.0 += point.orig1.coords * coord;
383            res.1 += point.orig2.coords * coord;
384        }
385
386        res
387    }
388}