1
0
forked from 0ad/0ad

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:
vts 2012-05-31 23:17:15 +00:00
parent 9e798addf9
commit 1c005b0c0e
5 changed files with 196 additions and 60 deletions

View File

@ -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

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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
{

View File

@ -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));
}
};