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 static if( __VERSION__ < 2066 ) private enum nogc = 1;
21 
22 /// A Segment is 2 points.
23 /// When considered like a vector, it represents the arrow from a to b.
24 align(1) struct Segment(T, int N)
25 {
26     align(1):
27     public
28     {
29         alias Vector!(T, N) point_t;
30         point_t a, b;
31     }
32 }
33 
34 alias Segment!(float, 2) seg2f;  /// 2D float segment.
35 alias Segment!(float, 3) seg3f;  /// 3D float segment.
36 alias Segment!(double, 2) seg2d; /// 2D double segment.
37 alias Segment!(double, 3) seg3d; /// 3D double segment.
38 alias Segment!(int, 2) seg2i;    /// 2D integer segment.
39 alias Segment!(int, 3) seg3i;    /// 3D integer segment.
40 
41 /// A Triangle is 3 points.
42 align(1) struct Triangle(T, int N)
43 {
44     align(1):
45     public
46     {
47         alias Vector!(T, N) point_t;
48         point_t a, b, c;
49 
50         static if (N == 2)
51         {
52             /// Returns: Area of a 2D triangle.
53             @nogc T area() pure const nothrow
54             {
55                 return abs(signedArea());
56             }
57 
58             /// Returns: Signed area of a 2D triangle.
59             @nogc T signedArea() pure const nothrow
60             {
61                 return ((b.x * a.y - a.x * b.y)
62                       + (c.x * b.y - b.x * c.y)
63                       + (a.x * c.y - c.x * a.y)) / 2;
64             }
65         }
66 
67         static if (N == 3)
68         {
69             /// Returns: Triangle normal.
70             @nogc Vector!(T, 3) computeNormal() pure const nothrow
71             {
72                 return cross(b - a, c - a).normalized();
73             }
74         }
75     }
76 }
77 
78 alias Triangle!(float, 2) triangle2f;  /// 2D float triangle.
79 alias Triangle!(float, 3) triangle3f;  /// 3D float triangle.
80 alias Triangle!(double, 2) triangle2d; /// 2D double triangle.
81 alias Triangle!(double, 3) triangle3d; /// 3D double triangle.
82 
83 /// A Sphere is a point + a radius.
84 align(1) struct Sphere(T, int N)
85 {
86     align(1):
87     public nothrow
88     {
89         alias Vector!(T, N) point_t;
90         point_t center;
91         T radius;
92 
93         /// Creates a sphere from a point and a radius.
94         @nogc this(in point_t center_, T radius_) pure nothrow
95         {
96             center = center_;
97             radius = radius_;
98         }
99 
100         /// Sphere contains point test.
101         /// Returns: true if the point is inside the sphere.
102         @nogc bool contains(in Sphere s) pure const nothrow
103         {
104             if (s.radius > radius)
105                 return false;
106 
107             T innerRadius = radius - s.radius;
108             return squaredDistanceTo(s.center) < innerRadius * innerRadius;
109         }
110 
111         /// Sphere vs point Euclidean distance squared.
112         @nogc T squaredDistanceTo(point_t p) pure const nothrow
113         {
114             return center.squaredDistanceTo(p);
115         }
116 
117         /// Sphere vs sphere intersection.
118         /// Returns: true if the spheres intersect.
119         @nogc bool intersects(Sphere s) pure const nothrow
120         {
121             T outerRadius = radius + s.radius;
122             return squaredDistanceTo(s.center) < outerRadius * outerRadius;
123         }
124 
125         static if (isFloatingPoint!T)
126         {
127             /// Sphere vs point Euclidean distance.
128             @nogc T distanceTo(point_t p) pure const nothrow
129             {
130                 return center.distanceTo(p);
131             }
132 
133             static if(N == 2)
134             {
135                 /// Returns: Circle area.
136                 @nogc T area() pure const nothrow
137                 {
138                     return PI * (radius * radius);
139                 }
140             }
141         }
142     }
143 }
144 
145 alias Sphere!(float, 2) sphere2f;  /// 2D float sphere (ie. a circle).
146 alias Sphere!(float, 3) sphere3f;  /// 3D float sphere.
147 alias Sphere!(double, 2) sphere2d; /// 2D double sphere (ie. a circle).
148 alias Sphere!(double, 3) sphere3d; /// 3D double sphere (ie. a circle).
149 
150 
151 /// A Ray ir a point + a direction.
152 align(1) struct Ray(T, int N)
153 {
154 align(1):
155 nothrow:
156     public
157     {
158         alias Vector!(T, N) point_t;
159         point_t orig;
160         point_t dir;
161 
162         /// Returns: A point further along the ray direction.
163         @nogc point_t progress(T t) pure const nothrow
164         {
165             return orig + dir * t;
166         }
167 
168         static if (N == 3)
169         {
170             /// Ray vs triangle intersection.
171             /// See_also: "Fast, Minimum Storage Ray/Triangle intersection", Mommer & Trumbore (1997)
172             /// Returns: Barycentric coordinates, the intersection point is at $(D (1 - u - v) * A + u * B + v * C).
173             @nogc bool intersect(Triangle!(T, 3) triangle, out T t, out T u, out T v) pure const nothrow
174             {
175                 point_t edge1 = triangle.b - triangle.a;
176                 point_t edge2 = triangle.c - triangle.a;
177                 point_t pvec = cross(dir, edge2);
178                 T det = dot(edge1, pvec);
179                 if (abs(det) < T.epsilon)
180                     return false; // no intersection
181                 T invDet = 1 / det;
182 
183                 // calculate distance from triangle.a to ray origin
184                 point_t tvec = orig - triangle.a;
185 
186                 // calculate U parameter and test bounds
187                 u = dot(tvec, pvec) * invDet;
188                 if (u < 0 || u > 1)
189                     return false;
190 
191                 // prepare to test V parameter
192                 point_t qvec = cross(tvec, edge1);
193 
194                 // calculate V parameter and test bounds
195                 v = dot(dir, qvec) * invDet;
196                 if (v < 0.0 || u + v > 1.0)
197                     return false;
198 
199                 // calculate t, ray intersects triangle
200                 t = dot(edge2, qvec) * invDet;
201                 return true;
202             }
203         }
204     }
205 }
206 
207 alias Ray!(float, 2) ray2f;  /// 2D float ray.
208 alias Ray!(float, 3) ray3f;  /// 3D float ray.
209 alias Ray!(double, 2) ray2d; /// 2D double ray.
210 alias Ray!(double, 3) ray3d; /// 3D double ray.
211 
212 
213 /// 3D plane.
214 /// See_also: Flipcode article by Nate Miller $(WEB www.flipcode.com/archives/Plane_Class.shtml).
215 align(1) struct Plane(T) if (isFloatingPoint!T)
216 {
217     align(1):
218     public
219     {
220         vec3!T n; /// Normal (always stored normalized).
221         T d;
222 
223         /// Create from four coordinates.
224         @nogc this(vec4!T abcd) pure nothrow
225         {
226             n = vec3!T(abcd.x, abcd.y, abcd.z).normalized();
227             d = abcd.z;
228         }
229 
230         /// Create from a point and a normal.
231         @nogc this(vec3!T origin, vec3!T normal) pure nothrow
232         {
233             n = normal.normalized();
234             d = -dot(origin, n);
235         }
236 
237         /// Create from 3 non-aligned points.
238         @nogc this(vec3!T A, vec3!T B, vec3!T C) pure nothrow
239         {
240             this(C, cross(B - A, C - A));
241         }
242 
243         /// Assign a plane with another plane.
244         @nogc ref Plane opAssign(Plane other) pure nothrow
245         {
246             n = other.n;
247             d = other.d;
248             return this;
249         }
250 
251         /// Returns: signed distance between a point and the plane.
252         @nogc T signedDistanceTo(vec3!T point) pure const nothrow
253         {
254             return dot(n, point) + d;
255         }
256 
257         /// Returns: absolute distance between a point and the plane.
258         @nogc T distanceTo(vec3!T point) pure const nothrow
259         {
260             return abs(signedDistanceTo(point));
261         }
262 
263         /// Returns: true if the point is in front of the plane.
264         @nogc bool isFront(vec3!T point) pure const nothrow
265         {
266             return signedDistanceTo(point) >= 0;
267         }
268 
269         /// Returns: true if the point is in the back of the plane.
270         @nogc bool isBack(vec3!T point) pure const nothrow
271         {
272             return signedDistanceTo(point) < 0;
273         }
274 
275         /// Returns: true if the point is on the plane, with a given epsilon.
276         @nogc bool isOn(vec3!T point, T epsilon) pure const nothrow
277         {
278             T sd = signedDistanceTo(point);
279             return (-epsilon < sd) && (sd < epsilon);
280         }
281     }
282 }
283 
284 alias Plane!float planef;  /// 3D float plane.
285 alias Plane!double planed; /// 3D double plane.
286 
287 unittest
288 {
289     auto p = planed(vec4d(1.0, 2.0, 3.0, 4.0));
290     auto p2 = planed(vec3d(1.0, 0.0, 0.0),
291                      vec3d(0.0, 1.0, 0.0),
292                      vec3d(0.0, 0.0, 1.0));
293 
294     assert(p2.isOn(vec3d(1.0, 0.0, 0.0), 1e-7));
295     assert(p2.isFront(vec3d(1.0, 1.0, 1.0)));
296     assert(p2.isBack(vec3d(0.0, 0.0, 0.0)));
297 }
298 
299 
300 
301 /// 3D frustum.
302 /// See_also: Flipcode article by Dion Picco $(WEB www.flipcode.com/archives/Frustum_Culling.shtml).
303 /// Bugs: verify proper signedness of half-spaces
304 align(1) struct Frustum(T) if (isFloatingPoint!T)
305 {
306     align(1):
307     public
308     {
309         enum int LEFT   = 0,
310                  RIGHT  = 1,
311                  TOP    = 2,
312                  BOTTOM = 3,
313                  NEAR   = 4,
314                  FAR    = 5;
315 
316         Plane!T[6] planes;
317 
318         /// Create a frustum from 6 planes.
319         @nogc this(Plane!T left, Plane!T right, Plane!T top, Plane!T bottom, Plane!T near, Plane!T far) pure nothrow
320         {
321             planes[LEFT] = left;
322             planes[RIGHT] = right;
323             planes[TOP] = top;
324             planes[BOTTOM] = bottom;
325             planes[NEAR] = near;
326             planes[FAR] = far;
327         }
328 
329         enum : int
330         {
331             OUTSIDE,   /// object is outside the frustum
332             INTERSECT, /// object intersects with the frustum
333             INSIDE     /// object is inside the frustum
334         }
335 
336         /// Point vs frustum intersection.
337         @nogc bool contains(vec3!T point) pure const nothrow
338         {
339             for(int i = 0; i < 6; ++i)
340             {
341                 T distance = planes[i].signedDistanceTo(point);
342 
343                 if(distance < 0)
344                     return false;
345             }
346             return true;
347         }
348 
349         /// Sphere vs frustum intersection.
350         /// Returns: Frustum.OUTSIDE, Frustum.INTERSECT or Frustum.INSIDE.
351         @nogc int contains(Sphere!(T, 3) sphere) pure const nothrow
352         {
353             // calculate our distances to each of the planes
354             for(int i = 0; i < 6; ++i)
355             {
356                 // find the distance to this plane
357                 T distance = planes[i].signedDistanceTo(sphere.center);
358 
359                 if(distance < -sphere.radius)
360                     return OUTSIDE;
361 
362                 else if (distance < sphere.radius)
363                     return INTERSECT;
364             }
365 
366             // otherwise we are fully in view
367             return INSIDE;
368         }
369 
370         /// AABB vs frustum intersection.
371         /// Returns: Frustum.OUTSIDE, Frustum.INTERSECT or Frustum.INSIDE.
372         @nogc int contains(box3!T box) pure const nothrow
373         {
374             vec3!T[8] corners;
375             int totalIn = 0;
376 
377             for (int i = 0; i < 2; ++i)
378                 for (int j = 0; j < 2; ++j)
379                     for (int k = 0; k < 2; ++k)
380                     {
381                         auto x = i == 0 ? box.min.x : box.max.x;
382                         auto y = i == 0 ? box.min.y : box.max.y;
383                         auto z = i == 0 ? box.min.z : box.max.z;
384                         corners[i*4 + j*2 + k] = vec3!T(x, y, z);
385                     }
386 
387             // test all 8 corners against the 6 sides
388             // if all points are behind 1 specific plane, we are out
389             // if we are in with all points, then we are fully in
390             for(int p = 0; p < 6; ++p)
391             {
392                 int inCount = 8;
393                 int ptIn = 1;
394 
395                 for(int i = 0; i < 8; ++i)
396                 {
397                     // test this point against the planes
398                     if (planes[p].isBack(corners[i]))
399                     {
400                         ptIn = 0;
401                         --inCount;
402                     }
403                 }
404 
405                 // were all the points outside of plane p?
406                 if (inCount == 0)
407                     return OUTSIDE;
408 
409                 // check if they were all on the right side of the plane
410                 totalIn += ptIn;
411             }
412 
413             // so if totalIn is 6, then all are inside the view
414             if(totalIn == 6)
415                 return INSIDE;
416 
417             // we must be partly in then otherwise
418             return INTERSECT;
419         }
420 
421     }
422 }
423 
424 unittest
425 {
426     seg2f se;
427     triangle3f tr;
428     Frustum!double frust;
429     planed pl;
430 }