diff --git a/source/maths/BoundingBoxAligned.cpp b/source/maths/BoundingBoxAligned.cpp index 2e0e706650..6d8be8f76f 100644 --- a/source/maths/BoundingBoxAligned.cpp +++ b/source/maths/BoundingBoxAligned.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2011 Wildfire Games. +/* Copyright (C) 2012 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -217,6 +217,12 @@ void CBoundingBoxAligned::Transform(const CMatrix3D& transform, CBoundingBoxOrie // Intersect with the given frustum in a conservative manner void CBoundingBoxAligned::IntersectFrustumConservative(const CFrustum& frustum) { + // if this bound is empty, then the result must be empty (we should not attempt to intersect with + // a brush, may cause crashes due to the numeric representation of empty bounds -- see + // http://trac.wildfiregames.com/ticket/1027) + if (IsEmpty()) + return; + CBrush brush(*this); CBrush buf; diff --git a/source/maths/BoundingBoxAligned.h b/source/maths/BoundingBoxAligned.h index 43f0a5bb76..5189ecb176 100644 --- a/source/maths/BoundingBoxAligned.h +++ b/source/maths/BoundingBoxAligned.h @@ -126,6 +126,7 @@ public: * * @note While not in the spirit of this function's purpose, a no-op would be a correct * implementation of this function. + * @note If this bound is empty, the result is the empty bound. * * @param frustum the frustum to intersect with */ diff --git a/source/maths/Brush.cpp b/source/maths/Brush.cpp index d6909cfd15..672284374f 100644 --- a/source/maths/Brush.cpp +++ b/source/maths/Brush.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2012 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -38,11 +38,13 @@ CBrush::CBrush(const CBoundingBoxAligned& bounds) for(size_t i = 0; i < 8; ++i) { - m_Vertices[i][0] = bounds[(i & 1) ? 1 : 0][0]; - m_Vertices[i][1] = bounds[(i & 2) ? 1 : 0][1]; - m_Vertices[i][2] = bounds[(i & 4) ? 1 : 0][2]; + m_Vertices[i][0] = bounds[(i & 1) ? 1 : 0][0]; // X + m_Vertices[i][1] = bounds[(i & 2) ? 1 : 0][1]; // Y + m_Vertices[i][2] = bounds[(i & 4) ? 1 : 0][2]; // Z } + // construct cube face indices, 5 vertex indices per face (start vertex included twice) + m_Faces.resize(30); m_Faces[0] = 0; m_Faces[1] = 1; m_Faces[2] = 3; m_Faces[3] = 2; m_Faces[4] = 0; // Z = min @@ -69,208 +71,278 @@ void CBrush::Bounds(CBoundingBoxAligned& result) const /////////////////////////////////////////////////////////////////////////////// // Cut the brush according to a given plane -struct SliceVertexInfo { - float d; // distance - size_t res; // index in result brush (or no_vertex if cut away) + +/// Holds information about what happens to a single vertex in a brush during a slicing operation. +struct SliceOpVertexInfo +{ + float planeDist; ///< Signed distance from this vertex to the slicing plane. + size_t resIdx; ///< Index of this vertex in the resulting brush (or NO_VERTEX if cut away) }; -struct NewVertexInfo { - size_t v1, v2; // adjacent vertices in original brush - size_t res; // index in result brush - - size_t neighb1, neighb2; // index into newv +/// Holds information about a newly introduced vertex on an edge in a brush as the result of a slicing operation. +struct SliceOpNewVertexInfo +{ + /// Indices of adjacent edge vertices in original brush + size_t edgeIdx1, edgeIdx2; + /// Index of newly introduced vertex in resulting brush + size_t resIdx; + + /** + * Index into SliceOpInfo.nvInfo; hold the indices of this new vertex's direct neighbours in the slicing plane face, + * with no consistent winding direction around the face for either field (e.g., the neighb1 of X can point back to + * X with either its neighb1 or neighb2). + */ + size_t neighbIdx1, neighbIdx2; }; -struct SliceInfo { - std::vector v; - std::vector newv; - size_t thisFaceNewVertex; // index into newv - const CBrush* original; +/// Holds support information during a CBrush/CPlane slicing operation. +struct SliceOpInfo +{ CBrush* result; + const CBrush* original; + + /** + * Holds information about what happens to each vertex in the original brush after the slice operation. + * Same size as m_Vertices of the brush getting sliced. + */ + std::vector ovInfo; + + /// Holds information about newly inserted vertices during a slice operation. + std::vector nvInfo; + + /** + * Indices into nvInfo; during the execution of the slicing algorithm, holds the previously inserted new vertex on + * one of the edges of the face that's currently being evaluated for slice points, or NO_VERTEX if no such vertex + * exists. + */ + size_t thisFaceNewVertexIdx; }; struct CBrush::Helper { - static size_t SliceNewVertex(SliceInfo& si, size_t v1, size_t v2); + /** + * Creates a new vertex between the given two vertices (indexed into the original brush). + * Returns the index of the new vertex in the resulting brush. + */ + static size_t SliceNewVertex(SliceOpInfo& sliceInfo, size_t v1, size_t v2); }; -// create a new vertex between the given two vertices (index into original brush) -// returns the index of the new vertex in the resulting brush -size_t CBrush::Helper::SliceNewVertex(SliceInfo& si, size_t v1, size_t v2) +size_t CBrush::Helper::SliceNewVertex(SliceOpInfo& sliceOp, size_t edgeIdx1, size_t edgeIdx2) { + // check if a new vertex has already been inserted on this edge size_t idx; - - for(idx = 0; idx < si.newv.size(); ++idx) + for(idx = 0; idx < sliceOp.nvInfo.size(); ++idx) { - if ((si.newv[idx].v1 == v1 && si.newv[idx].v2 == v2) || - (si.newv[idx].v1 == v2 && si.newv[idx].v2 == v1)) + if ((sliceOp.nvInfo[idx].edgeIdx1 == edgeIdx1 && sliceOp.nvInfo[idx].edgeIdx2 == edgeIdx2) || + (sliceOp.nvInfo[idx].edgeIdx1 == edgeIdx2 && sliceOp.nvInfo[idx].edgeIdx2 == edgeIdx1)) break; } - if (idx >= si.newv.size()) + if (idx >= sliceOp.nvInfo.size()) { - NewVertexInfo nvi; - CVector3D newpos; - float inv = 1.0 / (si.v[v1].d - si.v[v2].d); + // no previously inserted new vertex found on this edge; insert a new one + SliceOpNewVertexInfo nvi; + CVector3D newPos; + + // interpolate between the two vertices based on their distance from the plane + float inv = 1.0 / (sliceOp.ovInfo[edgeIdx1].planeDist - sliceOp.ovInfo[edgeIdx2].planeDist); + newPos = sliceOp.original->m_Vertices[edgeIdx2] * ( sliceOp.ovInfo[edgeIdx1].planeDist * inv) + + sliceOp.original->m_Vertices[edgeIdx1] * (-sliceOp.ovInfo[edgeIdx2].planeDist * inv); - newpos = si.original->m_Vertices[v2]*(si.v[v1].d*inv) + - si.original->m_Vertices[v1]*(-si.v[v2].d*inv); + nvi.edgeIdx1 = edgeIdx1; + nvi.edgeIdx2 = edgeIdx2; + nvi.resIdx = sliceOp.result->m_Vertices.size(); + nvi.neighbIdx1 = NO_VERTEX; + nvi.neighbIdx2 = NO_VERTEX; - nvi.v1 = v1; - nvi.v2 = v2; - nvi.res = si.result->m_Vertices.size(); - nvi.neighb1 = no_vertex; - nvi.neighb2 = no_vertex; - si.result->m_Vertices.push_back(newpos); - si.newv.push_back(nvi); + sliceOp.result->m_Vertices.push_back(newPos); + sliceOp.nvInfo.push_back(nvi); } - if (si.thisFaceNewVertex != no_vertex) + // at this point, 'idx' is the index into nvInfo of the vertex inserted onto the edge + + if (sliceOp.thisFaceNewVertexIdx != NO_VERTEX) { - if (si.newv[si.thisFaceNewVertex].neighb1 == no_vertex) - si.newv[si.thisFaceNewVertex].neighb1 = idx; - else - si.newv[si.thisFaceNewVertex].neighb2 = idx; + // a vertex has been previously inserted onto another edge of this face; link them together as neighbours + // (using whichever one of the neighbIdx1 or -2 links is still available) - if (si.newv[idx].neighb1 == no_vertex) - si.newv[idx].neighb1 = si.thisFaceNewVertex; + if (sliceOp.nvInfo[sliceOp.thisFaceNewVertexIdx].neighbIdx1 == NO_VERTEX) + sliceOp.nvInfo[sliceOp.thisFaceNewVertexIdx].neighbIdx1 = idx; else - si.newv[idx].neighb2 = si.thisFaceNewVertex; + sliceOp.nvInfo[sliceOp.thisFaceNewVertexIdx].neighbIdx2 = idx; - si.thisFaceNewVertex = no_vertex; + if (sliceOp.nvInfo[idx].neighbIdx1 == NO_VERTEX) + sliceOp.nvInfo[idx].neighbIdx1 = sliceOp.thisFaceNewVertexIdx; + else + sliceOp.nvInfo[idx].neighbIdx2 = sliceOp.thisFaceNewVertexIdx; + + // a plane should slice a face only in two locations, so reset for the next face + sliceOp.thisFaceNewVertexIdx = NO_VERTEX; } else { - si.thisFaceNewVertex = idx; + // store the index of the inserted vertex on this edge, so that we can retrieve it when the plane slices + // this face again in another edge + sliceOp.thisFaceNewVertexIdx = idx; } - return si.newv[idx].res; + return sliceOp.nvInfo[idx].resIdx; } void CBrush::Slice(const CPlane& plane, CBrush& result) const { ENSURE(&result != this); - SliceInfo si; + SliceOpInfo sliceOp; - si.original = this; - si.result = &result; - si.thisFaceNewVertex = no_vertex; - si.newv.reserve(m_Vertices.size() / 2); + sliceOp.original = this; + sliceOp.result = &result; + sliceOp.thisFaceNewVertexIdx = NO_VERTEX; + sliceOp.ovInfo.resize(m_Vertices.size()); + sliceOp.nvInfo.reserve(m_Vertices.size() / 2); result.m_Vertices.resize(0); // clear any left-overs result.m_Faces.resize(0); result.m_Vertices.reserve(m_Vertices.size() + 2); result.m_Faces.reserve(m_Faces.size() + 5); - // Classify and copy vertices - si.v.resize(m_Vertices.size()); - + // Copy vertices that weren't sliced away by the plane to the resulting brush. for(size_t i = 0; i < m_Vertices.size(); ++i) { - si.v[i].d = plane.DistanceToPlane(m_Vertices[i]); - if (si.v[i].d >= 0.0) + const CVector3D& vtx = m_Vertices[i]; // current vertex + SliceOpVertexInfo& vtxInfo = sliceOp.ovInfo[i]; // slicing operation info about current vertex + + vtxInfo.planeDist = plane.DistanceToPlane(vtx); + if (vtxInfo.planeDist >= 0.0) { - si.v[i].res = result.m_Vertices.size(); - result.m_Vertices.push_back(m_Vertices[i]); + // positive side of the plane; not sliced away + vtxInfo.resIdx = result.m_Vertices.size(); + result.m_Vertices.push_back(vtx); } else { - si.v[i].res = no_vertex; + // other side of the plane; sliced away + vtxInfo.resIdx = NO_VERTEX; } } - // Transfer faces - size_t firstInFace = no_vertex; // in original brush - size_t startInResultFaceArray = ~0u; + // Transfer faces. (Recall how faces are specified; see CBrush::m_Faces). The idea is to examine each face separately, + // and see where its edges cross the slicing plane (meaning that exactly one of the vertices of that edge was cut away). + // On those edges, new vertices are introduced where the edge intersects the plane, and the resulting brush's m_Faces + // array is updated to refer to the newly inserted vertices instead of the original one that got cut away. + + size_t currentFaceStartIdx = NO_VERTEX; // index of the first vertex of the current face in the original brush + size_t resultFaceStartIdx = NO_VERTEX; // index of the first vertex of the current face in the resulting brush for(size_t i = 0; i < m_Faces.size(); ++i) { - if (firstInFace == no_vertex) + if (currentFaceStartIdx == NO_VERTEX) { - ENSURE(si.thisFaceNewVertex == no_vertex); + // starting a new face + ENSURE(sliceOp.thisFaceNewVertexIdx == NO_VERTEX); - firstInFace = m_Faces[i]; - startInResultFaceArray = result.m_Faces.size(); + currentFaceStartIdx = m_Faces[i]; + resultFaceStartIdx = result.m_Faces.size(); continue; } - size_t prev = m_Faces[i-1]; - size_t cur = m_Faces[i]; + size_t prevIdx = m_Faces[i-1]; // index of previous vertex in this face list + size_t curIdx = m_Faces[i]; // index of current vertex in this face list - if (si.v[prev].res == no_vertex) + if (sliceOp.ovInfo[prevIdx].resIdx == NO_VERTEX) { - if (si.v[cur].res != no_vertex) + // previous face vertex got sliced away by the plane; see if the edge (prev,current) crosses the slicing plane + if (sliceOp.ovInfo[curIdx].resIdx != NO_VERTEX) { - // re-entering the front side of the plane - result.m_Faces.push_back(Helper::SliceNewVertex(si, prev, cur)); - result.m_Faces.push_back(si.v[cur].res); + // re-entering the front side of the plane; insert vertex on intersection of plane and (prev,current) edge + result.m_Faces.push_back(Helper::SliceNewVertex(sliceOp, prevIdx, curIdx)); + result.m_Faces.push_back(sliceOp.ovInfo[curIdx].resIdx); } } else { - if (si.v[cur].res != no_vertex) + // previous face vertex didn't get sliced away; see if the edge (prev,current) crosses the slicing plane + if (sliceOp.ovInfo[curIdx].resIdx != NO_VERTEX) { - // perfectly normal edge - result.m_Faces.push_back(si.v[cur].res); + // perfectly normal edge; doesn't cross the plane + result.m_Faces.push_back(sliceOp.ovInfo[curIdx].resIdx); } else { - // leaving the front side of the plane - result.m_Faces.push_back(Helper::SliceNewVertex(si, prev, cur)); + // leaving the front side of the plane; insert vertex on intersection of plane and edge (prev, current) + result.m_Faces.push_back(Helper::SliceNewVertex(sliceOp, prevIdx, curIdx)); } } - if (cur == firstInFace) + // if we're back at the first vertex of the current face, then we've completed the face + if (curIdx == currentFaceStartIdx) { - if (result.m_Faces.size() > startInResultFaceArray) - result.m_Faces.push_back(result.m_Faces[startInResultFaceArray]); - firstInFace = no_vertex; // start a new face + // close the index loop + if (result.m_Faces.size() > resultFaceStartIdx) + result.m_Faces.push_back(result.m_Faces[resultFaceStartIdx]); + + currentFaceStartIdx = NO_VERTEX; // start a new face } } - ENSURE(firstInFace == no_vertex); + ENSURE(currentFaceStartIdx == NO_VERTEX); - // Create the face that lies in the slicing plane - if (si.newv.size()) + // Create the face that lies in the slicing plane. Remember, all the intersections of the slicing plane with face + // edges of the brush have been stored in sliceOp.nvInfo by the SliceNewVertex function, and refer to their direct + // neighbours in the slicing plane face using the neighbIdx1 and neighbIdx2 fields (in no consistent winding order). + + if (sliceOp.nvInfo.size()) { - size_t prev = 0; - size_t idx; + // push the starting vertex + result.m_Faces.push_back(sliceOp.nvInfo[0].resIdx); + + // At this point, there is no consistent winding order in the neighbX fields, so at each vertex we need to figure + // out whether neighb1 or neighb2 points 'onwards' along the face, according to an initially chosen winding direction. + // (or, equivalently, which one points back to the one we were just at). At each vertex, we then set neighb1 to be the + // one to point onwards, deleting any pointers which we no longer need to complete the trace. - result.m_Faces.push_back(si.newv[0].res); - idx = si.newv[0].neighb2; - si.newv[0].neighb2 = no_vertex; + size_t idx; + size_t prev = 0; + + idx = sliceOp.nvInfo[0].neighbIdx2; // pick arbitrary starting direction + sliceOp.nvInfo[0].neighbIdx2 = NO_VERTEX; while(idx != 0) { - ENSURE(idx < si.newv.size()); - if (idx >= si.newv.size()) + ENSURE(idx < sliceOp.nvInfo.size()); + if (idx >= sliceOp.nvInfo.size()) break; - if (si.newv[idx].neighb1 == prev) + if (sliceOp.nvInfo[idx].neighbIdx1 == prev) { - si.newv[idx].neighb1 = si.newv[idx].neighb2; - si.newv[idx].neighb2 = no_vertex; + // neighb1 is pointing the wrong way; we want to normalize it to point onwards in the direction + // we initially chose, so swap it with neighb2 and delete neighb2 (no longer needed) + sliceOp.nvInfo[idx].neighbIdx1 = sliceOp.nvInfo[idx].neighbIdx2; + sliceOp.nvInfo[idx].neighbIdx2 = NO_VERTEX; } else { - ENSURE(si.newv[idx].neighb2 == prev); - - si.newv[idx].neighb2 = no_vertex; + // neighb1 isn't pointing to the previous vertex, so neighb2 must be (otherwise a pair of vertices failed to + // get paired properly during face/plane slicing). + ENSURE(sliceOp.nvInfo[idx].neighbIdx2 == prev); + sliceOp.nvInfo[idx].neighbIdx2 = NO_VERTEX; } - result.m_Faces.push_back(si.newv[idx].res); + result.m_Faces.push_back(sliceOp.nvInfo[idx].resIdx); + // move to next vertex; neighb1 has been normalized to point onward prev = idx; - idx = si.newv[idx].neighb1; - si.newv[prev].neighb1 = no_vertex; + idx = sliceOp.nvInfo[idx].neighbIdx1; + sliceOp.nvInfo[prev].neighbIdx1 = NO_VERTEX; // no longer needed, we've moved on } - result.m_Faces.push_back(si.newv[0].res); + // push starting vertex again to close the shape + result.m_Faces.push_back(sliceOp.nvInfo[0].resIdx); } } + /////////////////////////////////////////////////////////////////////////////// // Intersect with frustum by repeated slicing void CBrush::Intersect(const CFrustum& frustum, CBrush& result) const @@ -287,6 +359,9 @@ void CBrush::Intersect(const CFrustum& frustum, CBrush& result) const const CBrush* prev = this; CBrush* next; + // Repeatedly slice this brush with each plane of the frustum, alternating between 'result' and 'buf' to + // save intermediate results. Set up the starting brush so that the final version always ends up in 'result'. + if (frustum.GetNumPlanes() & 1) next = &result; else @@ -303,4 +378,39 @@ void CBrush::Intersect(const CFrustum& frustum, CBrush& result) const } ENSURE(prev == &result); +} +std::vector CBrush::GetVertices() const +{ + return m_Vertices; +} + +void CBrush::GetFaces(std::vector >& out) const +{ + // split the back-to-back faces into separate face vectors, so that they're in a + // user-friendlier format than the back-to-back vertex index array + // i.e. split 'x--xy------yz----z' into 'x--x', 'y-------y', 'z---z' + + size_t faceStartIdx = 0; + while (faceStartIdx < m_Faces.size()) + { + // start new face + std::vector singleFace; + singleFace.push_back(m_Faces[faceStartIdx]); + + // step over all the values in the face until we hit the starting value again (which closes the face) + size_t j = faceStartIdx + 1; + while (j < m_Faces.size() && m_Faces[j] != m_Faces[faceStartIdx]) + { + singleFace.push_back(m_Faces[j]); + j++; + } + + // each face must be closed by the same value that started it + ENSURE(m_Faces[faceStartIdx] == m_Faces[j]); + + singleFace.push_back(m_Faces[j]); + out.push_back(singleFace); + + faceStartIdx = j + 1; + } } \ No newline at end of file diff --git a/source/maths/Brush.h b/source/maths/Brush.h index bc87383a18..34e8aff6fb 100644 --- a/source/maths/Brush.h +++ b/source/maths/Brush.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2012 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -34,6 +34,8 @@ class CPlane; */ class CBrush { + friend class TestBrush; + public: CBrush() { } @@ -59,8 +61,9 @@ public: void Bounds(CBoundingBoxAligned& result) const; /** - * Slice: Cut the object along the given plane, resulting in a smaller (or even empty) - * brush representing the part of the object that lies in front of the plane. + * Slice: Cut the object along the given plane, resulting in a smaller (or even empty) brush representing + * the part of the object that lies in front of the plane (as defined by the positive direction of its + * normal vector). * * @param plane the slicing plane * @param result the resulting brush is stored here @@ -76,12 +79,34 @@ public: void Intersect(const CFrustum& frustum, CBrush& result) const; private: - static const size_t no_vertex = ~0u; + + /** + * Returns a copy of the vertices in this brush. Intended for testing purposes; you should not need to use + * this method directly. + */ + std::vector GetVertices() const; + + /** + * Writes a vector of the faces in this brush to @p out. Each face is itself a vector, listing the vertex indices + * that make up the face, starting and ending with the same index. Intended for testing purposes; you should not + * need to use this method directly. + */ + void GetFaces(std::vector >& out) const; + +private: + static const size_t NO_VERTEX = ~0u; typedef std::vector Vertices; typedef std::vector FaceIndices; + /// Collection of unique vertices that make up this shape. Vertices m_Vertices; + + /** + * Holds the face definitions of this brush. Each face is a sequence of indices into m_Vertices that starts and ends with + * the same vertex index, completing a loop through all the vertices that make up the face. This vector holds all the face + * sequences back-to-back, thus looking something like 'x---xy--------yz--z' in the general case. + */ FaceIndices m_Faces; struct Helper; diff --git a/source/maths/tests/test_Bound.h b/source/maths/tests/test_Bound.h index 54d3c7397f..282f54f331 100644 --- a/source/maths/tests/test_Bound.h +++ b/source/maths/tests/test_Bound.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2012 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -18,11 +18,12 @@ #include "lib/self_test.h" #include "maths/BoundingBoxAligned.h" +#include "maths/BoundingBoxOriented.h" class TestBound : public CxxTest::TestSuite { public: - void test_empty() + void test_empty_aabb() { CBoundingBoxAligned bound; TS_ASSERT(bound.IsEmpty()); @@ -32,6 +33,19 @@ public: TS_ASSERT(bound.IsEmpty()); } + void test_empty_obb() + { + CBoundingBoxOriented bound; + TS_ASSERT(bound.IsEmpty()); + bound.m_Basis[0] = CVector3D(1,0,0); + bound.m_Basis[1] = CVector3D(0,1,0); + bound.m_Basis[2] = CVector3D(0,0,1); + bound.m_HalfSizes = CVector3D(1,2,3); + TS_ASSERT(!bound.IsEmpty()); + bound.SetEmpty(); + TS_ASSERT(bound.IsEmpty()); + } + void test_extend_vector() { CBoundingBoxAligned bound; diff --git a/source/maths/tests/test_Brush.h b/source/maths/tests/test_Brush.h new file mode 100644 index 0000000000..4153fe5e4e --- /dev/null +++ b/source/maths/tests/test_Brush.h @@ -0,0 +1,183 @@ +/* Copyright (C) 2012 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "lib/self_test.h" + +#include "maths/Brush.h" +#include "maths/BoundingBoxAligned.h" +#include "graphics/Frustum.h" + +class TestBrush : public CxxTest::TestSuite +{ +public: + void setUp() + { + CxxTest::setAbortTestOnFail(true); + } + + void tearDown() + { + + } + + void test_slice_empty_brush() + { + // verifies that the result of slicing an empty bound with a plane yields an empty bound + CBrush brush; + CPlane plane(CVector4D(0, 0, -1, 0.5f)); // can be anything, really + + CBrush result; + brush.Slice(plane, result); + TS_ASSERT(brush.IsEmpty()); + } + + void test_slice_plane_simple() + { + // slice a 1x1x1 cube vertically down the middle at z = 0.5, with the plane normal pointing towards the negative + // end of the Z axis (i.e., anything with Z lower than 0.5 is considered 'in front of' the plane and is kept) + CPlane plane(CVector4D(0, 0, -1, 0.5f)); + CBrush brush(CBoundingBoxAligned(CVector3D(0,0,0), CVector3D(1,1,1))); + + CBrush result; + brush.Slice(plane, result); + + // verify that the resulting brush consists of exactly our 8 expected, unique vertices + TS_ASSERT_EQUALS(8, result.GetVertices().size()); + size_t LBF = GetUniqueVertexIndex(result, CVector3D(0, 0, 0)); // left-bottom-front <=> XYZ + size_t RBF = GetUniqueVertexIndex(result, CVector3D(1, 0, 0)); // right-bottom-front + size_t RBB = GetUniqueVertexIndex(result, CVector3D(1, 0, 0.5f)); // right-bottom-back + size_t LBB = GetUniqueVertexIndex(result, CVector3D(0, 0, 0.5f)); // etc. + size_t LTF = GetUniqueVertexIndex(result, CVector3D(0, 1, 0)); + size_t RTF = GetUniqueVertexIndex(result, CVector3D(1, 1, 0)); + size_t RTB = GetUniqueVertexIndex(result, CVector3D(1, 1, 0.5f)); + size_t LTB = GetUniqueVertexIndex(result, CVector3D(0, 1, 0.5f)); + + // verify that the brush contains the six expected planes (one of which is the slicing plane) + VerifyFacePresent(result, 5, LBF, RBF, RBB, LBB, LBF); // bottom face + VerifyFacePresent(result, 5, LTF, RTF, RTB, LTB, LTF); // top face + VerifyFacePresent(result, 5, LBF, LBB, LTB, LTF, LBF); // left face + VerifyFacePresent(result, 5, RBF, RBB, RTB, RTF, RBF); // right face + VerifyFacePresent(result, 5, LBF, RBF, RTF, LTF, LBF); // front face + VerifyFacePresent(result, 5, LBB, RBB, RTB, LTB, LBB); // back face + } + + void test_slice_plane_behind_brush() + { + // slice the (0,0,0) to (1,1,1) cube by the plane z = 1.5, with the plane normal pointing towards the negative + // end of the Z axis (i.e. the entire cube is 'in front of' the plane and should be kept) + CPlane plane(CVector4D(0, 0, -1, 1.5f)); + CBrush brush(CBoundingBoxAligned(CVector3D(0,0,0), CVector3D(1,1,1))); + + CBrush result; + brush.Slice(plane, result); + + // verify that the resulting brush consists of exactly our 8 expected, unique vertices + TS_ASSERT_EQUALS(8, result.GetVertices().size()); + size_t LBF = GetUniqueVertexIndex(result, CVector3D(0, 0, 0)); // left-bottom-front <=> XYZ + size_t RBF = GetUniqueVertexIndex(result, CVector3D(1, 0, 0)); // right-bottom-front + size_t RBB = GetUniqueVertexIndex(result, CVector3D(1, 0, 1)); // right-bottom-back + size_t LBB = GetUniqueVertexIndex(result, CVector3D(0, 0, 1)); // etc. + size_t LTF = GetUniqueVertexIndex(result, CVector3D(0, 1, 0)); + size_t RTF = GetUniqueVertexIndex(result, CVector3D(1, 1, 0)); + size_t RTB = GetUniqueVertexIndex(result, CVector3D(1, 1, 1)); + size_t LTB = GetUniqueVertexIndex(result, CVector3D(0, 1, 1)); + + // verify that the brush contains the six expected planes (one of which is the slicing plane) + VerifyFacePresent(result, 5, LBF, RBF, RBB, LBB, LBF); // bottom face + VerifyFacePresent(result, 5, LTF, RTF, RTB, LTB, LTF); // top face + VerifyFacePresent(result, 5, LBF, LBB, LTB, LTF, LBF); // left face + VerifyFacePresent(result, 5, RBF, RBB, RTB, RTF, RBF); // right face + VerifyFacePresent(result, 5, LBF, RBF, RTF, LTF, LBF); // front face + VerifyFacePresent(result, 5, LBB, RBB, RTB, LTB, LBB); // back face + } + + void test_slice_plane_in_front_of_brush() + { + // slices the (0,0,0) to (1,1,1) cube by the plane z = -0.5, with the plane normal pointing towards the negative + // end of the Z axis (i.e. the entire cube is 'behind' the plane and should be cut away) + CPlane plane(CVector4D(0, 0, -1, -0.5f)); + CBrush brush(CBoundingBoxAligned(CVector3D(0,0,0), CVector3D(1,1,1))); + + CBrush result; + brush.Slice(plane, result); + + TS_ASSERT_EQUALS(0, result.GetVertices().size()); + + std::vector > faces; + result.GetFaces(faces); + + TS_ASSERT_EQUALS(0, faces.size()); + } + +private: + size_t GetUniqueVertexIndex(const CBrush& brush, const CVector3D& vertex, float eps = 1e-6f) + { + std::vector vertices = brush.GetVertices(); + + for (size_t i = 0; i < vertices.size(); ++i) + { + const CVector3D& v = vertices[i]; + if (fabs(v.X - vertex.X) < eps + && fabs(v.Y - vertex.Y) < eps + && fabs(v.Z - vertex.Z) < eps) + return i; + } + + TS_FAIL("Vertex not found in brush"); + return ~0u; + } + + void VerifyFacePresent(const CBrush& brush, int count, ...) + { + std::vector face; + + va_list args; + va_start(args, count); + for (int x = 0; x < count; ++x) + face.push_back(va_arg(args, size_t)); + va_end(args); + + if (face.size() == 0) + return; + + std::vector > faces; + brush.GetFaces(faces); + + // the brush is free to use any starting vertex along the face, and to use any winding order, so have 'face' + // cycle through various starting values and see if any of them (or their reverse) matches one found in the brush. + + for (size_t c = 0; c < face.size() - 1; ++c) + { + std::vector >::iterator it1 = std::find(faces.begin(), faces.end(), face); + if (it1 != faces.end()) + return; + + // no match, try the reverse + std::vector faceReverse = face; + std::reverse(faceReverse.begin(), faceReverse.end()); + std::vector >::iterator it2 = std::find(faces.begin(), faces.end(), faceReverse); + if (it2 != faces.end()) + return; + + // no match, cycle it + face.erase(face.begin()); + face.push_back(face[0]); + } + + TS_FAIL("Face not found in brush"); + } +}; diff --git a/source/renderer/ShadowMap.cpp b/source/renderer/ShadowMap.cpp index 72d9d5590f..84eea48d15 100644 --- a/source/renderer/ShadowMap.cpp +++ b/source/renderer/ShadowMap.cpp @@ -224,11 +224,19 @@ void ShadowMap::AddShadowedBound(const CBoundingBoxAligned& bounds) void ShadowMapInternals::CalcShadowMatrices() { CRenderer& renderer = g_Renderer; - float minZ = ShadowBound[0].Z; ShadowBound.IntersectFrustumConservative(LightspaceCamera.GetFrustum()); + // ShadowBound might have been empty to begin with, producing an empty result + if (ShadowBound.IsEmpty()) + { + // no-op + LightProjection.SetIdentity(); + TextureMatrix = LightTransform; + return; + } + // round off the shadow boundaries to sane increments to help reduce swim effect float boundInc = 16.0f; ShadowBound[0].X = floor(ShadowBound[0].X / boundInc) * boundInc; @@ -270,14 +278,14 @@ void ShadowMapInternals::CalcShadowMatrices() LightProjection._34 = shift.Z * scale.Z + renderer.m_ShadowZBias; LightProjection._44 = 1.0; - // Calculate texture matrix by creating the clip space to texture coordinate matrix // and then concatenating all matrices that have been calculated so far - CMatrix3D lightToTex; + float texscalex = scale.X * 0.5f * (float)EffectiveWidth / (float)Width; float texscaley = scale.Y * 0.5f * (float)EffectiveHeight / (float)Height; float texscalez = scale.Z * 0.5f; + CMatrix3D lightToTex; lightToTex.SetZero(); lightToTex._11 = texscalex; lightToTex._14 = (offsetX - ShadowBound[0].X) * texscalex;