parry3d/query/gjk/
gjk.rs

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