abstraction

This commit is contained in:
Irlan 2018-04-29 00:06:24 -03:00
parent 16ea548248
commit 80ac01f13d
3 changed files with 301 additions and 297 deletions

View File

@ -21,6 +21,8 @@
#include <bounce/common/geometry.h> #include <bounce/common/geometry.h>
#define B3_NULL_U32 B3_MAX_U32
template<class T> template<class T>
struct qhList struct qhList
{ {
@ -31,8 +33,8 @@ struct qhList
u32 count; u32 count;
}; };
struct qhHalfEdge;
struct qhVertex; struct qhVertex;
struct qhHalfEdge;
enum qhFaceMark enum qhFaceMark
{ {
@ -53,13 +55,6 @@ struct qhFace
b3Plane plane; b3Plane plane;
qhFaceMark mark; qhFaceMark mark;
u32 GetVertexCount() const;
u32 GetEdgeCount() const;
qhHalfEdge* FindHalfEdge(const qhVertex* v1, const qhVertex* v2) const;
void ComputeCenterAndPlane();
// //
qhFace* freeNext; qhFace* freeNext;
@ -70,6 +65,7 @@ struct qhHalfEdge
{ {
qhHalfEdge* prev; qhHalfEdge* prev;
qhHalfEdge* next; qhHalfEdge* next;
qhHalfEdge* twin; qhHalfEdge* twin;
qhFace* face; qhFace* face;
@ -109,15 +105,15 @@ public:
// Use qhGetBufferSize to get the buffer size given the number of points. // Use qhGetBufferSize to get the buffer size given the number of points.
void Construct(void* buffer, const b3Vec3* vertices, u32 vertexCount); void Construct(void* buffer, const b3Vec3* vertices, u32 vertexCount);
// Get the number of iterations this algorithm ran.
u32 GetIterations() const;
// Get the list of vertices in this convex hull. // Get the list of vertices in this convex hull.
const qhList<qhVertex>& GetVertexList() const; const qhList<qhVertex>& GetVertexList() const;
// Get the list of faces in this convex hull. // Get the list of faces in this convex hull.
const qhList<qhFace>& GetFaceList() const; const qhList<qhFace>& GetFaceList() const;
// Get the number of iterations this algorithm ran.
u32 GetIterations() const;
// Validate this hull. // Validate this hull.
void Validate() const; void Validate() const;
void Validate(const qhFace* face) const; void Validate(const qhFace* face) const;
@ -126,25 +122,34 @@ public:
// Draw this hull. // Draw this hull.
void Draw() const; void Draw() const;
private: private:
bool BuildInitialHull(const b3Vec3* vertices, u32 count); //
qhVertex* AddVertex(const b3Vec3& position); qhVertex* AddVertex(const b3Vec3& position);
qhFace* RemoveEdge(qhHalfEdge* edge);
qhFace* AddFace(qhVertex* v1, qhVertex* v2, qhVertex* v3); qhFace* AddFace(qhVertex* v1, qhVertex* v2, qhVertex* v3);
qhFace* RemoveFace(qhFace* face); qhFace* RemoveFace(qhFace* face);
qhHalfEdge* FindHalfEdge(const qhVertex* v1, const qhVertex* v2) const;
//
bool BuildInitialHull(const b3Vec3* vertices, u32 count);
qhVertex* FindEyeVertex() const; qhVertex* FindEyeVertex() const;
void AddEyeVertex(qhVertex* eye); void AddEyeVertex(qhVertex* eye);
void FindHorizon(qhVertex* eye); void FindHorizon(qhVertex* eye);
void AddNewFaces(qhVertex* eye); void AddNewFaces(qhVertex* eye);
qhFace* AddNewFace(qhVertex* v1, qhVertex* v2, qhVertex* v3);
void MergeFaces(); void MergeFaces();
bool MergeFace(qhFace* face); bool MergeFace(qhFace* face);
qhHalfEdge* FindHalfEdge(const qhVertex* v1, const qhVertex* v2) const; // List of vertices
qhList<qhVertex> m_vertexList;
// List of faces
qhList<qhFace> m_faceList;
// Coplanarity tolerance // Coplanarity tolerance
float32 m_tolerance; float32 m_tolerance;
@ -152,12 +157,6 @@ private:
// Number of Quickhull iterations // Number of Quickhull iterations
u32 m_iterations; u32 m_iterations;
// List of vertices
qhList<qhVertex> m_vertexList;
// List of faces
qhList<qhFace> m_faceList;
// Memory // Memory
qhVertex* AllocateVertex(); qhVertex* AllocateVertex();
void FreeVertex(qhVertex* p); void FreeVertex(qhVertex* p);
@ -174,7 +173,11 @@ private:
qhHalfEdge** m_horizon; qhHalfEdge** m_horizon;
u32 m_horizonCount; u32 m_horizonCount;
qhVertex** m_horizonVertices;
qhVertex** m_conflictVertices;
u32 m_conflictCount;
qhFace** m_newFaces; qhFace** m_newFaces;
u32 m_newFaceCount; u32 m_newFaceCount;
}; };

View File

@ -1,6 +1,6 @@
// qhHull.h // qhHull.h
// Lists // qhList
template<class T> template<class T>
inline void qhList<T>::PushFront(T* link) inline void qhList<T>::PushFront(T* link)
@ -40,83 +40,6 @@ inline T* qhList<T>::Remove(T* link)
return next; return next;
} }
// qhFace
inline u32 qhFace::GetVertexCount() const
{
u32 count = 0;
qhHalfEdge* e = edge;
do
{
++count;
e = e->next;
} while (e != edge);
return count;
}
inline u32 qhFace::GetEdgeCount() const
{
u32 count = 0;
qhHalfEdge* e = edge;
do
{
++count;
e = e->next;
} while (e != edge);
return count;
}
inline qhHalfEdge* qhFace::FindHalfEdge(const qhVertex* v1, const qhVertex* v2) const
{
qhHalfEdge* e = edge;
do
{
if (e->tail == v1 && e->next->tail == v2)
{
return e;
}
e = e->next;
} while (e != edge);
return NULL;
}
static inline b3Vec3 b3Newell(const b3Vec3& a, const b3Vec3& b)
{
return b3Vec3((a.y - b.y) * (a.z + b.z), (a.z - b.z) * (a.x + b.x), (a.x - b.x) * (a.y + b.y));
}
inline void qhFace::ComputeCenterAndPlane()
{
b3Vec3 c;
c.SetZero();
b3Vec3 n;
n.SetZero();
u32 count = 0;
qhHalfEdge* e = edge;
do
{
b3Vec3 v1 = e->tail->position;
b3Vec3 v2 = e->next->tail->position;
n += b3Newell(v1, v2);
c += v1;
++count;
e = e->next;
} while (e != edge);
B3_ASSERT(count > 0);
c /= float32(count);
n.Normalize();
plane.normal = n;
plane.offset = b3Dot(n, c);
center = c;
}
// qhHull // qhHull
// Given a number of points return the required memory size in // Given a number of points return the required memory size in
@ -135,13 +58,16 @@ inline u32 qhGetBufferSize(u32 pointCount)
size += HE * sizeof(qhHalfEdge); size += HE * sizeof(qhHalfEdge);
size += F * sizeof(qhFace); size += F * sizeof(qhFace);
// Extra // Horizon
size += HE * sizeof(qhHalfEdge);
size += F * sizeof(qhFace);
// Horizon
size += HE * sizeof(qhHalfEdge*); size += HE * sizeof(qhHalfEdge*);
// Saved horizon vertices
// One vertex per horizon edge
size += HE * sizeof(qhVertex*);
// Saved conflict vertices
size += V * sizeof(qhVertex*);
// New Faces // New Faces
// One face per horizon edge // One face per horizon edge
size += HE * sizeof(qhFace*); size += HE * sizeof(qhFace*);
@ -149,11 +75,6 @@ inline u32 qhGetBufferSize(u32 pointCount)
return size; return size;
} }
inline u32 qhHull::GetIterations() const
{
return m_iterations;
}
inline const qhList<qhVertex>& qhHull::GetVertexList() const inline const qhList<qhVertex>& qhHull::GetVertexList() const
{ {
return m_vertexList; return m_vertexList;
@ -164,6 +85,11 @@ inline const qhList<qhFace>& qhHull::GetFaceList() const
return m_faceList; return m_faceList;
} }
inline u32 qhHull::GetIterations() const
{
return m_iterations;
}
inline qhVertex* qhHull::AllocateVertex() inline qhVertex* qhHull::AllocateVertex()
{ {
qhVertex* v = m_freeVertices; qhVertex* v = m_freeVertices;
@ -219,11 +145,21 @@ inline qhHalfEdge* qhHull::FindHalfEdge(const qhVertex* v1, const qhVertex* v2)
{ {
for (qhFace* face = m_faceList.head; face != NULL; face = face->next) for (qhFace* face = m_faceList.head; face != NULL; face = face->next)
{ {
qhHalfEdge* e = face->FindHalfEdge(v1, v2); qhHalfEdge* e = face->edge;
if (e) do
{ {
return e; if (e->tail == v1 && e->next->tail == v2)
} {
return e;
}
if (e->tail == v2 && e->next->tail == v1)
{
return e->twin;
}
e = e->next;
} while (e != face->edge);
} }
return NULL; return NULL;
} }

View File

@ -102,7 +102,12 @@ void qhHull::Construct(void* memory, const b3Vec3* vs, u32 count)
m_horizon = (qhHalfEdge**)((u8*)faces + F * sizeof(qhFace*)); m_horizon = (qhHalfEdge**)((u8*)faces + F * sizeof(qhFace*));
m_horizonCount = 0; m_horizonCount = 0;
m_newFaces = (qhFace**)((u8*)m_horizon + HE * sizeof(qhHalfEdge*)); m_horizonVertices = (qhVertex**)((u8*)m_horizon + HE * sizeof(qhHalfEdge*));
m_conflictVertices = (qhVertex**)((u8*)m_horizonVertices + HE * sizeof(qhVertex*));
m_conflictCount = 0;
m_newFaces = (qhFace**)((u8*)m_conflictVertices + V * sizeof(qhVertex*));
m_newFaceCount = 0; m_newFaceCount = 0;
m_vertexList.head = NULL; m_vertexList.head = NULL;
@ -110,7 +115,7 @@ void qhHull::Construct(void* memory, const b3Vec3* vs, u32 count)
m_faceList.head = NULL; m_faceList.head = NULL;
m_faceList.count = 0; m_faceList.count = 0;
m_iterations = 0; m_iterations = 0;
if (!BuildInitialHull(vs, count)) if (!BuildInitialHull(vs, count))
@ -426,105 +431,97 @@ void qhHull::AddNewFaces(qhVertex* eye)
B3_ASSERT(e1->tail == e2->tail); B3_ASSERT(e1->tail == e2->tail);
} }
// Save horizon vertices
for (u32 i = 0; i < m_horizonCount; ++i)
{
qhHalfEdge* edge = m_horizon[i];
m_horizonVertices[i] = edge->tail;
}
// Remove the eye vertex from the conflict list // Remove the eye vertex from the conflict list
b3Vec3 eyePosition = eye->position; b3Vec3 eyePosition = eye->position;
eye->conflictFace->conflictList.Remove(eye); eye->conflictFace->conflictList.Remove(eye);
FreeVertex(eye); FreeVertex(eye);
// Add the eye point to the hull // Add the eye point to the hull
qhVertex* v1 = AddVertex(eyePosition); qhVertex* v1 = AddVertex(eyePosition);
// Create new faces // Save conflict vertices
m_newFaceCount = 0; m_conflictCount = 0;
for (u32 i = 0; i < m_horizonCount; ++i)
{
qhHalfEdge* edge = m_horizon[i];
qhVertex* v2 = edge->tail; // Remove visible faces
qhVertex* v3 = edge->twin->tail;
qhFace* face = AddNewFace(v1, v2, v3);
}
// Remove obsolete faces
qhFace* f = m_faceList.head; qhFace* f = m_faceList.head;
while (f) while (f)
{ {
// Invisible faces are maintained. // Skip invisible faces.
if (f->mark == qhFaceMark::e_invisible) if (f->mark == qhFaceMark::e_invisible)
{ {
f = f->next; f = f->next;
continue; continue;
} }
// Move the orphaned vertices into the new faces
// Also remove internal points
qhVertex* v = f->conflictList.head; qhVertex* v = f->conflictList.head;
while (v) while (v)
{ {
b3Vec3 p = v->position; // Save vertex
m_conflictVertices[m_conflictCount++] = v;
float32 max = m_tolerance;
qhFace* maxFace = NULL; // Remove vertex from face
v->conflictFace = NULL;
for (u32 i = 0; i < m_newFaceCount; ++i) v = f->conflictList.Remove(v);
{
qhFace* newFace = m_newFaces[i];
float32 d = b3Distance(p, newFace->plane);
if (d > max)
{
max = d;
maxFace = newFace;
}
}
if (maxFace)
{
qhVertex* v0 = v;
v->conflictFace = NULL;
v = f->conflictList.Remove(v);
maxFace->conflictList.PushFront(v0);
v0->conflictFace = maxFace;
}
else
{
qhVertex* v0 = v;
v->conflictFace = NULL;
v = f->conflictList.Remove(v);
FreeVertex(v0);
}
} }
// Remove the face // Remove face
f = RemoveFace(f); f = RemoveFace(f);
} }
// Add the new faces to the hull // Add new faces to the hull
for (u32 i = 0; i < m_newFaceCount; ++i) m_newFaceCount = 0;
for (u32 i = 0; i < m_horizonCount; ++i)
{ {
qhFace* face = m_newFaces[i]; u32 j = i + 1 < m_horizonCount ? i + 1 : 0;
qhVertex* v2 = m_horizonVertices[i];
qhVertex* v3 = m_horizonVertices[j];
qhHalfEdge* begin = face->edge; m_newFaces[m_newFaceCount++] = AddFace(v1, v2, v3);
qhHalfEdge* edge = begin; }
do
// Move the orphaned conflict vertices into the new faces
// Remove internal conflict vertices
for (u32 i = 0; i < m_conflictCount; ++i)
{
qhVertex* v = m_conflictVertices[i];
b3Vec3 p = v->position;
float32 d0 = m_tolerance;
qhFace* f0 = NULL;
for (u32 j = 0; j < m_newFaceCount; ++j)
{ {
qhVertex* v1 = edge->tail; qhFace* nf = m_newFaces[j];
float32 d = b3Distance(p, nf->plane);
qhHalfEdge* next = edge->next; if (d > d0)
qhVertex* v2 = next->tail;
edge->twin = FindHalfEdge(v2, v1);
if (edge->twin)
{ {
edge->twin->twin = edge; d0 = d;
f0 = nf;
} }
}
edge = next; if (f0)
} while (edge != begin); {
// Add conflict vertex to the new face
// Add f0->conflictList.PushFront(v);
m_faceList.PushFront(face); v->conflictFace = f0;
}
else
{
// Remove conflict vertex
FreeVertex(v);
}
} }
} }
@ -535,10 +532,94 @@ qhVertex* qhHull::AddVertex(const b3Vec3& position)
v->conflictFace = NULL; v->conflictFace = NULL;
m_vertexList.PushFront(v); m_vertexList.PushFront(v);
return v; return v;
} }
static inline b3Vec3 b3Newell(const b3Vec3& a, const b3Vec3& b)
{
return b3Vec3((a.y - b.y) * (a.z + b.z), (a.z - b.z) * (a.x + b.x), (a.x - b.x) * (a.y + b.y));
}
static inline void b3ComputePlane(const qhFace* face, b3Plane& plane, b3Vec3& center)
{
b3Vec3 n;
n.SetZero();
b3Vec3 c;
c.SetZero();
u32 count = 0;
qhHalfEdge* e = face->edge;
do
{
b3Vec3 v1 = e->tail->position;
b3Vec3 v2 = e->next->tail->position;
n += b3Newell(v1, v2);
c += v1;
++count;
e = e->next;
} while (e != face->edge);
B3_ASSERT(count > 0);
c /= float32(count);
n.Normalize();
plane.normal = n;
plane.offset = b3Dot(n, c);
center = c;
}
qhFace* qhHull::RemoveEdge(qhHalfEdge* e)
{
qhFace* leftFace = e->twin->face;
qhFace* rightFace = e->face;
// Move left vertices into right
qhVertex* v = leftFace->conflictList.head;
while (v)
{
qhVertex* v0 = v;
v = leftFace->conflictList.Remove(v);
rightFace->conflictList.PushFront(v0);
v0->conflictFace = rightFace;
}
// Set right face to reference a non-deleted edge
B3_ASSERT(e->face == rightFace);
rightFace->edge = e->prev;
// Absorb face
qhHalfEdge* te = e->twin;
do
{
te->face = rightFace;
te = te->next;
} while (te != e->twin);
// Link edges
e->prev->next = e->twin->next;
e->next->prev = e->twin->prev;
e->twin->prev->next = e->next;
e->twin->next->prev = e->prev;
FreeEdge(e->twin);
FreeEdge(e);
m_faceList.Remove(leftFace);
FreeFace(leftFace);
// Compute face center and plane
b3ComputePlane(rightFace, rightFace->plane, rightFace->center);
// Validate
Validate(rightFace);
return rightFace;
}
qhFace* qhHull::AddFace(qhVertex* v1, qhVertex* v2, qhVertex* v3) qhFace* qhHull::AddFace(qhVertex* v1, qhVertex* v2, qhVertex* v3)
{ {
qhFace* face = AllocateFace(); qhFace* face = AllocateFace();
@ -547,49 +628,97 @@ qhFace* qhHull::AddFace(qhVertex* v1, qhVertex* v2, qhVertex* v3)
if (e1 == NULL) if (e1 == NULL)
{ {
e1 = AllocateEdge(); e1 = AllocateEdge();
e1->face = NULL;
e1->tail = NULL;
e1->twin = AllocateEdge();
e1->twin->face = NULL;
e1->twin->tail = NULL;
e1->twin->twin = e1;
}
if (e1->tail == NULL)
{
e1->tail = v1;
}
if (e1->face == NULL)
{
e1->face = face; e1->face = face;
} }
if (e1->twin->tail == NULL)
{
e1->twin->tail = v2;
}
qhHalfEdge* e2 = FindHalfEdge(v2, v3); qhHalfEdge* e2 = FindHalfEdge(v2, v3);
if (e2 == NULL) if (e2 == NULL)
{ {
e2 = AllocateEdge(); e2 = AllocateEdge();
e2->face = NULL;
e2->tail = NULL;
e2->twin = AllocateEdge();
e2->twin->face = NULL;
e2->twin->tail = NULL;
e2->twin->twin = e2;
}
if (e2->face == NULL)
{
e2->face = face; e2->face = face;
} }
if (e2->tail == NULL)
{
e2->tail = v2;
}
if (e2->twin->tail == NULL)
{
e2->twin->tail = v3;
}
qhHalfEdge* e3 = FindHalfEdge(v3, v1); qhHalfEdge* e3 = FindHalfEdge(v3, v1);
if (e3 == NULL) if (e3 == NULL)
{ {
e3 = AllocateEdge(); e3 = AllocateEdge();
e3->face = NULL;
e3->tail = NULL;
e3->twin = AllocateEdge();
e3->twin->face = NULL;
e3->twin->tail = NULL;
e3->twin->twin = e3;
}
if (e3->face == NULL)
{
e3->face = face; e3->face = face;
} }
e1->tail = v1; if (e3->tail == NULL)
{
e3->tail = v3;
}
if (e3->twin->tail == NULL)
{
e3->twin->tail = v1;
}
e1->prev = e3; e1->prev = e3;
e1->next = e2; e1->next = e2;
e1->twin = FindHalfEdge(v2, v1);
if (e1->twin)
{
e1->twin->twin = e1;
}
e2->tail = v2;
e2->prev = e1; e2->prev = e1;
e2->next = e3; e2->next = e3;
e2->twin = FindHalfEdge(v3, v2);
if (e2->twin)
{
e2->twin->twin = e2;
}
e3->tail = v3;
e3->prev = e2; e3->prev = e2;
e3->next = e1; e3->next = e1;
e3->twin = FindHalfEdge(v1, v3);
if (e3->twin)
{
e3->twin->twin = e3;
}
face->edge = e1; face->edge = e1;
face->center = (v1->position + v2->position + v3->position) / 3.0f; face->center = (v1->position + v2->position + v3->position) / 3.0f;
@ -606,20 +735,35 @@ qhFace* qhHull::RemoveFace(qhFace* face)
qhHalfEdge* e = face->edge; qhHalfEdge* e = face->edge;
do do
{ {
qhHalfEdge* twin = e->twin; qhHalfEdge* e0 = e;
e = e->next;
// Remove half-edge and its twin if twin face does not exist qhHalfEdge* twin = e0->twin;
if (twin->face == NULL || twin->face == e->face)
// Is the edge a boundary edge?
if (twin->face == NULL)
{ {
e->twin = NULL; e0->twin = NULL;
e0->tail = NULL;
e0->face = NULL;
e0->next = NULL;
e0->prev = NULL;
twin->twin = NULL;
FreeEdge(twin); // Free both half-edges if edge is a boundary.
qhHalfEdge* e0 = e;
e = e->next;
FreeEdge(e0); FreeEdge(e0);
FreeEdge(twin);
} }
else
{
e0->tail = NULL;
e0->face = NULL;
e0->next = NULL;
e0->prev = NULL;
}
} while (e != face->edge); } while (e != face->edge);
// Remove face // Remove face
@ -628,46 +772,6 @@ qhFace* qhHull::RemoveFace(qhFace* face)
return nextFace; return nextFace;
} }
qhFace* qhHull::AddNewFace(qhVertex* v1, qhVertex* v2, qhVertex* v3)
{
qhFace* face = AllocateFace();
qhHalfEdge* e1 = AllocateEdge();
e1->face = face;
e1->twin = NULL;
e1->tail = v1;
qhHalfEdge* e2 = AllocateEdge();
e2->face = face;
e2->twin = NULL;
e2->tail = v2;
qhHalfEdge* e3 = AllocateEdge();
e3->face = face;
e3->twin = NULL;
e3->tail = v3;
e1->prev = e3;
e1->next = e2;
e2->prev = e1;
e2->next = e3;
e3->prev = e2;
e3->next = e1;
face->edge = e1;
face->center = (v1->position + v2->position + v3->position) / 3.0f;
face->plane = b3Plane(v1->position, v2->position, v3->position);
face->prev = NULL;
face->next = NULL;
m_newFaces[m_newFaceCount++] = face;
return face;
}
bool qhHull::MergeFace(qhFace* rightFace) bool qhHull::MergeFace(qhFace* rightFace)
{ {
qhHalfEdge* e = rightFace->edge; qhHalfEdge* e = rightFace->edge;
@ -694,46 +798,7 @@ bool qhHull::MergeFace(qhFace* rightFace)
else else
{ {
// Concave or coplanar // Concave or coplanar
RemoveEdge(e);
// Move left vertices into right
qhVertex* v = leftFace->conflictList.head;
while (v)
{
qhVertex* v0 = v;
v = leftFace->conflictList.Remove(v);
rightFace->conflictList.PushFront(v0);
v0->conflictFace = rightFace;
}
// Set right face to reference a non-deleted edge
B3_ASSERT(e->face == rightFace);
rightFace->edge = e->prev;
// Absorb face
qhHalfEdge* te = e->twin;
do
{
te->face = rightFace;
te = te->next;
} while (te != e->twin);
// Link edges
e->prev->next = e->twin->next;
e->next->prev = e->twin->prev;
e->twin->prev->next = e->next;
e->twin->next->prev = e->prev;
FreeEdge(e->twin);
FreeEdge(e);
m_faceList.Remove(leftFace);
FreeFace(leftFace);
// Compute face center and plane
rightFace->ComputeCenterAndPlane();
// Validate
Validate(rightFace);
return true; return true;
} }
@ -769,12 +834,12 @@ void qhHull::Validate(const qhHalfEdge* edge) const
B3_ASSERT(edge->tail->active == true); B3_ASSERT(edge->tail->active == true);
b3Vec3 A = edge->tail->position; b3Vec3 A = edge->tail->position;
B3_ASSERT(twin->tail->active == true); B3_ASSERT(twin->tail->active == true);
b3Vec3 B = twin->tail->position; b3Vec3 B = twin->tail->position;
B3_ASSERT(b3DistanceSquared(A, B) > B3_EPSILON * B3_EPSILON); B3_ASSERT(b3DistanceSquared(A, B) > B3_EPSILON * B3_EPSILON);
const qhHalfEdge* next = edge->next; const qhHalfEdge* next = edge->next;
B3_ASSERT(next->active == true); B3_ASSERT(next->active == true);
B3_ASSERT(twin->tail == next->tail); B3_ASSERT(twin->tail == next->tail);
@ -792,7 +857,7 @@ void qhHull::Validate(const qhHalfEdge* edge) const
void qhHull::Validate(const qhFace* face) const void qhHull::Validate(const qhFace* face) const
{ {
B3_ASSERT(face->active == true); B3_ASSERT(face->active == true);
const qhHalfEdge* begin = face->edge; const qhHalfEdge* begin = face->edge;
const qhHalfEdge* edge = begin; const qhHalfEdge* edge = begin;
do do