1 /* Gilbert-Johnson-Keerthi intersection algorithm with Expanding Polytope Algorithm
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3 of the License ONLY.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module iv
.vmath2d
.gjk
;
22 version = vm2d_debug_gjk_warnings
;
23 //version = vm2d_debug_save_minkowski_points;
24 //version = vm2d_debug_count_iterations;
26 version(vm2d_debug_count_iterations
) int gjkIterationCount
, epaIterationCount
;
29 // ////////////////////////////////////////////////////////////////////////// //
30 /* GJK object should support:
32 * Vec2 support (Vec2 dir) -- in world space
33 * bool inside (Vec2 pt) -- is point inside body?
37 * // dumb O(n) support function, just brute force check all points
38 * vec2 support() (in auto ref vec2 dir) const {
39 * VT furthestPoint = verts[0];
40 * auto maxDot = furthestPoint.dot(dir);
41 * foreach (const ref v; verts[1..$]) {
42 * auto d = v.dot(dir);
48 * return furthestPoint;
53 // ////////////////////////////////////////////////////////////////////////// //
54 /// check if two convex shapes are colliding. can optionally return separating vector in `sepmove`.
55 public bool gjkcollide(CT
, VT
) (auto ref CT body0
, auto ref CT body1
, VT
* sepmove
=null) if (IsGoodVertexStorage
!(CT
, VT
)) {
56 VT sdir
= body1
.centroid
-body0
.centroid
; // initial search direction
57 if (sdir
.isZero
) sdir
= VT(1, 0); // use arbitrary normal if initial direction is zero
58 VT
[3] spx
= void; // simplex; [0] is most recently added, [2] is oldest
59 spx
.ptr
[2] = getSupportPoint(body0
, body1
, sdir
);
60 if (spx
.ptr
[2].dot(sdir
) <= 0) return false; // past the origin
61 sdir
= -sdir
; // change search direction
63 version(vm2d_debug_count_iterations
) gjkIterationCount
= 0;
65 version(vm2d_debug_count_iterations
) ++gjkIterationCount
;
66 spx
.ptr
[spxidx
] = getSupportPoint(body0
, body1
, sdir
);
67 if (spx
.ptr
[spxidx
].dot(sdir
) <= 0) return false; // past the origin
68 if (checkSimplex(sdir
, spx
[spxidx
..$])) {
69 if (sepmove
!is null) *sepmove
= EPA(body0
, body1
, spx
[spxidx
..$]);
78 // ////////////////////////////////////////////////////////////////////////// //
79 /// return distance between two convex shapes, and separation normal
80 /// negative distance means that shapes are overlapping, and zero distance means touching (and ops are invalid)
81 public auto gjkdistance(CT
, VT
) (auto ref CT body0
, auto ref CT body1
, VT
* op0
=null, VT
* op1
=null, VT
* sepnorm
=null) if (IsGoodVertexStorage
!(CT
, VT
)) {
82 static VT
segClosestToOrigin() (in auto ref VT segp0
, in auto ref VT segp1
) {
83 immutable oseg
= segp1
-segp0
;
84 immutable ab2
= oseg
.dot(oseg
);
85 immutable apab
= (-segp0
).dot(oseg
);
86 if (ab2
<= EPSILON
!(VT
.Float
)) return segp0
;
87 VT
.Float t
= apab
/ab2
;
88 if (t
< 0) t
= 0; else if (t
> 1) t
= 1;
92 enum GetSupport(string smpx
) =
93 smpx
~"p0 = body0.support(d);\n"~
94 smpx
~"p1 = body1.support(-d);\n"~
95 smpx
~" = "~smpx
~"p0-"~smpx
~"p1;";
97 if (sepnorm
!is null) *sepnorm
= VT(0, 0);
98 if (op0
!is null) *op0
= VT(0, 0);
99 if (op1
!is null) *op1
= VT(0, 0);
101 VT a
, b
, c
; // simplex
102 VT ap0
, bp0
, cp0
; // simplex support points, needed for closest points calculation
103 VT ap1
, bp1
, cp1
; // simplex support points, needed for closest points calculation
104 // centroid is centroid, use that fact
105 auto d
= body1
.centroid
-body0
.centroid
;
106 // check for a zero direction vector
107 if (d
.isZero
) return cast(VT
.Float
)-1; // centroids are the same, not separated
108 //getSupport(a, ap0, ap1, d);
109 mixin(GetSupport
!"a");
111 mixin(GetSupport
!"b");
112 d
= segClosestToOrigin(b
, a
);
113 version(vm2d_debug_count_iterations
) gjkIterationCount
= 0;
114 foreach (immutable iter
; 0..32) {
115 if (d
.lengthSquared
<= EPSILON
!(VT
.Float
)*EPSILON
!(VT
.Float
)) return cast(VT
.Float
)-1; // if the closest point is the origin, not separated
116 version(vm2d_debug_count_iterations
) ++gjkIterationCount
;
118 mixin(GetSupport
!"c");
119 // is simplex triangle contains origin?
120 immutable sa
= a
.cross(b
);
121 if (sa
*b
.cross(c
) > 0 && sa
*c
.cross(a
) > 0) return cast(VT
.Float
)-1; // yes, not separated
122 if (c
.dot(d
)-a
.dot(d
) < EPSILON
!(VT
.Float
)*EPSILON
!(VT
.Float
)) break; // new point is not far enough, we found her!
123 auto p0
= segClosestToOrigin(a
, c
);
124 auto p1
= segClosestToOrigin(c
, b
);
125 immutable p0sqlen
= p0
.lengthSquared
;
126 immutable p1sqlen
= p1
.lengthSquared
;
127 if (p0sqlen
<= EPSILON
!(VT
.Float
)*EPSILON
!(VT
.Float
) || p1sqlen
<= EPSILON
!(VT
.Float
)*EPSILON
!(VT
.Float
)) {
128 // origin is very close, but not exactly on edge; assume zero distance (special case)
129 if (sepnorm
!is null) *sepnorm
= d
.normalized
;
130 return cast(VT
.Float
)0;
132 if (p0sqlen
< p1sqlen
) { b
= c
; bp0
= cp0
; bp1
= cp1
; d
= p0
; } else { a
= c
; ap0
= cp0
; ap1
= cp1
; d
= p1
; }
134 // either out of iterations, or new point was not far enough
136 auto dist
= -c
.dot(d
);
137 // get closest points
138 if (op0
!is null || op1
!is null) {
141 if (op0
!is null) *op0
= ap0
;
142 if (op1
!is null) *op1
= ap1
;
144 immutable ll
= l
.dot(l
);
145 immutable l2
= -l
.dot(a
)/ll
;
146 immutable l1
= cast(VT
.Float
)1-l2
;
148 if (op0
!is null) *op0
= bp0
;
149 if (op1
!is null) *op1
= bp1
;
151 if (op0
!is null) *op0
= ap0
;
152 if (op1
!is null) *op1
= ap1
;
154 if (op0
!is null) *op0
= ap0
*l1
+bp0
*l2
;
155 if (op1
!is null) *op1
= ap1
*l1
+bp1
*l2
;
159 if (dist
< 0) { d
= -d
; dist
= -dist
; }
160 if (sepnorm
!is null) *sepnorm
= d
;
165 // ////////////////////////////////////////////////////////////////////////// //
166 // return the Minkowski sum point (ok, something *like* it, but not Minkowski difference yet ;-)
167 private VT
getSupportPoint(CT
, VT
) (ref CT body0
, ref CT body1
, in ref VT sdir
) {
168 pragma(inline
, true);
169 return body0
.support(sdir
)-body1
.support(-sdir
);
173 // check if simplex contains origin, update sdir, and possibly update simplex
174 private bool checkSimplex(VT
) (ref VT sdir
, VT
[] spx
) {
175 assert(spx
.length
== 2 || spx
.length
== 3);
176 if (spx
.length
== 3) {
177 // simplex has 3 elements
178 auto a
= spx
.ptr
[0]; // last added point
179 auto ao
= -a
; // to origin
181 auto ab
= spx
.ptr
[1]-a
;
182 auto ac
= spx
.ptr
[2]-a
;
183 // get the edge normals
184 auto abn
= ac
.tripleProduct(ab
, ab
);
185 auto acn
= ab
.tripleProduct(ac
, ac
);
186 // see where the origin is at
187 auto acloc
= acn
.dot(ao
);
189 // remove middle element
190 spx
.ptr
[1] = spx
.ptr
[0];
193 auto abloc
= abn
.dot(ao
);
194 if (abloc
< 0) return true; // intersection
195 // remove last element
196 spx
.ptr
[2] = spx
.ptr
[1];
197 spx
.ptr
[1] = spx
.ptr
[0];
201 // simplex has 2 elements
202 auto a
= spx
.ptr
[0]; // last added point
203 auto ao
= -a
; // to origin
204 auto ab
= spx
.ptr
[1]-a
;
205 sdir
= ab
.tripleProduct(ao
, ab
);
206 if (sdir
.lengthSquared
<= EPSILON
!(VT
.Float
)*EPSILON
!(VT
.Float
)) sdir
= sdir
.rperp
; // bad direction, use any normal
212 // ////////////////////////////////////////////////////////////////////////// //
213 // Expanding Polytope Algorithm
214 // find minimum translation vector to resolve collision
215 // using the final simplex obtained with the GJK algorithm
216 private VT
EPA(CT
, VT
) (ref CT body0
, ref CT body1
, const(VT
)[] spx
...) {
217 enum MaxIterations
= 128;
218 enum MaxFaces
= MaxIterations
*3;
219 assert(MaxIterations
>= body0
.length
*body1
.length
);
221 static struct SxEdge
{
225 usize nextFree
; // will be used to store my temp index too
227 @disable this (this);
230 void calcNormDist (int winding
) {
231 pragma(inline
, true);
232 mixin(ImportCoreMath
!(VT
.Float
, "fabs"));
234 if (winding
< 0) normal
= normal
.perp
; else normal
= normal
.rperp
;
236 dist
= fabs(p0
.dot(normal
));
239 void set (in ref VT ap0
, in ref VT ap1
, int winding
) {
242 calcNormDist(winding
);
246 // as this cannot be called recursive, we can use thread-local static here
247 // use binary heap to store faces
248 static SxEdge
[MaxFaces
+2] faces
= void;
249 static usize
[MaxFaces
+2] faceMap
= void;
251 usize freeFaceIdx
= usize
.max
;
256 void heapify (usize root
) {
258 auto smallest
= 2*root
+1; // left child
259 if (smallest
>= faceCount
) break; // anyway
260 immutable right
= smallest
+1; // right child
261 if (!(faces
.ptr
[faceMap
.ptr
[smallest
]].dist
< faces
.ptr
[faceMap
.ptr
[root
]].dist
)) smallest
= root
;
262 if (right
< faceCount
&& faces
.ptr
[faceMap
.ptr
[right
]].dist
< faces
.ptr
[faceMap
.ptr
[smallest
]].dist
) smallest
= right
;
263 if (smallest
== root
) break;
265 auto tmp
= faceMap
.ptr
[root
];
266 faceMap
.ptr
[root
] = faceMap
.ptr
[smallest
];
267 faceMap
.ptr
[smallest
] = tmp
;
272 void insertFace (in ref VT p0
, in ref VT p1
) {
273 if (faceCount
== faces
.length
) assert(0, "too many elements in heap");
275 usize ffidx
= freeFaceIdx
; // allocated face index in `faces[]`
276 if (ffidx
!= usize
.max
) {
277 // had free face in free list, fix free list
278 freeFaceIdx
= faces
.ptr
[ffidx
].nextFree
;
280 // no free faces, use next unallocated
283 assert(ffidx
< faces
.length
);
285 faces
.ptr
[ffidx
].set(p0
, p1
, winding
);
286 immutable nfdist
= faces
.ptr
[ffidx
].dist
;
287 // fix heap, and find place for new face
289 auto par
= (i
-1)/2; // parent
290 if (!(nfdist
< faces
.ptr
[faceMap
.ptr
[par
]].dist
)) break;
291 faceMap
.ptr
[i
] = faceMap
.ptr
[par
];
294 faceMap
.ptr
[i
] = ffidx
;
297 // remove face from heap, but don't add it to free list yet
298 SxEdge
* popSmallestFace () {
299 assert(faceCount
> 0);
300 usize fidx
= faceMap
.ptr
[0];
301 SxEdge
* res
= faces
.ptr
+fidx
;
302 res
.nextFree
= fidx
; // store face index; it will be used in `freeFace()`
303 // remove from heap (but don't add to free list yet)
304 faceMap
.ptr
[0] = faceMap
.ptr
[--faceCount
];
309 // add face to free list
310 void freeFace (SxEdge
* e
) {
312 auto fidx
= e
.nextFree
;
313 e
.nextFree
= freeFaceIdx
;
317 // compute the winding
319 foreach (const ref v
; spx
[]) {
320 auto cp
= prevv
.cross(v
);
321 if (cp
> 0) { winding
= 1; break; }
322 if (cp
< 0) { winding
= -1; break; }
326 // build the initial edge queue
328 foreach (const ref v
; spx
[]) {
329 insertFace(prevv
, v
);
335 version(vm2d_debug_count_iterations
) epaIterationCount
= 0;
336 foreach (immutable i
; 0..body0
.length
*body1
.length
) {
337 version(vm2d_debug_count_iterations
) ++epaIterationCount
;
338 e
= popSmallestFace();
339 p
= getSupportPoint(body0
, body1
, e
.normal
);
340 immutable proj
= p
.dot(e
.normal
);
341 if (proj
-e
.dist
< EPSILON
!(VT
.Float
)) return e
.normal
*proj
;
346 assert(e
!is null); // just in case
347 version(vm2d_debug_gjk_warnings
) { import core
.stdc
.stdio
; stderr
.fprintf("EPA: out of iterations!\n"); }
348 return e
.normal
*p
.dot(e
.normal
);
352 // ////////////////////////////////////////////////////////////////////////// //
353 // see Gino van den Bergen's "Ray Casting against General Convex Objects with Application to Continuous Collision Detection" paper
354 // http://www.dtecta.com/papers/jgt04raycast.pdf
356 // ////////////////////////////////////////////////////////////////////////// //
357 public static struct Raycast(VT
) {
358 VT p
= VT
.Invalid
, n
= VT
.Invalid
; // point and normal
360 @property bool valid () const nothrow @safe @nogc { pragma(inline
, true); return n
.isFinite
; }
364 // ////////////////////////////////////////////////////////////////////////// //
365 // simplex for gjk raycaster
366 private struct RCSimplex(VT
) {
367 VT start
; // ray start (not modified by solver)
368 VT r
; // ray direction (not modified by solver)
369 VT
.Float maxlen
= 0; // maximum ray length (not modified by solver)
370 VT x
; // current closest point on the ray
371 VT v
; // current direction
372 VT a
= VT
.Invalid
, b
= VT
.Invalid
; // simplex points
373 VT
.Float distsq
= VT
.Float
.infinity
;
374 VT p
; // current support point (in world space)
375 VT n
; // normal at the hit point
376 VT
.Float lambda
= 0; // current distance
379 // astart: start point
380 // adir: velocity, not normalized
381 // apoint: first point for simplex, arbitrary point from a body we're checking
382 // returns `false` if velocity is too small to get something sane
383 bool setup() (in auto ref VT astart
, in auto ref VT adir
, in auto ref VT apoint
) {
388 maxlen
= r
.normalizeRetLength
;
392 // return `false` if we can stop iterating due to "solution not found" condition
393 // set `p` to new support point before calling this
395 immutable VT w
= x
-p
;
396 version(vm2d_debug_save_minkowski_points
)mink
~= w
;
397 immutable VT
.Float dvw
= v
.dot(w
);
399 immutable VT
.Float dvr
= v
.dot(r
);
400 if (dvr
>= -(EPSILON
!(VT
.Float
)*EPSILON
!(VT
.Float
))) return false;
402 if (lambda
> maxlen
) return false;
403 x
= start
+r
*lambda
; // shorten ray
413 immutable VT p1
= x
.projectToSeg(a
, p
);
414 immutable VT p2
= x
.projectToSeg(p
, b
);
415 if (p1
.distanceSquared(x
) < p2
.distanceSquared(x
)) {
417 distsq
= p1
.distanceSquared(x
);
420 distsq
= p2
.distanceSquared(x
);
422 immutable VT ab
= b
-a
;
423 immutable VT ax
= x
-a
;
424 v
= ab
.tripleProduct(ax
, ab
);
427 immutable VT ab
= b
-a
;
428 immutable VT ax
= x
-a
;
429 v
= ab
.tripleProduct(ax
, ab
);
438 // returns `false` if a solution wasn't found, or modify `res` if a solution was found
439 bool solve(int maxiters
=32, double distEps
=0.0001) (ref Raycast
!VT res
, scope VT
delegate (in ref VT sdir
) getSupportPoint
) {
440 version(vm2d_debug_save_minkowski_points
)mink
= null;
441 version(vm2d_debug_count_iterations
) gjkIterationCount
= 0;
442 foreach (immutable itnum
; 0..maxiters
) {
443 version(vm2d_debug_count_iterations
) ++gjkIterationCount
;
444 p
= getSupportPoint(v
);
445 if (!advance()) return false;
447 if (distsq
<= cast(VT
.Float
)distEps
) {
450 res
.n
= n
.normalized
;
460 // ////////////////////////////////////////////////////////////////////////// //
461 public Raycast
!VT
gjkraycast(bool checkRayStart
=true, int maxiters
=32, double distEps
=0.0001, VT
, CT
) (auto ref CT abody
, in auto ref VT rayO
, in auto ref VT rayD
) if (IsGoodVertexStorage
!(CT
, VT
)) {
463 RCSimplex
!VT simplex
;
465 static if (checkRayStart
) {
466 if (abody
.inside(rayO
)) return res
; // the start point is inside the body, oops
469 if (!simplex
.setup(rayO
, rayD
, abody
.centroid
)) {
470 // velocity is zero, use infinite length
471 simplex
.maxlen
= VT
.Float
.infinity
;
473 if (!simplex
.solve
!(maxiters
, distEps
)(res
, (in ref sdir
) => abody
.support(sdir
))) return res
;
479 // ////////////////////////////////////////////////////////////////////////// //
480 // this inflates *bbody*, and traces ray from *abody* origin to *bbody*. it is IMPORTANT! ;-)
481 // if result is valid, body a can move by `res.p-abody.origin` before hit
482 public Raycast
!VT
gjksweep(bool checkRayStart
=true, int maxiters
=32, double distEps
=0.0001, VT
, CT
) (auto ref CT abody
, auto ref CT bbody
, in auto ref VT lvelA
, in auto ref VT lvelB
) {
484 RCSimplex
!VT simplex
;
486 immutable VT act
= abody
.centroid
;
487 immutable VT bct
= bbody
.centroid
;
489 if (act
.equals(bct
)) return res
; // obviously collided
490 static if (checkRayStart
) {
491 if (bbody
.inside(act
)) return res
; // the start point is inside the destination body, oops
494 // trace from abody to bbody, with relative motion (lvelA-lvelB)
495 if (!simplex
.setup(act
, lvelA
-lvelB
, bct
)) return res
; // velocity is zero, so oops
496 if (!simplex
.solve
!(maxiters
, distEps
)(res
, (in ref sdir
) => bbody
.support(sdir
)-(abody
.support(-sdir
)-act
))) return res
;