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 }