Gears/external/gfm/math/shapes.d

646 lines
21 KiB
D

/**
This module implements some abstract geometric shapes:
$(UL
$(LI Line segments.)
$(LI Triangle.)
$(LI Circles/spheres.)
$(LI Rays)
$(LI Planes)
$(LI Frustum)
)
*/
module gfm.math.shapes;
import std.math,
std.traits;
import gfm.math.vector,
gfm.math.box;
/// A Segment is 2 points.
/// When considered like a vector, it represents the arrow from a to b.
struct Segment(T, int N)
{
public
{
alias Vector!(T, N) point_t;
point_t a, b;
static if (N == 3 && isFloatingPoint!T)
{
/// Segment vs plane intersection.
/// See_also: "Geometry for Computer Graphics: Formulae, Examples
/// and Proofs", Vince (2005), p. 62
/// Returns: $(D true) if the segment crosses the plane exactly
/// once, or $(D false) if the segment is parallel to or does
/// not reach the plane
/// Params:
/// plane = The plane to intersect.
/// intersection = Point of intersection.
/// progress = Set to the point's progress between endpoints
/// $(D a) at 0.0 and $(D b) at 1.0, or T.infinity
/// if parallel to the plane
@nogc bool intersect(Plane!T plane, out point_t intersection, out T progress) pure const nothrow
{
// direction vector from a to b
point_t dir = b-a;
// dot product will be 0 if angle to plane is 0
T dp = dot(plane.n, dir);
if (abs(dp) < T.epsilon)
{
progress = T.infinity;
return false; // parallel to plane
}
// relative distance along segment where intersection happens
progress = -(dot(plane.n, a) - plane.d) / dp;
intersection = progress*dir + a;
return progress >= 0 && progress <= 1;
}
}
}
}
alias Segment!(float, 2) seg2f; /// 2D float segment.
alias Segment!(float, 3) seg3f; /// 3D float segment.
alias Segment!(double, 2) seg2d; /// 2D double segment.
alias Segment!(double, 3) seg3d; /// 3D double segment.
alias Segment!(int, 2) seg2i; /// 2D integer segment.
alias Segment!(int, 3) seg3i; /// 3D integer segment.
/// A Triangle is 3 points.
struct Triangle(T, int N)
{
public
{
alias Vector!(T, N) point_t;
point_t a, b, c;
static if (N == 2)
{
/// Returns: Area of a 2D triangle.
@nogc T area() pure const nothrow
{
return abs(signedArea());
}
/// Returns: Signed area of a 2D triangle.
@nogc T signedArea() pure const nothrow
{
return ((b.x * a.y - a.x * b.y)
+ (c.x * b.y - b.x * c.y)
+ (a.x * c.y - c.x * a.y)) / 2;
}
}
static if (N == 3 && isFloatingPoint!T)
{
/// Returns: Triangle normal.
@nogc Vector!(T, 3) computeNormal() pure const nothrow
{
return cross(b - a, c - a).normalized();
}
}
}
}
alias Triangle!(float, 2) triangle2f; /// 2D float triangle.
alias Triangle!(float, 3) triangle3f; /// 3D float triangle.
alias Triangle!(double, 2) triangle2d; /// 2D double triangle.
alias Triangle!(double, 3) triangle3d; /// 3D double triangle.
/// A Sphere is a point + a radius.
struct Sphere(T, int N)
{
public nothrow
{
alias Vector!(T, N) point_t;
point_t center;
T radius;
/// Creates a sphere from a point and a radius.
@nogc this(in point_t center_, T radius_) pure nothrow
{
center = center_;
radius = radius_;
}
/// Sphere contains point test.
/// Returns: true if the point is inside the sphere.
@nogc bool contains(in Sphere s) pure const nothrow
{
if (s.radius > radius)
return false;
T innerRadius = radius - s.radius;
return squaredDistanceTo(s.center) < innerRadius * innerRadius;
}
/// Sphere vs point Euclidean distance squared.
@nogc T squaredDistanceTo(point_t p) pure const nothrow
{
return center.squaredDistanceTo(p);
}
/// Sphere vs sphere intersection.
/// Returns: true if the spheres intersect.
@nogc bool intersects(Sphere s) pure const nothrow
{
T outerRadius = radius + s.radius;
return squaredDistanceTo(s.center) < outerRadius * outerRadius;
}
static if (isFloatingPoint!T)
{
/// Sphere vs point Euclidean distance.
@nogc T distanceTo(point_t p) pure const nothrow
{
return center.distanceTo(p);
}
static if(N == 2)
{
/// Returns: Circle area.
@nogc T area() pure const nothrow
{
return T(PI) * (radius * radius);
}
}
}
}
}
alias Sphere!(float, 2) sphere2f; /// 2D float sphere (ie. a circle).
alias Sphere!(float, 3) sphere3f; /// 3D float sphere.
alias Sphere!(double, 2) sphere2d; /// 2D double sphere (ie. a circle).
alias Sphere!(double, 3) sphere3d; /// 3D double sphere (ie. a circle).
/// A Ray ir a point + a direction.
struct Ray(T, int N)
{
nothrow:
public
{
alias Vector!(T, N) point_t;
point_t orig;
point_t dir;
/// Returns: A point further along the ray direction.
@nogc point_t progress(T t) pure const nothrow
{
return orig + dir * t;
}
static if (N == 3 && isFloatingPoint!T)
{
/// Ray vs triangle intersection.
/// See_also: "Fast, Minimum Storage Ray/Triangle intersection", Mommer & Trumbore (1997)
/// Returns: Barycentric coordinates, the intersection point is at $(D (1 - u - v) * A + u * B + v * C).
@nogc bool intersect(Triangle!(T, 3) triangle, out T t, out T u, out T v) pure const nothrow
{
point_t edge1 = triangle.b - triangle.a;
point_t edge2 = triangle.c - triangle.a;
point_t pvec = cross(dir, edge2);
T det = dot(edge1, pvec);
if (abs(det) < T.epsilon)
return false; // no intersection
T invDet = 1 / det;
// calculate distance from triangle.a to ray origin
point_t tvec = orig - triangle.a;
// calculate U parameter and test bounds
u = dot(tvec, pvec) * invDet;
if (u < 0 || u > 1)
return false;
// prepare to test V parameter
point_t qvec = cross(tvec, edge1);
// calculate V parameter and test bounds
v = dot(dir, qvec) * invDet;
if (v < 0.0 || u + v > 1.0)
return false;
// calculate t, ray intersects triangle
t = dot(edge2, qvec) * invDet;
return true;
}
/// Ray vs plane intersection.
/// See_also: "Geometry for Computer Graphics: Formulae, Examples
/// and Proofs", Vince (2005), p. 62
/// Returns: $(D true) if the ray crosses the plane exactly once,
/// or $(D false) if the ray is parallel to or points away from
/// the plane
/// Params:
/// plane = Plane to intersect.
/// intersection = Point of intersection.
/// distance = set to the point's distance along the ray, or
/// T.infinity if parallel to the plane
@nogc bool intersect(Plane!T plane, out point_t intersection, out T distance) pure const nothrow
{
// dot product will be 0 if angle to plane is 0
T dp = dot(plane.n, dir);
if (abs(dp) < T.epsilon)
{
distance = T.infinity;
return false; // parallel to plane
}
// distance along ray where intersection happens
distance = -(dot(plane.n, orig) - plane.d) / dp;
intersection = distance*dir + orig;
return distance >= 0;
}
}
}
}
alias Ray!(float, 2) ray2f; /// 2D float ray.
alias Ray!(float, 3) ray3f; /// 3D float ray.
alias Ray!(double, 2) ray2d; /// 2D double ray.
alias Ray!(double, 3) ray3d; /// 3D double ray.
/// 3D plane.
/// See_also: Flipcode article by Nate Miller $(WEB www.flipcode.com/archives/Plane_Class.shtml).
struct Plane(T) if (isFloatingPoint!T)
{
public
{
vec3!T n; /// Normal (always stored normalized).
T d;
/// Create from four coordinates.
@nogc this(vec4!T abcd) pure nothrow
{
n = vec3!T(abcd.x, abcd.y, abcd.z).normalized();
d = abcd.w;
}
/// Create from a point and a normal.
@nogc this(vec3!T origin, vec3!T normal) pure nothrow
{
n = normal.normalized();
d = -dot(origin, n);
}
/// Create from 3 non-aligned points.
@nogc this(vec3!T A, vec3!T B, vec3!T C) pure nothrow
{
this(C, cross(B - A, C - A));
}
/// Assign a plane with another plane.
@nogc ref Plane opAssign(Plane other) pure nothrow
{
n = other.n;
d = other.d;
return this;
}
/// Returns: signed distance between a point and the plane.
@nogc T signedDistanceTo(vec3!T point) pure const nothrow
{
return dot(n, point) + d;
}
/// Returns: absolute distance between a point and the plane.
@nogc T distanceTo(vec3!T point) pure const nothrow
{
return abs(signedDistanceTo(point));
}
/// Returns: true if the point is in front of the plane.
@nogc bool isFront(vec3!T point) pure const nothrow
{
return signedDistanceTo(point) >= 0;
}
/// Returns: true if the point is in the back of the plane.
@nogc bool isBack(vec3!T point) pure const nothrow
{
return signedDistanceTo(point) < 0;
}
/// Returns: true if the point is on the plane, with a given epsilon.
@nogc bool isOn(vec3!T point, T epsilon) pure const nothrow
{
T sd = signedDistanceTo(point);
return (-epsilon < sd) && (sd < epsilon);
}
}
}
alias Plane!float planef; /// 3D float plane.
alias Plane!double planed; /// 3D double plane.
unittest
{
auto p = planed(vec4d(1.0, 2.0, 3.0, 4.0));
auto p2 = planed(vec3d(1.0, 0.0, 0.0),
vec3d(0.0, 1.0, 0.0),
vec3d(0.0, 0.0, 1.0));
assert(p2.isOn(vec3d(1.0, 0.0, 0.0), 1e-7));
assert(p2.isFront(vec3d(1.0, 1.0, 1.0)));
assert(p2.isBack(vec3d(0.0, 0.0, 0.0)));
}
/// Plane intersection tests
@nogc pure nothrow unittest
{
void testR(planed p, ray3d r, bool shouldIntersect, double expectedDistance, vec3d expectedPoint = vec3d.init) pure nothrow @nogc
{
vec3d point;
double distance;
assert(r.intersect(p, point, distance) == shouldIntersect);
assert(approxEqual(distance, expectedDistance));
if (shouldIntersect)
assert(approxEqual(point.v[], expectedPoint.v[]));
}
// ray facing plane
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)),
true, 1.0, vec3d(1.0, 3.0, 4.0));
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()),
true, 2.0*sqrt(3.0), vec3d(-1.0, -1.0, -1.0));
// ray facing away
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)),
false, -1.0);
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)),
false, -3.0);
// ray parallel
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)),
false, double.infinity);
void testS(planed p, seg3d s, bool shouldIntersect, double expectedProgress, vec3d expectedPoint = vec3d.init) pure nothrow @nogc
{
vec3d point;
double progress;
assert(s.intersect(p, point, progress) == shouldIntersect);
assert(approxEqual(progress, expectedProgress));
if (shouldIntersect)
assert(approxEqual(point.v[], expectedPoint.v[]));
}
// segment crossing plane
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)),
true, 0.5, vec3d(2.0, 3.0, 4.0));
// segment too short
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)),
false, -0.5);
}
/// 3D frustum.
/// See_also: Flipcode article by Dion Picco $(WEB www.flipcode.com/archives/Frustum_Culling.shtml).
/// Bugs: verify proper signedness of half-spaces
struct Frustum(T) if (isFloatingPoint!T)
{
public
{
enum int LEFT = 0,
RIGHT = 1,
TOP = 2,
BOTTOM = 3,
NEAR = 4,
FAR = 5;
Plane!T[6] planes;
/// Create a frustum from 6 planes.
@nogc this(Plane!T left, Plane!T right, Plane!T top, Plane!T bottom, Plane!T near, Plane!T far) pure nothrow
{
planes[LEFT] = left;
planes[RIGHT] = right;
planes[TOP] = top;
planes[BOTTOM] = bottom;
planes[NEAR] = near;
planes[FAR] = far;
}
enum : int
{
OUTSIDE, /// object is outside the frustum
INTERSECT, /// object intersects with the frustum
INSIDE /// object is inside the frustum
}
/// Point vs frustum intersection.
@nogc bool contains(vec3!T point) pure const nothrow
{
for(int i = 0; i < 6; ++i)
{
T distance = planes[i].signedDistanceTo(point);
if(distance < 0)
return false;
}
return true;
}
/// Sphere vs frustum intersection.
/// Returns: Frustum.OUTSIDE, Frustum.INTERSECT or Frustum.INSIDE.
@nogc int contains(Sphere!(T, 3) sphere) pure const nothrow
{
// calculate our distances to each of the planes
for(int i = 0; i < 6; ++i)
{
// find the distance to this plane
T distance = planes[i].signedDistanceTo(sphere.center);
if(distance < -sphere.radius)
return OUTSIDE;
else if (distance < sphere.radius)
return INTERSECT;
}
// otherwise we are fully in view
return INSIDE;
}
/// AABB vs frustum intersection.
/// Returns: Frustum.OUTSIDE, Frustum.INTERSECT or Frustum.INSIDE.
@nogc int contains(box3!T box) pure const nothrow
{
vec3!T[8] corners;
int totalIn = 0;
for (int i = 0; i < 2; ++i)
for (int j = 0; j < 2; ++j)
for (int k = 0; k < 2; ++k)
{
auto x = i == 0 ? box.min.x : box.max.x;
auto y = j == 0 ? box.min.y : box.max.y;
auto z = k == 0 ? box.min.z : box.max.z;
corners[i*4 + j*2 + k] = vec3!T(x, y, z);
}
// test all 8 corners against the 6 sides
// if all points are behind 1 specific plane, we are out
// if we are in with all points, then we are fully in
for(int p = 0; p < 6; ++p)
{
int inCount = 8;
int ptIn = 1;
for(int i = 0; i < 8; ++i)
{
// test this point against the planes
if (planes[p].isBack(corners[i]))
{
ptIn = 0;
--inCount;
}
}
// were all the points outside of plane p?
if (inCount == 0)
return OUTSIDE;
// check if they were all on the right side of the plane
totalIn += ptIn;
}
// so if totalIn is 6, then all are inside the view
if(totalIn == 6)
return INSIDE;
// we must be partly in then otherwise
return INTERSECT;
}
}
}
unittest
{
seg2f se;
triangle3f tr;
Frustum!double frust;
planed pl;
}
/// True if `T` is a kind of Segment
enum isSegment(T) = is(T : Segment!U, U...);
/// True if `T` is a kind of Triangle
enum isTriangle(T) = is(T : Triangle!U, U...);
/// True if `T` is a kind of Sphere
enum isSphere(T) = is(T : Sphere!U, U...);
/// True if `T` is a kind of Ray
enum isRay(T) = is(T : Ray!U, U...);
/// True if `T` is a kind of Plane
enum isPlane(T) = is(T : Plane!U, U);
/// True if `T` is a kind of Frustum
enum isFrustum(T) = is(T : Frustum!U, U);
/// True if `T` is a kind of 2 dimensional Segment
enum isSegment2D(T) = is(T : Segment!(U, 2), U);
/// True if `T` is a kind of 2 dimensional Triangle
enum isTriangle2D(T) = is(T : Triangle!(U, 2), U);
/// True if `T` is a kind of 2 dimensional Sphere
enum isSphere2D(T) = is(T : Sphere!(U, 2), U);
/// True if `T` is a kind of 2 dimensional Ray
enum isRay2D(T) = is(T : Ray!(U, 2), U);
/// True if `T` is a kind of 3 dimensional Segment
enum isSegment3D(T) = is(T : Segment!(U, 3), U);
/// True if `T` is a kind of 3 dimensional Triangle
enum isTriangle3D(T) = is(T : Triangle!(U, 3), U);
/// True if `T` is a kind of 3 dimensional Sphere
enum isSphere3D(T) = is(T : Sphere!(U, 3), U);
/// True if `T` is a kind of 3 dimensional Ray
enum isRay3D(T) = is(T : Ray!(U, 3), U);
unittest
{
enum ShapeType
{
segment,
triangle,
sphere,
ray,
plane,
frustum
}
void test(T, ShapeType type, int dim)()
{
with (ShapeType)
{
static assert(isSegment!T == (type == segment ));
static assert(isTriangle!T == (type == triangle));
static assert(isSphere!T == (type == sphere ));
static assert(isRay!T == (type == ray ));
static assert(isPlane!T == (type == plane ));
static assert(isFrustum!T == (type == frustum ));
static assert(isSegment2D!T == (type == segment && dim == 2));
static assert(isTriangle2D!T == (type == triangle && dim == 2));
static assert(isSphere2D!T == (type == sphere && dim == 2));
static assert(isRay2D!T == (type == ray && dim == 2));
static assert(isSegment3D!T == (type == segment && dim == 3));
static assert(isTriangle3D!T == (type == triangle && dim == 3));
static assert(isSphere3D!T == (type == sphere && dim == 3));
static assert(isRay3D!T == (type == ray && dim == 3));
}
}
with (ShapeType)
{
// test case type #dimensions
test!(seg2f , segment , 2);
test!(seg3d , segment , 3);
test!(triangle2d , triangle, 2);
test!(triangle3f , triangle, 3);
test!(sphere2d , sphere , 2);
test!(Sphere!(uint, 3), sphere , 3);
test!(ray2f , ray , 2);
test!(Ray!(real, 3) , ray , 3);
test!(planed , plane , 0); // ignore dimension (always 3D)
test!(Plane!float , plane , 0);
test!(Frustum!double , frustum , 0);
}
}
/// Get the numeric type used to measure a shape's dimensions.
alias DimensionType(T : Segment!U, U...) = U[0];
/// ditto
alias DimensionType(T : Triangle!U, U...) = U[0];
/// ditto
alias DimensionType(T : Sphere!U, U...) = U[0];
/// ditto
alias DimensionType(T : Ray!U, U...) = U[0];
/// ditto
alias DimensionType(T : Plane!U, U) = U;
/// ditto
alias DimensionType(T : Frustum!U, U) = U;
///
unittest
{
static assert(is(DimensionType!seg2i == int));
static assert(is(DimensionType!triangle3d == double));
static assert(is(DimensionType!sphere2d == double));
static assert(is(DimensionType!ray3f == float));
static assert(is(DimensionType!planed == double));
static assert(is(DimensionType!(Frustum!real) == real));
}