Created
June 21, 2011 19:31
-
-
Save monkstone/1038673 to your computer and use it in GitHub Desktop.
Updated quickhull 3D
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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