Skip to content

Instantly share code, notes, and snippets.

@monkstone
Created June 21, 2011 19:31
Show Gist options
  • Save monkstone/1038673 to your computer and use it in GitHub Desktop.
Save monkstone/1038673 to your computer and use it in GitHub Desktop.
Updated quickhull 3D
/**
* Copyright John E. Lloyd, 2004. All rights reserved. Permission to use,
* copy, modify and redistribute is granted, provided that this copyright
* notice is retained and the author is given credit whenever appropriate.
*
* This software is distributed "as is", without any warranty, including
* any implied warranty of merchantability or fitness for a particular
* use. The author assumes no responsibility for, and shall not be liable
* for, any special, indirect, or consequential damages, or any damages
* whatsoever, arising out of or in connection with the use of this
* software.
*/
package quickhull3d;
import java.util.*;
import java.io.*;
/**
* Computes the convex hull of a set of three dimensional points.
*
* <p>The algorithm is a three dimensional implementation of Quickhull, as
* described in Barber, Dobkin, and Huhdanpaa, <a
* href=http://citeseer.ist.psu.edu/barber96quickhull.html> ``The Quickhull
* Algorithm for Convex Hulls''</a> (ACM Transactions on Mathematical Software,
* Vol. 22, No. 4, December 1996), and has a complexity of O(n log(n)) with
* respect to the number of points. A well-known C implementation of Quickhull
* that works for arbitrary dimensions is provided by <a
* href=http://www.qhull.org>qhull</a>.
*
* <p>A hull is constructed by providing a set of points
* to either a constructor or a
* {@link #build(Point3d[]) build} method. After
* the hull is built, its vertices and faces can be retrieved
* using {@link #getVertices()
* getVertices} and {@link #getFaces() getFaces}.
* A typical usage might look like this:
* <pre>
* // x y z coordinates of 6 points
* Point3d[] points = new Point3d[]
* { new Point3d (0.0, 0.0, 0.0),
* new Point3d (1.0, 0.5, 0.0),
* new Point3d (2.0, 0.0, 0.0),
* new Point3d (0.5, 0.5, 0.5),
* new Point3d (0.0, 0.0, 2.0),
* new Point3d (0.1, 0.2, 0.3),
* new Point3d (0.0, 2.0, 0.0),
* };
*
* QuickHull3D hull = new QuickHull3D();
* hull.build (points);
*
* System.out.println ("Vertices:");
* Point3d[] vertices = hull.getVertices();
* for (int i = 0; i < vertices.length; i++)
* { Point3d pnt = vertices[i];
* System.out.println (pnt.x + " " + pnt.y + " " + pnt.z);
* }
*
* System.out.println ("Faces:");
* int[][] faceIndices = hull.getFaces();
* for (int i = 0; i < vertices.length; i++)
* { for (int k = 0; k < faceIndices[i].length; k++)
* { System.out.print (faceIndices[i][k] + " ");
* }
* System.out.println ("");
* }
* </pre>
* As a convenience, there are also {@link #build(double[]) build}
* and {@link #getVertices(double[]) getVertex} methods which
* pass point information using an array of doubles.
*
* <h3><a name=distTol>Robustness</h3> Because this algorithm uses floating
* point arithmetic, it is potentially vulnerable to errors arising from
* numerical imprecision. We address this problem in the same way as <a
* href=http://www.qhull.org>qhull</a>, by merging faces whose edges are not
* clearly convex. A face is convex if its edges are convex, and an edge is
* convex if the centroid of each adjacent plane is clearly <i>below</i> the
* plane of the other face. The centroid is considered below a plane if its
* distance to the plane is less than the negative of a {@link
* #getDistanceTolerance() distance tolerance}. This tolerance represents the
* smallest distance that can be reliably computed within the available numeric
* precision. It is normally computed automatically from the point data,
* although an application may {@link #setExplicitDistanceTolerance set this
* tolerance explicitly}.
*
* <p>Numerical problems are more likely to arise in situations where data
* points lie on or within the faces or edges of the convex hull. We have
* tested QuickHull3D for such situations by computing the convex hull of a
* random point set, then adding additional randomly chosen points which lie
* very close to the hull vertices and edges, and computing the convex
* hull again. The hull is deemed correct if {@link #check check} returns
* <code>true</code>. These tests have been successful for a large number of
* trials and so we are confident that QuickHull3D is reasonably robust.
*
* <h3>Merged Faces</h3> The merging of faces means that the faces returned by
* QuickHull3D may be convex polygons instead of triangles. If triangles are
* desired, the application may {@link #triangulate triangulate} the faces, but
* it should be noted that this may result in triangles which are very small or
* thin and hence difficult to perform reliable convexity tests on. In other
* words, triangulating a merged face is likely to restore the numerical
* problems which the merging process removed. Hence is it
* possible that, after triangulation, {@link #check check} will fail (the same
* behavior is observed with triangulated output from <a
* href=http://www.qhull.org>qhull</a>).
*
* <h3>Degenerate Input</h3>It is assumed that the input points
* are non-degenerate in that they are not coincident, colinear, or
* colplanar, and thus the convex hull has a non-zero volume.
* If the input points are detected to be degenerate within
* the {@link #getDistanceTolerance() distance tolerance}, an
* IllegalArgumentException will be thrown.
*
* @author John E. Lloyd, Fall 2004 */
public class QuickHull3D
{
/**
* Specifies that (on output) vertex indices for a face should be
* listed in clockwise order.
*/
public static final int CLOCKWISE = 0x1;
/**
* Specifies that (on output) the vertex indices for a face should be
* numbered starting from 1.
*/
public static final int INDEXED_FROM_ONE = 0x2;
/**
* Specifies that (on output) the vertex indices for a face should be
* numbered starting from 0.
*/
public static final int INDEXED_FROM_ZERO = 0x4;
/**
* Specifies that (on output) the vertex indices for a face should be
* numbered with respect to the original input points.
*/
public static final int POINT_RELATIVE = 0x8;
/**
* Specifies that the distance tolerance should be
* computed automatically from the input point data.
*/
public static final double AUTOMATIC_TOLERANCE = -1;
protected int findIndex = -1;
// estimated size of the point set
protected double charLength;
protected boolean debug = false;
protected Vertex[] pointBuffer = new Vertex[0];
protected int[] vertexPointIndices = new int[0];
private Face[] discardedFaces = new Face[3];
private Vertex[] maxVtxs = new Vertex[3];
private Vertex[] minVtxs = new Vertex[3];
protected List<Face> faces = new ArrayList<Face>(16);
protected List<HalfEdge> horizon = new ArrayList<HalfEdge>(16);
private FaceList newFaces = new FaceList();
private VertexList unclaimed = new VertexList();
private VertexList claimed = new VertexList();
protected int numVertices;
protected int numFaces;
protected int numPoints;
protected double explicitTolerance = AUTOMATIC_TOLERANCE;
protected double tolerance;
/**
* Returns true if debugging is enabled.
*
* @return true is debugging is enabled
* @see QuickHull3D#setDebug
*/
public boolean getDebug()
{
return debug;
}
/**
* Enables the printing of debugging diagnostics.
*
* @param enable if true, enables debugging
*/
public void setDebug (boolean enable)
{
debug = enable;
}
/**
* Precision of a double.
*/
static private final double DOUBLE_PREC = 2.2204460492503131e-16;
/**
* Returns the distance tolerance that was used for the most recently
* computed hull. The distance tolerance is used to determine when
* faces are unambiguously convex with respect to each other, and when
* points are unambiguously above or below a face plane, in the
* presence of <a href=#distTol>numerical imprecision</a>. Normally,
* this tolerance is computed automatically for each set of input
* points, but it can be set explicitly by the application.
*
* @return distance tolerance
* @see QuickHull3D#setExplicitDistanceTolerance
*/
public double getDistanceTolerance()
{
return tolerance;
}
/**
* Sets an explicit distance tolerance for convexity tests.
* If {@link #AUTOMATIC_TOLERANCE AUTOMATIC_TOLERANCE}
* is specified (the default), then the tolerance will be computed
* automatically from the point data.
*
* @param tol explicit tolerance
* @see #getDistanceTolerance
*/
public void setExplicitDistanceTolerance(double tol)
{
explicitTolerance = tol;
}
/**
* Returns the explicit distance tolerance.
*
* @return explicit tolerance
* @see #setExplicitDistanceTolerance
*/
public double getExplicitDistanceTolerance()
{
return explicitTolerance;
}
private void addPointToFace (Vertex vtx, Face face)
{
vtx.face = face;
if (face.outside == null)
{ claimed.add (vtx);
}
else
{ claimed.insertBefore (vtx, face.outside);
}
face.outside = vtx;
}
private void removePointFromFace (Vertex vtx, Face face)
{
if (vtx == face.outside)
{ if (vtx.next != null && vtx.next.face == face)
{ face.outside = vtx.next;
}
else
{ face.outside = null;
}
}
claimed.delete (vtx);
}
private Vertex removeAllPointsFromFace (Face face)
{
if (face.outside != null)
{
Vertex end = face.outside;
while (end.next != null && end.next.face == face)
{ end = end.next;
}
claimed.delete (face.outside, end);
end.next = null;
return face.outside;
}
else
{ return null;
}
}
/**
* Creates an empty convex hull object.
*/
public QuickHull3D ()
{
}
/**
* Creates a convex hull object and initializes it to the convex hull
* of a set of points whose coordinates are given by an
* array of doubles.
*
* @param coords x, y, and z coordinates of each input
* point. The length of this array will be three times
* the the number of input points.
* @throws IllegalArgumentException the number of input points is less
* than four, or the points appear to be coincident, colinear, or
* coplanar.
*/
public QuickHull3D (double[] coords)
throws IllegalArgumentException
{
build (coords, coords.length/3);
}
/**
* Creates a convex hull object and initializes it to the convex hull
* of a set of points.
*
* @param points input points.
* @throws IllegalArgumentException the number of input points is less
* than four, or the points appear to be coincident, colinear, or
* coplanar.
*/
public QuickHull3D (Point3d[] points)
throws IllegalArgumentException
{
build (points, points.length);
}
private HalfEdge findHalfEdge (Vertex tail, Vertex head)
{
// brute force ... OK, since setHull is not used much
for (Face face:faces)
{ HalfEdge he = face.findEdge (tail, head);
if (he != null)
{ return he;
}
}
return null;
}
protected void setHull (double[] coords, int nump,
int[][] faceIndices, int numf)
{
initBuffers (nump);
setPoints (coords, nump);
computeMaxAndMin ();
for (int i=0; i<numf; i++)
{ Face face = Face.create (pointBuffer, faceIndices[i]);
HalfEdge he = face.he0;
do
{ HalfEdge heOpp = findHalfEdge (he.head(), he.tail());
if (heOpp != null)
{ he.setOpposite (heOpp);
}
he = he.next;
}
while (he != face.he0);
faces.add (face);
}
}
private void printQhullErrors (Process proc)
throws IOException
{
boolean wrote = false;
InputStream es = proc.getErrorStream();
while (es.available() > 0)
{ System.out.write (es.read());
wrote = true;
}
if (wrote)
{ System.out.println("");
}
}
protected void setFromQhull (double[] coords, int nump,
boolean triangulate)
{
String commandStr = "./qhull i";
if (triangulate)
{ commandStr += " -Qt";
}
try
{
Process proc = Runtime.getRuntime().exec (commandStr);
PrintStream ps = new PrintStream (proc.getOutputStream());
StreamTokenizer stok =
new StreamTokenizer (
new InputStreamReader (proc.getInputStream()));
ps.println ("3 " + nump);
for (int i=0; i<nump; i++)
{ ps.println (
coords[i*3+0] + " " +
coords[i*3+1] + " " +
coords[i*3+2]);
}
ps.flush();
ps.close();
List<Integer> indexList = new ArrayList<Integer>(3);
stok.eolIsSignificant(true);
printQhullErrors (proc);
do
{ stok.nextToken();
}
while (stok.sval == null ||
!stok.sval.startsWith ("MERGEexact"));
for (int i=0; i<4; i++)
{ stok.nextToken();
}
if (stok.ttype != StreamTokenizer.TT_NUMBER)
{ System.out.println ("Expecting number of faces");
System.exit(1);
}
int numf = (int)stok.nval;
stok.nextToken(); // clear EOL
int[][] faceIndices = new int[numf][];
for (int i=0; i<numf; i++)
{ indexList.clear();
while (stok.nextToken() != StreamTokenizer.TT_EOL)
{ if (stok.ttype != StreamTokenizer.TT_NUMBER)
{ System.out.println ("Expecting face index");
System.exit(1);
}
indexList.add (0, new Integer((int)stok.nval));
}
faceIndices[i] = new int[indexList.size()];
int k = 0;
for (Integer it: indexList )
{ faceIndices[i][k++] = it.intValue();
}
}
setHull (coords, nump, faceIndices, numf);
}
catch (Exception e)
{ e.printStackTrace();
System.exit(1);
}
}
private void printPoints (PrintStream ps)
{
for (int i=0; i<numPoints; i++)
{ Point3d pnt = pointBuffer[i].pnt;
ps.println (pnt.x + ", " + pnt.y + ", " + pnt.z + ",");
}
}
/**
* Constructs the convex hull of a set of points whose
* coordinates are given by an array of doubles.
*
* @param coords x, y, and z coordinates of each input
* point. The length of this array will be three times
* the number of input points.
* @throws IllegalArgumentException the number of input points is less
* than four, or the points appear to be coincident, colinear, or
* coplanar.
*/
public void build (double[] coords)
throws IllegalArgumentException
{
build (coords, coords.length/3);
}
/**
* Constructs the convex hull of a set of points whose
* coordinates are given by an array of doubles.
*
* @param coords x, y, and z coordinates of each input
* point. The length of this array must be at least three times
* <code>nump</code>.
* @param nump number of input points
* @throws IllegalArgumentException the number of input points is less
* than four or greater than 1/3 the length of <code>coords</code>,
* or the points appear to be coincident, colinear, or
* coplanar.
*/
public void build (double[] coords, int nump)
throws IllegalArgumentException
{
if (nump < 4)
{ throw new IllegalArgumentException (
"Less than four input points specified");
}
if (coords.length/3 < nump)
{ throw new IllegalArgumentException (
"Coordinate array too small for specified number of points");
}
initBuffers (nump);
setPoints (coords, nump);
buildHull ();
}
/**
* Constructs the convex hull of a set of points.
*
* @param points input points
* @throws IllegalArgumentException the number of input points is less
* than four, or the points appear to be coincident, colinear, or
* coplanar.
*/
public void build (Point3d[] points)
throws IllegalArgumentException
{
build (points, points.length);
}
/**
* Constructs the convex hull of a set of points.
*
* @param points input points
* @param nump number of input points
* @throws IllegalArgumentException the number of input points is less
* than four or greater then the length of <code>points</code>, or the
* points appear to be coincident, colinear, or coplanar.
*/
public void build (Point3d[] points, int nump)
throws IllegalArgumentException
{
if (nump < 4)
{ throw new IllegalArgumentException (
"Less than four input points specified");
}
if (points.length < nump)
{ throw new IllegalArgumentException (
"Point array too small for specified number of points");
}
initBuffers (nump);
setPoints (points, nump);
buildHull ();
}
/**
* Triangulates any non-triangular hull faces. In some cases, due to
* precision issues, the resulting triangles may be very thin or small,
* and hence appear to be non-convex (this same limitation is present
* in <a href=http://www.qhull.org>qhull</a>).
*/
public void triangulate ()
{
double minArea = 1000*charLength*DOUBLE_PREC;
newFaces.clear();
for (Face face: faces)
{
if (face.mark == Face.VISIBLE)
{
face.triangulate (newFaces, minArea);
// splitFace (face);
}
}
for (Face face=newFaces.first(); face!=null; face=face.next)
{ faces.add (face);
}
}
// private void splitFace (Face face)
// {
// Face newFace = face.split();
// if (newFace != null)
// { newFaces.add (newFace);
// splitFace (newFace);
// splitFace (face);
// }
// }
protected void initBuffers (int nump)
{
if (pointBuffer.length < nump)
{ Vertex[] newBuffer = new Vertex[nump];
vertexPointIndices = new int[nump];
for (int i=0; i<pointBuffer.length; i++)
{ newBuffer[i] = pointBuffer[i];
}
for (int i=pointBuffer.length; i<nump; i++)
{ newBuffer[i] = new Vertex();
}
pointBuffer = newBuffer;
}
faces.clear();
claimed.clear();
numFaces = 0;
numPoints = nump;
}
protected void setPoints (double[] coords, int nump)
{
for (int i=0; i<nump; i++)
{
Vertex vtx = pointBuffer[i];
vtx.pnt.set (coords[i*3+0], coords[i*3+1], coords[i*3+2]);
vtx.index = i;
}
}
protected void setPoints (Point3d[] pnts, int nump)
{
for (int i=0; i<nump; i++)
{
Vertex vtx = pointBuffer[i];
vtx.pnt.set (pnts[i]);
vtx.index = i;
}
}
protected void computeMaxAndMin ()
{
Vector3d max = new Vector3d();
Vector3d min = new Vector3d();
for (int i=0; i<3; i++)
{ maxVtxs[i] = minVtxs[i] = pointBuffer[0];
}
max.set (pointBuffer[0].pnt);
min.set (pointBuffer[0].pnt);
for (int i=1; i<numPoints; i++)
{ Point3d pnt = pointBuffer[i].pnt;
if (pnt.x > max.x)
{ max.x = pnt.x;
maxVtxs[0] = pointBuffer[i];
}
else if (pnt.x < min.x)
{ min.x = pnt.x;
minVtxs[0] = pointBuffer[i];
}
if (pnt.y > max.y)
{ max.y = pnt.y;
maxVtxs[1] = pointBuffer[i];
}
else if (pnt.y < min.y)
{ min.y = pnt.y;
minVtxs[1] = pointBuffer[i];
}
if (pnt.z > max.z)
{ max.z = pnt.z;
maxVtxs[2] = pointBuffer[i];
}
else if (pnt.z < min.z)
{ min.z = pnt.z;
maxVtxs[2] = pointBuffer[i];
}
}
// this epsilon formula comes from QuickHull, and I'm
// not about to quibble.
charLength = Math.max(max.x-min.x, max.y-min.y);
charLength = Math.max(max.z-min.z, charLength);
if (explicitTolerance == AUTOMATIC_TOLERANCE)
{ tolerance =
3*DOUBLE_PREC*(Math.max(Math.abs(max.x),Math.abs(min.x))+
Math.max(Math.abs(max.y),Math.abs(min.y))+
Math.max(Math.abs(max.z),Math.abs(min.z)));
}
else
{ tolerance = explicitTolerance;
}
}
/**
* Creates the initial simplex from which the hull will be built.
*/
protected void createInitialSimplex ()
throws IllegalArgumentException
{
double max = 0;
int imax = 0;
for (int i=0; i<3; i++)
{ double diff = maxVtxs[i].pnt.get(i)-minVtxs[i].pnt.get(i);
if (diff > max)
{ max = diff;
imax = i;
}
}
if (max <= tolerance)
{ throw new IllegalArgumentException (
"Input points appear to be coincident");
}
Vertex[] vtx = new Vertex[4];
// set first two vertices to be those with the greatest
// one dimensional separation
vtx[0] = maxVtxs[imax];
vtx[1] = minVtxs[imax];
// set third vertex to be the vertex farthest from
// the line between vtx0 and vtx1
Vector3d u01 = new Vector3d();
Vector3d diff02 = new Vector3d();
Vector3d nrml = new Vector3d();
Vector3d xprod = new Vector3d();
double maxSqr = 0;
u01.sub (vtx[1].pnt, vtx[0].pnt);
u01.normalize();
for (int i=0; i<numPoints; i++)
{ diff02.sub (pointBuffer[i].pnt, vtx[0].pnt);
xprod.cross (u01, diff02);
double lenSqr = xprod.normSquared();
if (lenSqr > maxSqr &&
pointBuffer[i] != vtx[0] && // paranoid
pointBuffer[i] != vtx[1])
{ maxSqr = lenSqr;
vtx[2] = pointBuffer[i];
nrml.set (xprod);
}
}
if (Math.sqrt(maxSqr) <= 100*tolerance)
{ throw new IllegalArgumentException (
"Input points appear to be colinear");
}
nrml.normalize();
double maxDist = 0;
double d0 = vtx[2].pnt.dot (nrml);
for (int i=0; i<numPoints; i++)
{ double dist = Math.abs (pointBuffer[i].pnt.dot(nrml) - d0);
if (dist > maxDist &&
pointBuffer[i] != vtx[0] && // paranoid
pointBuffer[i] != vtx[1] &&
pointBuffer[i] != vtx[2])
{ maxDist = dist;
vtx[3] = pointBuffer[i];
}
}
if (Math.abs(maxDist) <= 100*tolerance)
{ throw new IllegalArgumentException (
"Input points appear to be coplanar");
}
if (debug)
{ System.out.println ("initial vertices:");
System.out.println (vtx[0].index + ": " + vtx[0].pnt);
System.out.println (vtx[1].index + ": " + vtx[1].pnt);
System.out.println (vtx[2].index + ": " + vtx[2].pnt);
System.out.println (vtx[3].index + ": " + vtx[3].pnt);
}
Face[] tris = new Face[4];
if (vtx[3].pnt.dot (nrml) - d0 < 0)
{ tris[0] = Face.createTriangle (vtx[0], vtx[1], vtx[2]);
tris[1] = Face.createTriangle (vtx[3], vtx[1], vtx[0]);
tris[2] = Face.createTriangle (vtx[3], vtx[2], vtx[1]);
tris[3] = Face.createTriangle (vtx[3], vtx[0], vtx[2]);
for (int i=0; i<3; i++)
{ int k = (i+1)%3;
tris[i+1].getEdge(1).setOpposite (tris[k+1].getEdge(0));
tris[i+1].getEdge(2).setOpposite (tris[0].getEdge(k));
}
}
else
{ tris[0] = Face.createTriangle (vtx[0], vtx[2], vtx[1]);
tris[1] = Face.createTriangle (vtx[3], vtx[0], vtx[1]);
tris[2] = Face.createTriangle (vtx[3], vtx[1], vtx[2]);
tris[3] = Face.createTriangle (vtx[3], vtx[2], vtx[0]);
for (int i=0; i<3; i++)
{ int k = (i+1)%3;
tris[i+1].getEdge(0).setOpposite (tris[k+1].getEdge(1));
tris[i+1].getEdge(2).setOpposite (tris[0].getEdge((3-i)%3));
}
}
for (int i=0; i<4; i++)
{ faces.add (tris[i]);
}
for (int i=0; i<numPoints; i++)
{ Vertex v = pointBuffer[i];
if (v == vtx[0] || v == vtx[1] || v == vtx[2] || v == vtx[3])
{ continue;
}
maxDist = tolerance;
Face maxFace = null;
for (int k=0; k<4; k++)
{ double dist = tris[k].distanceToPlane (v.pnt);
if (dist > maxDist)
{ maxFace = tris[k];
maxDist = dist;
}
}
if (maxFace != null)
{ addPointToFace (v, maxFace);
}
}
}
/**
* Returns the number of vertices in this hull.
*
* @return number of vertices
*/
public int getNumVertices()
{
return numVertices;
}
/**
* Returns the vertex points in this hull.
*
* @return array of vertex points
* @see QuickHull3D#getVertices(double[])
* @see QuickHull3D#getFaces()
*/
public Point3d[] getVertices()
{
Point3d[] vtxs = new Point3d[numVertices];
for (int i=0; i<numVertices; i++)
{ vtxs[i] = pointBuffer[vertexPointIndices[i]].pnt;
}
return vtxs;
}
/**
* Returns the coordinates of the vertex points of this hull.
*
* @param coords returns the x, y, z coordinates of each vertex.
* This length of this array must be at least three times
* the number of vertices.
* @return the number of vertices
* @see QuickHull3D#getVertices()
* @see QuickHull3D#getFaces()
*/
public int getVertices(double[] coords)
{
for (int i=0; i<numVertices; i++)
{ Point3d pnt = pointBuffer[vertexPointIndices[i]].pnt;
coords[i*3+0] = pnt.x;
coords[i*3+1] = pnt.y;
coords[i*3+2] = pnt.z;
}
return numVertices;
}
/**
* Returns an array specifing the index of each hull vertex
* with respect to the original input points.
*
* @return vertex indices with respect to the original points
*/
public int[] getVertexPointIndices()
{
int[] indices = new int[numVertices];
for (int i=0; i<numVertices; i++)
{ indices[i] = vertexPointIndices[i];
}
return indices;
}
/**
* Returns the number of faces in this hull.
*
* @return number of faces
*/
public int getNumFaces()
{
return faces.size();
}
/**
* Returns the faces associated with this hull.
*
* <p>Each face is represented by an integer array which gives the
* indices of the vertices. These indices are numbered
* relative to the
* hull vertices, are zero-based,
* and are arranged counter-clockwise. More control
* over the index format can be obtained using
* {@link #getFaces(int) getFaces(indexFlags)}.
*
* @return array of integer arrays, giving the vertex
* indices for each face.
* @see QuickHull3D#getVertices()
* @see QuickHull3D#getFaces(int)
*/
public int[][] getFaces ()
{
return getFaces(0);
}
/**
* Returns the faces associated with this hull.
*
* <p>Each face is represented by an integer array which gives the
* indices of the vertices. By default, these indices are numbered with
* respect to the hull vertices (as opposed to the input points), are
* zero-based, and are arranged counter-clockwise. However, this
* can be changed by setting {@link #POINT_RELATIVE
* POINT_RELATIVE}, {@link #INDEXED_FROM_ONE INDEXED_FROM_ONE}, or
* {@link #CLOCKWISE CLOCKWISE} in the indexFlags parameter.
*
* @param indexFlags specifies index characteristics (0 results
* in the default)
* @return array of integer arrays, giving the vertex
* indices for each face.
* @see QuickHull3D#getVertices()
*/
public int[][] getFaces (int indexFlags)
{
int[][] allFaces = new int[faces.size()][];
int k = 0;
for (Face face: faces )
{
allFaces[k] = new int[face.numVertices()];
getFaceIndices (allFaces[k], face, indexFlags);
k++;
}
return allFaces;
}
/**
* Prints the vertices and faces of this hull to the stream ps.
*
* <p>
* This is done using the Alias Wavefront .obj file
* format, with the vertices printed first (each preceding by
* the letter <code>v</code>), followed by the vertex indices
* for each face (each
* preceded by the letter <code>f</code>).
*
* <p>The face indices are numbered with respect to the hull vertices
* (as opposed to the input points), with a lowest index of 1, and are
* arranged counter-clockwise. More control over the index format can
* be obtained using
* {@link #print(PrintStream,int) print(ps,indexFlags)}.
*
* @param ps stream used for printing
* @see QuickHull3D#print(PrintStream,int)
* @see QuickHull3D#getVertices()
* @see QuickHull3D#getFaces()
*/
public void print (PrintStream ps)
{
print (ps, 0);
}
/**
* Prints the vertices and faces of this hull to the stream ps.
*
* <p> This is done using the Alias Wavefront .obj file format, with
* the vertices printed first (each preceding by the letter
* <code>v</code>), followed by the vertex indices for each face (each
* preceded by the letter <code>f</code>).
*
* <p>By default, the face indices are numbered with respect to the
* hull vertices (as opposed to the input points), with a lowest index
* of 1, and are arranged counter-clockwise. However, this
* can be changed by setting {@link #POINT_RELATIVE POINT_RELATIVE},
* {@link #INDEXED_FROM_ONE INDEXED_FROM_ZERO}, or {@link #CLOCKWISE
* CLOCKWISE} in the indexFlags parameter.
*
* @param ps stream used for printing
* @param indexFlags specifies index characteristics
* (0 results in the default).
* @see QuickHull3D#getVertices()
* @see QuickHull3D#getFaces()
*/
public void print (PrintStream ps, int indexFlags)
{
if ((indexFlags & INDEXED_FROM_ZERO) == 0)
{ indexFlags |= INDEXED_FROM_ONE;
}
for (int i=0; i<numVertices; i++)
{ Point3d pnt = pointBuffer[vertexPointIndices[i]].pnt;
ps.println ("v " + pnt.x + " " + pnt.y + " " + pnt.z);
}
for (Face face: faces )
{
int[] indices = new int[face.numVertices()];
getFaceIndices (indices, face, indexFlags);
ps.print ("f");
for (int k=0; k<indices.length; k++)
{ ps.print (" " + indices[k]);
}
ps.println ("");
}
}
private void getFaceIndices (int[] indices, Face face, int flags)
{
boolean ccw = ((flags & CLOCKWISE) == 0);
boolean indexedFromOne = ((flags & INDEXED_FROM_ONE) != 0);
boolean pointRelative = ((flags & POINT_RELATIVE) != 0);
HalfEdge hedge = face.he0;
int k = 0;
do
{ int idx = hedge.head().index;
if (pointRelative)
{ idx = vertexPointIndices[idx];
}
if (indexedFromOne)
{ idx++;
}
indices[k++] = idx;
hedge = (ccw ? hedge.next : hedge.prev);
}
while (hedge != face.he0);
}
protected void resolveUnclaimedPoints (FaceList newFaces)
{
Vertex vtxNext = unclaimed.first();
for (Vertex vtx=vtxNext; vtx!=null; vtx=vtxNext)
{ vtxNext = vtx.next;
double maxDist = tolerance;
Face maxFace = null;
for (Face newFace=newFaces.first(); newFace != null;
newFace=newFace.next)
{
if (newFace.mark == Face.VISIBLE)
{ double dist = newFace.distanceToPlane(vtx.pnt);
if (dist > maxDist)
{ maxDist = dist;
maxFace = newFace;
}
if (maxDist > 1000*tolerance)
{ break;
}
}
}
if (maxFace != null)
{
addPointToFace (vtx, maxFace);
if (debug && vtx.index == findIndex)
{ System.out.println (findIndex + " CLAIMED BY " +
maxFace.getVertexString());
}
}
else
{ if (debug && vtx.index == findIndex)
{ System.out.println (findIndex + " DISCARDED");
}
}
}
}
protected void deleteFacePoints (Face face, Face absorbingFace)
{
Vertex faceVtxs = removeAllPointsFromFace (face);
if (faceVtxs != null)
{
if (absorbingFace == null)
{ unclaimed.addAll (faceVtxs);
}
else
{ Vertex vtxNext = faceVtxs;
for (Vertex vtx=vtxNext; vtx!=null; vtx=vtxNext)
{ vtxNext = vtx.next;
double dist = absorbingFace.distanceToPlane (vtx.pnt);
if (dist > tolerance)
{
addPointToFace (vtx, absorbingFace);
}
else
{
unclaimed.add (vtx);
}
}
}
}
}
private static final int NONCONVEX_WRT_LARGER_FACE = 1;
private static final int NONCONVEX = 2;
protected double oppFaceDistance (HalfEdge he)
{
return he.face.distanceToPlane (he.opposite.face.getCentroid());
}
private boolean doAdjacentMerge (Face face, int mergeType)
{
HalfEdge hedge = face.he0;
boolean convex = true;
do
{ Face oppFace = hedge.oppositeFace();
boolean merge = false;
double dist1, dist2;
if (mergeType == NONCONVEX)
{ // then merge faces if they are definitively non-convex
if (oppFaceDistance (hedge) > -tolerance ||
oppFaceDistance (hedge.opposite) > -tolerance)
{ merge = true;
}
}
else // mergeType == NONCONVEX_WRT_LARGER_FACE
{ // merge faces if they are parallel or non-convex
// wrt to the larger face; otherwise, just mark
// the face non-convex for the second pass.
if (face.area > oppFace.area)
{ if ((dist1 = oppFaceDistance (hedge)) > -tolerance)
{ merge = true;
}
else if (oppFaceDistance (hedge.opposite) > -tolerance)
{ convex = false;
}
}
else
{ if (oppFaceDistance (hedge.opposite) > -tolerance)
{ merge = true;
}
else if (oppFaceDistance (hedge) > -tolerance)
{ convex = false;
}
}
}
if (merge)
{ if (debug)
{ System.out.println (
" merging " + face.getVertexString() + " and " +
oppFace.getVertexString());
}
int numd = face.mergeAdjacentFace (hedge, discardedFaces);
for (int i=0; i<numd; i++)
{ deleteFacePoints (discardedFaces[i], face);
}
if (debug)
{ System.out.println (
" result: " + face.getVertexString());
}
return true;
}
hedge = hedge.next;
}
while (hedge != face.he0);
if (!convex)
{ face.mark = Face.NON_CONVEX;
}
return false;
}
protected void calculateHorizon (Point3d eyePnt, HalfEdge edge0, Face face, List<HalfEdge> horizon)
{
// oldFaces.add (face);
deleteFacePoints (face, null);
face.mark = Face.DELETED;
if (debug)
{ System.out.println (" visiting face " + face.getVertexString());
}
HalfEdge edge;
if (edge0 == null)
{ edge0 = face.getEdge(0);
edge = edge0;
}
else
{ edge = edge0.getNext();
}
do
{ Face oppFace = edge.oppositeFace();
if (oppFace.mark == Face.VISIBLE)
{ if (oppFace.distanceToPlane (eyePnt) > tolerance)
{ calculateHorizon (eyePnt, edge.getOpposite(),
oppFace, horizon);
}
else
{ horizon.add (edge);
if (debug)
{ System.out.println (" adding horizon edge " +
edge.getVertexString());
}
}
}
edge = edge.getNext();
}
while (edge != edge0);
}
private HalfEdge addAdjoiningFace (
Vertex eyeVtx, HalfEdge he)
{
Face face = Face.createTriangle (
eyeVtx, he.tail(), he.head());
faces.add (face);
face.getEdge(-1).setOpposite(he.getOpposite());
return face.getEdge(0);
}
protected void addNewFaces (FaceList newFaces, Vertex eyeVtx, List<HalfEdge> horizon)
{
newFaces.clear();
HalfEdge hedgeSidePrev = null;
HalfEdge hedgeSideBegin = null;
for (HalfEdge horizonHe : horizon )
{
HalfEdge hedgeSide = addAdjoiningFace (eyeVtx, horizonHe);
if (debug)
{ System.out.println (
"new face: " + hedgeSide.face.getVertexString());
}
if (hedgeSidePrev != null)
{ hedgeSide.next.setOpposite (hedgeSidePrev);
}
else
{ hedgeSideBegin = hedgeSide;
}
newFaces.add (hedgeSide.getFace());
hedgeSidePrev = hedgeSide;
}
hedgeSideBegin.next.setOpposite (hedgeSidePrev);
}
protected Vertex nextPointToAdd()
{
if (!claimed.isEmpty())
{ Face eyeFace = claimed.first().face;
Vertex eyeVtx = null;
double maxDist = 0;
for (Vertex vtx=eyeFace.outside;
vtx != null && vtx.face==eyeFace;
vtx = vtx.next)
{ double dist = eyeFace.distanceToPlane(vtx.pnt);
if (dist > maxDist)
{ maxDist = dist;
eyeVtx = vtx;
}
}
return eyeVtx;
}
else
{ return null;
}
}
protected void addPointToHull(Vertex eyeVtx)
{
horizon.clear();
unclaimed.clear();
if (debug)
{ System.out.println ("Adding point: " + eyeVtx.index);
System.out.println (
" which is " + eyeVtx.face.distanceToPlane(eyeVtx.pnt) +
" above face " + eyeVtx.face.getVertexString());
}
removePointFromFace (eyeVtx, eyeVtx.face);
calculateHorizon (eyeVtx.pnt, null, eyeVtx.face, horizon);
newFaces.clear();
addNewFaces (newFaces, eyeVtx, horizon);
// first merge pass ... merge faces which are non-convex
// as determined by the larger face
for (Face face = newFaces.first(); face!=null; face=face.next)
{
if (face.mark == Face.VISIBLE)
{ while (doAdjacentMerge(face, NONCONVEX_WRT_LARGER_FACE))
;
}
}
// second merge pass ... merge faces which are non-convex
// wrt either face
for (Face face = newFaces.first(); face!=null; face=face.next)
{
if (face.mark == Face.NON_CONVEX)
{ face.mark = Face.VISIBLE;
while (doAdjacentMerge(face, NONCONVEX))
;
}
}
resolveUnclaimedPoints(newFaces);
}
protected void buildHull ()
{
int cnt = 0;
Vertex eyeVtx;
computeMaxAndMin ();
createInitialSimplex ();
while ((eyeVtx = nextPointToAdd()) != null)
{ addPointToHull (eyeVtx);
cnt++;
if (debug)
{ System.out.println ("iteration " + cnt + " done");
}
}
reindexFacesAndVertices();
if (debug)
{ System.out.println ("hull done");
}
}
private void markFaceVertices (Face face, int mark)
{
HalfEdge he0 = face.getFirstEdge();
HalfEdge he = he0;
do
{ he.head().index = mark;
he = he.next;
}
while (he != he0);
}
protected void reindexFacesAndVertices()
{
for (int i=0; i<numPoints; i++)
{ pointBuffer[i].index = -1;
}
// remove inactive faces and mark active vertices
numFaces = 0;
for (Iterator it=faces.iterator(); it.hasNext(); )
{ Face face = (Face)it.next();
if (face.mark != Face.VISIBLE)
{ it.remove();
}
else
{ markFaceVertices (face, 0);
numFaces++;
}
}
// reindex vertices
numVertices = 0;
for (int i=0; i<numPoints; i++)
{ Vertex vtx = pointBuffer[i];
if (vtx.index == 0)
{ vertexPointIndices[numVertices] = i;
vtx.index = numVertices++;
}
}
}
protected boolean checkFaceConvexity (
Face face, double tol, PrintStream ps)
{
double dist;
HalfEdge he = face.he0;
do
{ face.checkConsistency();
// make sure edge is convex
dist = oppFaceDistance (he);
if (dist > tol)
{ if (ps != null)
{ ps.println ("Edge " + he.getVertexString() +
" non-convex by " + dist);
}
return false;
}
dist = oppFaceDistance (he.opposite);
if (dist > tol)
{ if (ps != null)
{ ps.println ("Opposite edge " +
he.opposite.getVertexString() +
" non-convex by " + dist);
}
return false;
}
if (he.next.oppositeFace() == he.oppositeFace())
{ if (ps != null)
{ ps.println ("Redundant vertex " + he.head().index +
" in face " + face.getVertexString());
}
return false;
}
he = he.next;
}
while (he != face.he0);
return true;
}
protected boolean checkFaces(double tol, PrintStream ps)
{
// check edge convexity
boolean convex = true;
for (Face face : faces)
{
if (face.mark == Face.VISIBLE)
{ if (!checkFaceConvexity (face, tol, ps))
{ convex = false;
}
}
}
return convex;
}
/**
* Checks the correctness of the hull using the distance tolerance
* returned by {@link QuickHull3D#getDistanceTolerance
* getDistanceTolerance}; see
* {@link QuickHull3D#check(PrintStream,double)
* check(PrintStream,double)} for details.
*
* @param ps print stream for diagnostic messages; may be
* set to <code>null</code> if no messages are desired.
* @return true if the hull is valid
* @see QuickHull3D#check(PrintStream,double)
*/
public boolean check (PrintStream ps)
{
return check (ps, getDistanceTolerance());
}
/**
* Checks the correctness of the hull. This is done by making sure that
* no faces are non-convex and that no points are outside any face.
* These tests are performed using the distance tolerance <i>tol</i>.
* Faces are considered non-convex if any edge is non-convex, and an
* edge is non-convex if the centroid of either adjoining face is more
* than <i>tol</i> above the plane of the other face. Similarly,
* a point is considered outside a face if its distance to that face's
* plane is more than 10 times <i>tol</i>.
*
* <p>If the hull has been {@link #triangulate triangulated},
* then this routine may fail if some of the resulting
* triangles are very small or thin.
*
* @param ps print stream for diagnostic messages; may be
* set to <code>null</code> if no messages are desired.
* @param tol distance tolerance
* @return true if the hull is valid
* @see QuickHull3D#check(PrintStream)
*/
public boolean check (PrintStream ps, double tol)
{
// check to make sure all edges are fully connected
// and that the edges are convex
double dist;
double pointTol = 10*tol;
if (!checkFaces(tolerance, ps))
{ return false;
}
// check point inclusion
for (int i=0; i<numPoints; i++)
{ Point3d pnt = pointBuffer[i].pnt;
for (Face face : faces)
{
if (face.mark == Face.VISIBLE)
{
dist = face.distanceToPlane (pnt);
if (dist > pointTol)
{ if (ps != null)
{ ps.println (
"Point " + i + " " + dist + " above face " +
face.getVertexString());
}
return false;
}
}
}
}
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment