Fixed AABB to OBB transformation resulting in NaN basis vectors when the AABB size in one or more dimensions is zero. Fixes #1121.
This was SVN commit r11928.
This commit is contained in:
parent
9e798addf9
commit
1c005b0c0e
@ -165,53 +165,33 @@ void CBoundingBoxAligned::Transform(const CMatrix3D& m, CBoundingBoxAligned& res
|
||||
|
||||
void CBoundingBoxAligned::Transform(const CMatrix3D& transform, CBoundingBoxOriented& result) const
|
||||
{
|
||||
// The idea is this: compute the corners of this bounding box, transform them according to the specified matrix,
|
||||
// then derive the box center, orientation vectors, and half-sizes.
|
||||
// TODO: this implementation can be further optimized; see Philip's comments on http://trac.wildfiregames.com/ticket/914
|
||||
const CVector3D& pMin = m_Data[0];
|
||||
const CVector3D& pMax = m_Data[1];
|
||||
|
||||
// Find the corners of these bounds. We only need some of the corners to derive the information we need, so let's
|
||||
// not actually compute all of them. The corners are numbered starting from the minimum position (m_Data[0]), going
|
||||
// counter-clockwise in the bottom plane, and then in the same order for the top plane (starting from the corner
|
||||
// that's directly above the minimum position). Hence, corner0 is pMin and corner6 is pMax, so we don't need to
|
||||
// custom-create those.
|
||||
|
||||
CVector3D corner0; // corner0 is pMin, no need to copy it
|
||||
CVector3D corner1(pMax.X, pMin.Y, pMin.Z);
|
||||
CVector3D corner3(pMin.X, pMin.Y, pMax.Z);
|
||||
CVector3D corner4(pMin.X, pMax.Y, pMin.Z);
|
||||
CVector3D corner6; // corner6 is pMax, no need to copy it
|
||||
|
||||
// transform corners to world space
|
||||
corner0 = transform.Transform(pMin); // = corner0
|
||||
corner1 = transform.Transform(corner1);
|
||||
corner3 = transform.Transform(corner3);
|
||||
corner4 = transform.Transform(corner4);
|
||||
corner6 = transform.Transform(pMax); // = corner6
|
||||
|
||||
// Compute orientation vectors, half-size vector, and box center. We can get the orientation vectors by just taking
|
||||
// the directional vectors from a specific corner point (corner0) to the other corners, once in each direction. The
|
||||
// half-sizes are similarly computed by taking the distances of those sides and dividing them by 2. Finally, the
|
||||
// center is simply the middle between the transformed pMin and pMax corners.
|
||||
|
||||
const CVector3D sideU(corner1 - corner0);
|
||||
const CVector3D sideV(corner4 - corner0);
|
||||
const CVector3D sideW(corner3 - corner0);
|
||||
|
||||
result.m_Basis[0] = sideU.Normalized();
|
||||
result.m_Basis[1] = sideV.Normalized();
|
||||
result.m_Basis[2] = sideW.Normalized();
|
||||
// the basis vectors of the OBB are the normalized versions of the transformed AABB basis vectors, which
|
||||
// are the columns of the identity matrix, so the unnormalized OBB basis vectors are the transformation
|
||||
// matrix columns:
|
||||
CVector3D u(transform._11, transform._21, transform._31);
|
||||
CVector3D v(transform._12, transform._22, transform._32);
|
||||
CVector3D w(transform._13, transform._23, transform._33);
|
||||
|
||||
// the half-sizes are scaled by whatever factor the AABB unit vectors end up scaled by
|
||||
result.m_HalfSizes = CVector3D(
|
||||
sideU.Length()/2.f,
|
||||
sideV.Length()/2.f,
|
||||
sideW.Length()/2.f
|
||||
(pMax.X - pMin.X) / 2.f * u.Length(),
|
||||
(pMax.Y - pMin.Y) / 2.f * v.Length(),
|
||||
(pMax.Z - pMin.Z) / 2.f * w.Length()
|
||||
);
|
||||
|
||||
result.m_Center = (corner0 + corner6) * 0.5f;
|
||||
}
|
||||
u.Normalize();
|
||||
v.Normalize();
|
||||
w.Normalize();
|
||||
|
||||
result.m_Basis[0] = u;
|
||||
result.m_Basis[1] = v;
|
||||
result.m_Basis[2] = w;
|
||||
|
||||
result.m_Center = transform.Transform((pMax + pMin) * 0.5f);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Intersect with the given frustum in a conservative manner
|
||||
|
@ -51,7 +51,7 @@ public:
|
||||
/**
|
||||
* Transform these bounds using the matrix @p transform, and write out the result as an oriented (i.e. non-axis-aligned) box.
|
||||
* The difference with @ref Transform(const CMatrix3D&, CBoundingBoxAligned&) is that that method is equivalent to first
|
||||
* computing this result, and then from that taking the axis-aligned bounding boxes again.
|
||||
* computing this result, and then taking the axis-aligned bounding boxes from the result again.
|
||||
*/
|
||||
void Transform(const CMatrix3D& m, CBoundingBoxOriented& result) const;
|
||||
|
||||
|
@ -54,29 +54,40 @@ bool CBoundingBoxOriented::RayIntersect(const CVector3D& origin, const CVector3D
|
||||
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
float e = m_Basis[i].Dot(p);
|
||||
float f = m_Basis[i].Dot(dir);
|
||||
// test the ray for intersections with the slab whose normal vector is m_Basis[i]
|
||||
float e = m_Basis[i].Dot(p); // distance between the ray origin and the box center projected onto the slab normal
|
||||
float f = m_Basis[i].Dot(dir); // cosine of the angle between the slab normal and the ray direction
|
||||
|
||||
if(fabs(f) > 1e-10f)
|
||||
if(fabsf(f) > 1e-10f)
|
||||
{
|
||||
// Determine the distances t1 and t2 from the origin of the ray to the points where it intersects
|
||||
// the slab. See docs/ray_intersect.pdf for why/how this works.
|
||||
float invF = 1.f/f;
|
||||
float t1 = (e + m_HalfSizes[i]) * invF;
|
||||
float t2 = (e - m_HalfSizes[i]) * invF;
|
||||
|
||||
// make sure t1 <= t2, swap if necessary
|
||||
if (t1 > t2)
|
||||
{
|
||||
float tmp = t1;
|
||||
t1 = t2;
|
||||
t2 = tmp;
|
||||
}
|
||||
|
||||
// update the overall tMin and tMax if necessary
|
||||
if (t1 > tMin) tMin = t1;
|
||||
if (t2 < tMax) tMax = t2;
|
||||
if (tMin > tMax) return false;
|
||||
if (tMax < 0) return false;
|
||||
|
||||
// try to break out of the loop as fast as possible by checking for some conditions
|
||||
if (tMin > tMax) return false; // ray misses the box
|
||||
if (tMax < 0) return false; // box is behind the ray origin
|
||||
}
|
||||
else
|
||||
{
|
||||
if(-e - m_HalfSizes[i] > 0 || -e + m_HalfSizes[i] < 0) return false;
|
||||
// the ray is parallel to the slab currently being tested, or is as close to parallel
|
||||
// as makes no difference; return false if the ray is outside of the slab.
|
||||
if (e > m_HalfSizes[i] || -e > m_HalfSizes[i])
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,9 +23,9 @@
|
||||
class CBoundingBoxAligned;
|
||||
|
||||
/*
|
||||
* Generic oriented box. Originally intended to be used an Oriented Bounding Box (OBB), as opposed to CBoundingBoxAligned which is
|
||||
* always aligned to the world-space axes (AABB). However, it can also be used to represent more generic shapes, such as
|
||||
* parallelepipeds, for other purposes.
|
||||
* Generic oriented box. Originally intended to be used an Oriented Bounding Box (OBB),
|
||||
* as opposed to CBoundingBoxAligned which is always aligned to the world-space axes (AABB).
|
||||
* However, it could also be used to represent more generic shapes, such as parallelepipeds.
|
||||
*/
|
||||
class CBoundingBoxOriented
|
||||
{
|
||||
@ -35,9 +35,10 @@ public:
|
||||
CBoundingBoxOriented() { SetEmpty(); }
|
||||
|
||||
/**
|
||||
* Constructs a new oriented box centered at @p center and with normalized side vectors @p u, @p v and @p w. These vectors should
|
||||
* be mutually orthonormal for a proper rectangular box. The half-widths of the box in each dimension are given by the corresponding
|
||||
* components of @p halfSizes.
|
||||
* Constructs a new oriented box centered at @p center and with normalized side vectors @p u,
|
||||
* @p v and @p w. These vectors should be mutually orthonormal for a proper rectangular box.
|
||||
* The half-widths of the box in each dimension are given by the corresponding components of
|
||||
* @p halfSizes.
|
||||
*/
|
||||
CBoundingBoxOriented(const CVector3D& center, const CVector3D& u, const CVector3D& v, const CVector3D& w, const CVector3D& halfSizes)
|
||||
: m_Center(center), m_HalfSizes(halfSizes)
|
||||
@ -51,22 +52,26 @@ public:
|
||||
explicit CBoundingBoxOriented(const CBoundingBoxAligned& bound);
|
||||
|
||||
/**
|
||||
* Check if a given ray intersects this box.
|
||||
* See also Real-Time Rendering, Third Edition by T. Akenine-Möller, p. 741--742.
|
||||
* Should not be used if IsEmpty() is true.
|
||||
* Check if a given ray intersects this box. Must not be used if IsEmpty() is true.
|
||||
* See Real-Time Rendering, Third Edition by T. Akenine-Möller, p. 741--744.
|
||||
*
|
||||
* @param[in] origin Origin of the ray.
|
||||
* @param[in] dir Direction vector of the ray, defining the positive direction of the ray. Must be of unit length.
|
||||
* @param[out] tMin,tMax distance in the positive direction from the origin of the ray to the entry and exit points in the
|
||||
* box. If the origin is inside the box, then this is counted as an intersection and one of @p tMin and @p tMax may be negative.
|
||||
* @param[in] dir Direction vector of the ray, defining the positive direction of the ray.
|
||||
* Must be of unit length.
|
||||
* @param[out] tMin,tMax Distance in the positive direction from the origin of the ray to the
|
||||
* entry and exit points in the box, provided that the ray intersects the box. if
|
||||
* the ray does not intersect the box, no values are written to these variables.
|
||||
* If the origin is inside the box, then this is counted as an intersection and one
|
||||
* of @p tMin and @p tMax may be negative.
|
||||
*
|
||||
* @return true if the ray originating in @p origin and with unit direction vector @p dir intersects this box, false otherwise.
|
||||
* @return true If the ray originating in @p origin and with unit direction vector @p dir intersects
|
||||
* this box, false otherwise.
|
||||
*/
|
||||
bool RayIntersect(const CVector3D& origin, const CVector3D& dir, float& tMin, float& tMax) const;
|
||||
|
||||
/**
|
||||
* Returns the corner at coordinate (@p u, @p v, @p w). Each of @p u, @p v and @p w must be exactly 1 or -1.
|
||||
* Should not be used if IsEmpty() is true.
|
||||
* Must not be used if IsEmpty() is true.
|
||||
*/
|
||||
void GetCorner(int u, int v, int w, CVector3D& out) const
|
||||
{
|
||||
|
@ -17,12 +17,24 @@
|
||||
|
||||
#include "lib/self_test.h"
|
||||
|
||||
#include "lib/posix/posix.h"
|
||||
#include "maths/BoundingBoxAligned.h"
|
||||
#include "maths/BoundingBoxOriented.h"
|
||||
#include "maths/Matrix3D.h"
|
||||
|
||||
#define TS_ASSERT_VEC_DELTA(v, x, y, z, delta) \
|
||||
TS_ASSERT_DELTA(v.X, x, delta); \
|
||||
TS_ASSERT_DELTA(v.Y, y, delta); \
|
||||
TS_ASSERT_DELTA(v.Z, z, delta);
|
||||
|
||||
class TestBound : public CxxTest::TestSuite
|
||||
{
|
||||
public:
|
||||
void setUp()
|
||||
{
|
||||
CxxTest::setAbortTestOnFail(true);
|
||||
}
|
||||
|
||||
void test_empty_aabb()
|
||||
{
|
||||
CBoundingBoxAligned bound;
|
||||
@ -68,4 +80,132 @@ public:
|
||||
bound.GetCentre(centre);
|
||||
TS_ASSERT_EQUALS(centre, v);
|
||||
}
|
||||
|
||||
void test_aabb_to_obb_translation()
|
||||
{
|
||||
CBoundingBoxAligned aabb(CVector3D(-1,-2,-1), CVector3D(1,2,1));
|
||||
|
||||
CMatrix3D translation;
|
||||
translation.SetTranslation(CVector3D(1,3,7));
|
||||
|
||||
CBoundingBoxOriented result;
|
||||
aabb.Transform(translation, result);
|
||||
|
||||
TS_ASSERT_VEC_DELTA(result.m_Center, 1.f, 3.f, 7.f, 1e-7f);
|
||||
TS_ASSERT_VEC_DELTA(result.m_Basis[0], 1.f, 0.f, 0.f, 1e-7f);
|
||||
TS_ASSERT_VEC_DELTA(result.m_Basis[1], 0.f, 1.f, 0.f, 1e-7f);
|
||||
TS_ASSERT_VEC_DELTA(result.m_Basis[2], 0.f, 0.f, 1.f, 1e-7f);
|
||||
TS_ASSERT_VEC_DELTA(result.m_HalfSizes, 1.f, 2.f, 1.f, 1e-7f);
|
||||
}
|
||||
|
||||
void test_aabb_to_obb_rotation_around_origin()
|
||||
{
|
||||
// rotate a 4x3x3 AABB centered at (5,0,0) 90 degrees CCW around the Z axis, and verify that the
|
||||
// resulting OBB is correct
|
||||
CBoundingBoxAligned aabb(CVector3D(3, -1.5f, -1.5f), CVector3D(7, 1.5f, 1.5f));
|
||||
|
||||
CMatrix3D rotation;
|
||||
rotation.SetZRotation(float(M_PI)/2.f);
|
||||
|
||||
CBoundingBoxOriented result;
|
||||
aabb.Transform(rotation, result);
|
||||
|
||||
TS_ASSERT_VEC_DELTA(result.m_Center, 0.f, 5.f, 0.f, 1e-6f); // involves some trigonometry, lower precision
|
||||
TS_ASSERT_VEC_DELTA(result.m_Basis[0], 0.f, 1.f, 0.f, 1e-7f);
|
||||
TS_ASSERT_VEC_DELTA(result.m_Basis[1], -1.f, 0.f, 0.f, 1e-7f);
|
||||
TS_ASSERT_VEC_DELTA(result.m_Basis[2], 0.f, 0.f, 1.f, 1e-7f);
|
||||
}
|
||||
|
||||
void test_aabb_to_obb_rotation_around_point()
|
||||
{
|
||||
// rotate a 4x3x3 AABB centered at (5,0,0) 45 degrees CW around the Z axis through (2,0,0)
|
||||
CBoundingBoxAligned aabb(CVector3D(3, -1.5f, -1.5f), CVector3D(7, 1.5f, 1.5f));
|
||||
|
||||
// move everything so (2,0,0) becomes the origin, do the rotation, then move everything back
|
||||
CMatrix3D translate;
|
||||
CMatrix3D rotate;
|
||||
CMatrix3D translateBack;
|
||||
translate.SetTranslation(-2.f, 0, 0);
|
||||
rotate.SetZRotation(-float(M_PI)/4.f);
|
||||
translateBack.SetTranslation(2.f, 0, 0);
|
||||
|
||||
CMatrix3D transform;
|
||||
transform.SetIdentity();
|
||||
transform.Concatenate(translate);
|
||||
transform.Concatenate(rotate);
|
||||
transform.Concatenate(translateBack);
|
||||
|
||||
CBoundingBoxOriented result;
|
||||
aabb.Transform(transform, result);
|
||||
|
||||
const float invSqrt2 = 1.f/sqrtf(2.f);
|
||||
|
||||
TS_ASSERT_VEC_DELTA(result.m_Center, 3*invSqrt2 + 2, -3*invSqrt2, 0.f, 1e-6f); // involves some trigonometry, lower precision
|
||||
TS_ASSERT_VEC_DELTA(result.m_Basis[0], invSqrt2, -invSqrt2, 0.f, 1e-7f);
|
||||
TS_ASSERT_VEC_DELTA(result.m_Basis[1], invSqrt2, invSqrt2, 0.f, 1e-7f);
|
||||
TS_ASSERT_VEC_DELTA(result.m_Basis[2], 0.f, 0.f, 1.f, 1e-7f);
|
||||
}
|
||||
|
||||
void test_aabb_to_obb_scale()
|
||||
{
|
||||
CBoundingBoxAligned aabb(CVector3D(3, -1.5f, -1.5f), CVector3D(7, 1.5f, 1.5f));
|
||||
|
||||
CMatrix3D scale;
|
||||
scale.SetScaling(1.f, 3.f, 7.f);
|
||||
|
||||
CBoundingBoxOriented result;
|
||||
aabb.Transform(scale, result);
|
||||
|
||||
TS_ASSERT_VEC_DELTA(result.m_Center, 5.f, 0.f, 0.f, 1e-7f);
|
||||
TS_ASSERT_VEC_DELTA(result.m_HalfSizes, 2.f, 4.5f, 10.5f, 1e-7f);
|
||||
TS_ASSERT_VEC_DELTA(result.m_Basis[0], 1.f, 0.f, 0.f, 1e-7f);
|
||||
TS_ASSERT_VEC_DELTA(result.m_Basis[1], 0.f, 1.f, 0.f, 1e-7f);
|
||||
TS_ASSERT_VEC_DELTA(result.m_Basis[2], 0.f, 0.f, 1.f, 1e-7f);
|
||||
}
|
||||
|
||||
// Verify that ray/OBB intersection is correctly determined in degenerate case where the
|
||||
// box has zero size in one of its dimensions.
|
||||
void test_degenerate_obb_ray_intersect()
|
||||
{
|
||||
// create OBB of a flat 1x1 square in the X/Z plane, with 0 size in the Y dimension
|
||||
CBoundingBoxOriented bound;
|
||||
bound.m_Basis[0] = CVector3D(1,0,0); // X
|
||||
bound.m_Basis[1] = CVector3D(0,1,0); // Y
|
||||
bound.m_Basis[2] = CVector3D(0,0,1); // Z
|
||||
bound.m_HalfSizes[0] = 1.f;
|
||||
bound.m_HalfSizes[1] = 0.f; // no height, i.e. a "flat" OBB
|
||||
bound.m_HalfSizes[2] = 1.f;
|
||||
bound.m_Center = CVector3D(0,0,0);
|
||||
|
||||
// create two rays; one that should hit the OBB, and one that should miss it
|
||||
CVector3D ray1origin(-3.5f, 3.f, 0.f);
|
||||
CVector3D ray1direction(1.f, -1.f, 0.f);
|
||||
CVector3D ray2origin(-4.5f, 3.f, 0.f);
|
||||
CVector3D ray2direction(1.f, -1.f, 0.f);
|
||||
|
||||
float tMin, tMax;
|
||||
TSM_ASSERT("Ray 1 should intersect the OBB", bound.RayIntersect(ray1origin, ray1direction, tMin, tMax));
|
||||
TSM_ASSERT("Ray 2 should not intersect the OBB", !bound.RayIntersect(ray2origin, ray2direction, tMin, tMax));
|
||||
}
|
||||
|
||||
// Verify that transforming a flat AABB to an OBB does not produce NaN basis vectors in the
|
||||
// resulting OBB (see http://trac.wildfiregames.com/ticket/1121)
|
||||
void test_degenerate_aabb_to_obb_transform()
|
||||
{
|
||||
// create a flat AABB, transform it with some matrix (can even be the identity matrix),
|
||||
// and verify that the result does not contain any NaN values in its basis vectors
|
||||
// and/or half-sizes
|
||||
CBoundingBoxAligned flatAabb(CVector3D(-1,0,-1), CVector3D(1,0,1));
|
||||
|
||||
CMatrix3D transform;
|
||||
transform.SetIdentity();
|
||||
|
||||
CBoundingBoxOriented result;
|
||||
flatAabb.Transform(transform, result);
|
||||
|
||||
TS_ASSERT(!isnan(result.m_Basis[0].X) && !isnan(result.m_Basis[0].Y) && !isnan(result.m_Basis[0].Z));
|
||||
TS_ASSERT(!isnan(result.m_Basis[1].X) && !isnan(result.m_Basis[1].Y) && !isnan(result.m_Basis[1].Z));
|
||||
TS_ASSERT(!isnan(result.m_Basis[2].X) && !isnan(result.m_Basis[2].Y) && !isnan(result.m_Basis[2].Z));
|
||||
}
|
||||
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user