1 /**
2   This module implements some abstract geometric shapes:
3   $(UL
4   $(LI Line segments.)
5   $(LI Triangle.)
6   $(LI Circles/spheres.)
7   $(LI Rays)
8   $(LI Planes)
9   $(LI Frustum)
10   )
11  */
12 module gfm.math.shapes;
13 
14 import std.math,
15        std.traits;
16 
17 import gfm.math.vector,
18        gfm.math.box;
19 
20 /// A Segment is 2 points.
21 /// When considered like a vector, it represents the arrow from a to b.
22 struct Segment(T, int N)
23 {
24     public
25     {
26         alias Vector!(T, N) point_t;
27         point_t a, b;
28 
29         static if (N == 3 && isFloatingPoint!T)
30         {
31             /// Segment vs plane intersection.
32             /// See_also: "Geometry for Computer Graphics: Formulae, Examples
33             ///     and Proofs", Vince (2005), p. 62
34             /// Returns: $(D true) if the segment crosses the plane exactly
35             ///     once, or $(D false) if the segment is parallel to or does
36             ///     not reach the plane
37             /// Params:
38             ///     plane = The plane to intersect.
39             ///     intersection = Point of intersection.
40             ///     progress = Set to the point's progress between endpoints
41             ///                $(D a) at 0.0 and $(D b) at 1.0, or T.infinity
42             ///                if parallel to the plane
43             @nogc bool intersect(Plane!T plane, out point_t intersection, out T progress) pure const nothrow
44             {
45                 // direction vector from a to b
46                 point_t dir = b-a;
47 
48                 // dot product will be 0 if angle to plane is 0
49                 T dp = dot(plane.n, dir);
50                 if (abs(dp) < T.epsilon)
51                 {
52                     progress = T.infinity;
53                     return false; // parallel to plane
54                 }
55 
56                 // relative distance along segment where intersection happens
57                 progress = -(dot(plane.n, a) - plane.d) / dp;
58 
59                 intersection = progress*dir + a;
60                 return progress >= 0 && progress <= 1;
61             }
62         }
63     }
64 }
65 
66 alias Segment!(float, 2) seg2f;  /// 2D float segment.
67 alias Segment!(float, 3) seg3f;  /// 3D float segment.
68 alias Segment!(double, 2) seg2d; /// 2D double segment.
69 alias Segment!(double, 3) seg3d; /// 3D double segment.
70 alias Segment!(int, 2) seg2i;    /// 2D integer segment.
71 alias Segment!(int, 3) seg3i;    /// 3D integer segment.
72 
73 /// A Triangle is 3 points.
74 struct Triangle(T, int N)
75 {
76     public
77     {
78         alias Vector!(T, N) point_t;
79         point_t a, b, c;
80 
81         static if (N == 2)
82         {
83             /// Returns: Area of a 2D triangle.
84             @nogc T area() pure const nothrow
85             {
86                 return abs(signedArea());
87             }
88 
89             /// Returns: Signed area of a 2D triangle.
90             @nogc T signedArea() pure const nothrow
91             {
92                 return ((b.x * a.y - a.x * b.y)
93                       + (c.x * b.y - b.x * c.y)
94                       + (a.x * c.y - c.x * a.y)) / 2;
95             }
96         }
97 
98         static if (N == 3 && isFloatingPoint!T)
99         {
100             /// Returns: Triangle normal.
101             @nogc Vector!(T, 3) computeNormal() pure const nothrow
102             {
103                 return cross(b - a, c - a).normalized();
104             }
105         }
106     }
107 }
108 
109 alias Triangle!(float, 2) triangle2f;  /// 2D float triangle.
110 alias Triangle!(float, 3) triangle3f;  /// 3D float triangle.
111 alias Triangle!(double, 2) triangle2d; /// 2D double triangle.
112 alias Triangle!(double, 3) triangle3d; /// 3D double triangle.
113 
114 /// A Sphere is a point + a radius.
115 struct Sphere(T, int N)
116 {
117     public nothrow
118     {
119         alias Vector!(T, N) point_t;
120         point_t center;
121         T radius;
122 
123         /// Creates a sphere from a point and a radius.
124         @nogc this(in point_t center_, T radius_) pure nothrow
125         {
126             center = center_;
127             radius = radius_;
128         }
129 
130         /// Sphere contains point test.
131         /// Returns: true if the point is inside the sphere.
132         @nogc bool contains(in Sphere s) pure const nothrow
133         {
134             if (s.radius > radius)
135                 return false;
136 
137             T innerRadius = radius - s.radius;
138             return squaredDistanceTo(s.center) < innerRadius * innerRadius;
139         }
140 
141         /// Sphere vs point Euclidean distance squared.
142         @nogc T squaredDistanceTo(point_t p) pure const nothrow
143         {
144             return center.squaredDistanceTo(p);
145         }
146 
147         /// Sphere vs sphere intersection.
148         /// Returns: true if the spheres intersect.
149         @nogc bool intersects(Sphere s) pure const nothrow
150         {
151             T outerRadius = radius + s.radius;
152             return squaredDistanceTo(s.center) < outerRadius * outerRadius;
153         }
154 
155         static if (isFloatingPoint!T)
156         {
157             /// Sphere vs point Euclidean distance.
158             @nogc T distanceTo(point_t p) pure const nothrow
159             {
160                 return center.distanceTo(p);
161             }
162 
163             static if(N == 2)
164             {
165                 /// Returns: Circle area.
166                 @nogc T area() pure const nothrow
167                 {
168                     return T(PI) * (radius * radius);
169                 }
170             }
171         }
172     }
173 }
174 
175 alias Sphere!(float, 2) sphere2f;  /// 2D float sphere (ie. a circle).
176 alias Sphere!(float, 3) sphere3f;  /// 3D float sphere.
177 alias Sphere!(double, 2) sphere2d; /// 2D double sphere (ie. a circle).
178 alias Sphere!(double, 3) sphere3d; /// 3D double sphere (ie. a circle).
179 
180 
181 /// A Ray ir a point + a direction.
182 struct Ray(T, int N)
183 {
184 nothrow:
185     public
186     {
187         alias Vector!(T, N) point_t;
188         point_t orig;
189         point_t dir;
190 
191         /// Returns: A point further along the ray direction.
192         @nogc point_t progress(T t) pure const nothrow
193         {
194             return orig + dir * t;
195         }
196 
197         static if (N == 3 && isFloatingPoint!T)
198         {
199             /// Ray vs triangle intersection.
200             /// See_also: "Fast, Minimum Storage Ray/Triangle intersection", Mommer & Trumbore (1997)
201             /// Returns: Barycentric coordinates, the intersection point is at $(D (1 - u - v) * A + u * B + v * C).
202             @nogc bool intersect(Triangle!(T, 3) triangle, out T t, out T u, out T v) pure const nothrow
203             {
204                 point_t edge1 = triangle.b - triangle.a;
205                 point_t edge2 = triangle.c - triangle.a;
206                 point_t pvec = cross(dir, edge2);
207                 T det = dot(edge1, pvec);
208                 if (abs(det) < T.epsilon)
209                     return false; // no intersection
210                 T invDet = 1 / det;
211 
212                 // calculate distance from triangle.a to ray origin
213                 point_t tvec = orig - triangle.a;
214 
215                 // calculate U parameter and test bounds
216                 u = dot(tvec, pvec) * invDet;
217                 if (u < 0 || u > 1)
218                     return false;
219 
220                 // prepare to test V parameter
221                 point_t qvec = cross(tvec, edge1);
222 
223                 // calculate V parameter and test bounds
224                 v = dot(dir, qvec) * invDet;
225                 if (v < 0.0 || u + v > 1.0)
226                     return false;
227 
228                 // calculate t, ray intersects triangle
229                 t = dot(edge2, qvec) * invDet;
230                 return true;
231             }
232 
233             /// Ray vs plane intersection.
234             /// See_also: "Geometry for Computer Graphics: Formulae, Examples
235             ///     and Proofs", Vince (2005), p. 62
236             /// Returns: $(D true) if the ray crosses the plane exactly once,
237             ///     or $(D false) if the ray is parallel to or points away from
238             ///     the plane
239             /// Params:
240             ///     plane = Plane to intersect.
241             ///     intersection = Point of intersection.
242             ///     distance = set to the point's distance along the ray, or
243             ///                T.infinity if parallel to the plane
244             @nogc bool intersect(Plane!T plane, out point_t intersection, out T distance) pure const nothrow
245             {
246                 // dot product will be 0 if angle to plane is 0
247                 T dp = dot(plane.n, dir);
248                 if (abs(dp) < T.epsilon)
249                 {
250                     distance = T.infinity;
251                     return false; // parallel to plane
252                 }
253 
254                 // distance along ray where intersection happens
255                 distance = -(dot(plane.n, orig) - plane.d) / dp;
256 
257                 intersection = distance*dir + orig;
258                 return distance >= 0;
259             }
260         }
261     }
262 }
263 
264 alias Ray!(float, 2) ray2f;  /// 2D float ray.
265 alias Ray!(float, 3) ray3f;  /// 3D float ray.
266 alias Ray!(double, 2) ray2d; /// 2D double ray.
267 alias Ray!(double, 3) ray3d; /// 3D double ray.
268 
269 
270 /// 3D plane.
271 /// See_also: Flipcode article by Nate Miller $(WEB www.flipcode.com/archives/Plane_Class.shtml).
272 struct Plane(T) if (isFloatingPoint!T)
273 {
274     public
275     {
276         vec3!T n; /// Normal (always stored normalized).
277         T d;
278 
279         /// Create from four coordinates.
280         @nogc this(vec4!T abcd) pure nothrow
281         {
282             n = vec3!T(abcd.x, abcd.y, abcd.z).normalized();
283             d = abcd.w;
284         }
285 
286         /// Create from a point and a normal.
287         @nogc this(vec3!T origin, vec3!T normal) pure nothrow
288         {
289             n = normal.normalized();
290             d = -dot(origin, n);
291         }
292 
293         /// Create from 3 non-aligned points.
294         @nogc this(vec3!T A, vec3!T B, vec3!T C) pure nothrow
295         {
296             this(C, cross(B - A, C - A));
297         }
298 
299         /// Assign a plane with another plane.
300         @nogc ref Plane opAssign(Plane other) pure nothrow
301         {
302             n = other.n;
303             d = other.d;
304             return this;
305         }
306 
307         /// Returns: signed distance between a point and the plane.
308         @nogc T signedDistanceTo(vec3!T point) pure const nothrow
309         {
310             return dot(n, point) + d;
311         }
312 
313         /// Returns: absolute distance between a point and the plane.
314         @nogc T distanceTo(vec3!T point) pure const nothrow
315         {
316             return abs(signedDistanceTo(point));
317         }
318 
319         /// Returns: true if the point is in front of the plane.
320         @nogc bool isFront(vec3!T point) pure const nothrow
321         {
322             return signedDistanceTo(point) >= 0;
323         }
324 
325         /// Returns: true if the point is in the back of the plane.
326         @nogc bool isBack(vec3!T point) pure const nothrow
327         {
328             return signedDistanceTo(point) < 0;
329         }
330 
331         /// Returns: true if the point is on the plane, with a given epsilon.
332         @nogc bool isOn(vec3!T point, T epsilon) pure const nothrow
333         {
334             T sd = signedDistanceTo(point);
335             return (-epsilon < sd) && (sd < epsilon);
336         }
337     }
338 }
339 
340 alias Plane!float planef;  /// 3D float plane.
341 alias Plane!double planed; /// 3D double plane.
342 
343 unittest
344 {
345     auto p = planed(vec4d(1.0, 2.0, 3.0, 4.0));
346     auto p2 = planed(vec3d(1.0, 0.0, 0.0),
347                      vec3d(0.0, 1.0, 0.0),
348                      vec3d(0.0, 0.0, 1.0));
349 
350     assert(p2.isOn(vec3d(1.0, 0.0, 0.0), 1e-7));
351     assert(p2.isFront(vec3d(1.0, 1.0, 1.0)));
352     assert(p2.isBack(vec3d(0.0, 0.0, 0.0)));
353 }
354 
355 /// Plane intersection tests
356 @nogc pure nothrow unittest
357 {
358     void testR(planed p, ray3d r, bool shouldIntersect, double expectedDistance, vec3d expectedPoint = vec3d.init) pure nothrow @nogc
359     {
360         vec3d point;
361         double distance;
362         assert(r.intersect(p, point, distance) == shouldIntersect);
363         assert(approxEqual(distance, expectedDistance));
364         if (shouldIntersect)
365             assert(approxEqual(point.v[], expectedPoint.v[]));
366     }
367     // ray facing plane
368     testR(planed(vec4d(1.0, 0.0, 0.0, 1.0)), ray3d(vec3d(2.0, 3.0, 4.0), vec3d(-1.0, 0.0, 0.0)),
369         true, 1.0, vec3d(1.0, 3.0, 4.0));
370     testR(planed(vec4d(1.0, 1.0, 1.0, -sqrt(3.0))), ray3d(vec3d(1.0, 1.0, 1.0), vec3d(-1.0, -1.0, -1.0).normalized()),
371         true, 2.0*sqrt(3.0), vec3d(-1.0, -1.0, -1.0));
372     // ray facing away
373     testR(planed(vec4d(1.0, 0.0, 0.0, 1.0)), ray3d(vec3d(2.0, 3.0, 4.0), vec3d(1.0, 0.0, 0.0)),
374         false, -1.0);
375     testR(planed(vec4d(1.0, 0.0, 0.0, 5.0)), ray3d(vec3d(2.0, 3.0, 4.0), vec3d(-1.0, 0.0, 0.0)),
376         false, -3.0);
377     // ray parallel
378     testR(planed(vec4d(0.0, 0.0, 1.0, 1.0)), ray3d(vec3d(1.0, 2.0, 3.0), vec3d(1.0, 0.0, 0.0)),
379         false, double.infinity);
380 
381     void testS(planed p, seg3d s, bool shouldIntersect, double expectedProgress, vec3d expectedPoint = vec3d.init) pure nothrow @nogc
382     {
383         vec3d point;
384         double progress;
385         assert(s.intersect(p, point, progress) == shouldIntersect);
386         assert(approxEqual(progress, expectedProgress));
387         if (shouldIntersect)
388             assert(approxEqual(point.v[], expectedPoint.v[]));
389     }
390     // segment crossing plane
391     testS(planed(vec4d(1.0, 0.0, 0.0, 2.0)), seg3d(vec3d(1.0, 2.0, 3.0), vec3d(3.0, 4.0, 5.0)),
392         true, 0.5, vec3d(2.0, 3.0, 4.0));
393     // segment too short
394     testS(planed(vec4d(1.0, 0.0, 0.0, 0.0)), seg3d(vec3d(1.0, 2.0, 3.0), vec3d(3.0, 4.0, 5.0)),
395         false, -0.5);
396 }
397 
398 
399 /// 3D frustum.
400 /// See_also: Flipcode article by Dion Picco $(WEB www.flipcode.com/archives/Frustum_Culling.shtml).
401 /// Bugs: verify proper signedness of half-spaces
402 struct Frustum(T) if (isFloatingPoint!T)
403 {
404     public
405     {
406         enum int LEFT   = 0,
407                  RIGHT  = 1,
408                  TOP    = 2,
409                  BOTTOM = 3,
410                  NEAR   = 4,
411                  FAR    = 5;
412 
413         Plane!T[6] planes;
414 
415         /// Create a frustum from 6 planes.
416         @nogc this(Plane!T left, Plane!T right, Plane!T top, Plane!T bottom, Plane!T near, Plane!T far) pure nothrow
417         {
418             planes[LEFT] = left;
419             planes[RIGHT] = right;
420             planes[TOP] = top;
421             planes[BOTTOM] = bottom;
422             planes[NEAR] = near;
423             planes[FAR] = far;
424         }
425 
426         enum : int
427         {
428             OUTSIDE,   /// object is outside the frustum
429             INTERSECT, /// object intersects with the frustum
430             INSIDE     /// object is inside the frustum
431         }
432 
433         /// Point vs frustum intersection.
434         @nogc bool contains(vec3!T point) pure const nothrow
435         {
436             for(int i = 0; i < 6; ++i)
437             {
438                 T distance = planes[i].signedDistanceTo(point);
439 
440                 if(distance < 0)
441                     return false;
442             }
443             return true;
444         }
445 
446         /// Sphere vs frustum intersection.
447         /// Returns: Frustum.OUTSIDE, Frustum.INTERSECT or Frustum.INSIDE.
448         @nogc int contains(Sphere!(T, 3) sphere) pure const nothrow
449         {
450             // calculate our distances to each of the planes
451             for(int i = 0; i < 6; ++i)
452             {
453                 // find the distance to this plane
454                 T distance = planes[i].signedDistanceTo(sphere.center);
455 
456                 if(distance < -sphere.radius)
457                     return OUTSIDE;
458 
459                 else if (distance < sphere.radius)
460                     return INTERSECT;
461             }
462 
463             // otherwise we are fully in view
464             return INSIDE;
465         }
466 
467         /// AABB vs frustum intersection.
468         /// Returns: Frustum.OUTSIDE, Frustum.INTERSECT or Frustum.INSIDE.
469         @nogc int contains(box3!T box) pure const nothrow
470         {
471             vec3!T[8] corners;
472             int totalIn = 0;
473 
474             for (int i = 0; i < 2; ++i)
475                 for (int j = 0; j < 2; ++j)
476                     for (int k = 0; k < 2; ++k)
477                     {
478                         auto x = i == 0 ? box.min.x : box.max.x;
479                         auto y = j == 0 ? box.min.y : box.max.y;
480                         auto z = k == 0 ? box.min.z : box.max.z;
481                         corners[i*4 + j*2 + k] = vec3!T(x, y, z);
482                     }
483 
484             // test all 8 corners against the 6 sides
485             // if all points are behind 1 specific plane, we are out
486             // if we are in with all points, then we are fully in
487             for(int p = 0; p < 6; ++p)
488             {
489                 int inCount = 8;
490                 int ptIn = 1;
491 
492                 for(int i = 0; i < 8; ++i)
493                 {
494                     // test this point against the planes
495                     if (planes[p].isBack(corners[i]))
496                     {
497                         ptIn = 0;
498                         --inCount;
499                     }
500                 }
501 
502                 // were all the points outside of plane p?
503                 if (inCount == 0)
504                     return OUTSIDE;
505 
506                 // check if they were all on the right side of the plane
507                 totalIn += ptIn;
508             }
509 
510             // so if totalIn is 6, then all are inside the view
511             if(totalIn == 6)
512                 return INSIDE;
513 
514             // we must be partly in then otherwise
515             return INTERSECT;
516         }
517 
518     }
519 }
520 
521 unittest
522 {
523     seg2f se;
524     triangle3f tr;
525     Frustum!double frust;
526     planed pl;
527 }
528 
529 /// True if `T` is a kind of Segment
530 enum isSegment(T) = is(T : Segment!U, U...);
531 
532 /// True if `T` is a kind of Triangle
533 enum isTriangle(T) = is(T : Triangle!U, U...);
534 
535 /// True if `T` is a kind of Sphere
536 enum isSphere(T) = is(T : Sphere!U, U...);
537 
538 /// True if `T` is a kind of Ray
539 enum isRay(T) = is(T : Ray!U, U...);
540 
541 /// True if `T` is a kind of Plane
542 enum isPlane(T) = is(T : Plane!U, U);
543 
544 /// True if `T` is a kind of Frustum
545 enum isFrustum(T) = is(T : Frustum!U, U);
546 
547 /// True if `T` is a kind of 2 dimensional Segment
548 enum isSegment2D(T) = is(T : Segment!(U, 2), U);
549 
550 /// True if `T` is a kind of 2 dimensional Triangle
551 enum isTriangle2D(T) = is(T : Triangle!(U, 2), U);
552 
553 /// True if `T` is a kind of 2 dimensional Sphere
554 enum isSphere2D(T) = is(T : Sphere!(U, 2), U);
555 
556 /// True if `T` is a kind of 2 dimensional Ray
557 enum isRay2D(T) = is(T : Ray!(U, 2), U);
558 
559 /// True if `T` is a kind of 3 dimensional Segment
560 enum isSegment3D(T) = is(T : Segment!(U, 3), U);
561 
562 /// True if `T` is a kind of 3 dimensional Triangle
563 enum isTriangle3D(T) = is(T : Triangle!(U, 3), U);
564 
565 /// True if `T` is a kind of 3 dimensional Sphere
566 enum isSphere3D(T) = is(T : Sphere!(U, 3), U);
567 
568 /// True if `T` is a kind of 3 dimensional Ray
569 enum isRay3D(T) = is(T : Ray!(U, 3), U);
570 
571 unittest
572 {
573     enum ShapeType
574     {
575         segment,
576         triangle,
577         sphere,
578         ray,
579         plane,
580         frustum
581     }
582 
583     void test(T, ShapeType type, int dim)()
584     {
585         with (ShapeType)
586         {
587             static assert(isSegment!T  == (type == segment ));
588             static assert(isTriangle!T == (type == triangle));
589             static assert(isSphere!T   == (type == sphere  ));
590             static assert(isRay!T      == (type == ray     ));
591             static assert(isPlane!T    == (type == plane   ));
592             static assert(isFrustum!T  == (type == frustum ));
593 
594             static assert(isSegment2D!T  == (type == segment  && dim == 2));
595             static assert(isTriangle2D!T == (type == triangle && dim == 2));
596             static assert(isSphere2D!T   == (type == sphere   && dim == 2));
597             static assert(isRay2D!T      == (type == ray      && dim == 2));
598 
599             static assert(isSegment3D!T  == (type == segment  && dim == 3));
600             static assert(isTriangle3D!T == (type == triangle && dim == 3));
601             static assert(isSphere3D!T   == (type == sphere   && dim == 3));
602             static assert(isRay3D!T      == (type == ray      && dim == 3));
603         }
604     }
605 
606     with (ShapeType)
607     {
608         //    test case         type      #dimensions
609         test!(seg2f           , segment , 2);
610         test!(seg3d           , segment , 3);
611         test!(triangle2d      , triangle, 2);
612         test!(triangle3f      , triangle, 3);
613         test!(sphere2d        , sphere  , 2);
614         test!(Sphere!(uint, 3), sphere  , 3);
615         test!(ray2f           , ray     , 2);
616         test!(Ray!(real, 3)   , ray     , 3);
617         test!(planed          , plane   , 0); // ignore dimension (always 3D)
618         test!(Plane!float     , plane   , 0);
619         test!(Frustum!double  , frustum , 0);
620     }
621 }
622 
623 /// Get the numeric type used to measure a shape's dimensions.
624 alias DimensionType(T : Segment!U, U...) = U[0];
625 /// ditto
626 alias DimensionType(T : Triangle!U, U...) = U[0];
627 /// ditto
628 alias DimensionType(T : Sphere!U, U...) = U[0];
629 /// ditto
630 alias DimensionType(T : Ray!U, U...) = U[0];
631 /// ditto
632 alias DimensionType(T : Plane!U, U) = U;
633 /// ditto
634 alias DimensionType(T : Frustum!U, U) = U;
635 
636 ///
637 unittest
638 {
639     static assert(is(DimensionType!seg2i          == int));
640     static assert(is(DimensionType!triangle3d     == double));
641     static assert(is(DimensionType!sphere2d       == double));
642     static assert(is(DimensionType!ray3f          == float));
643     static assert(is(DimensionType!planed         == double));
644     static assert(is(DimensionType!(Frustum!real) == real));
645 }