/** | |
* 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 org.eclipse.apogy.common.math.quickhull3d; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.InputStreamReader; | |
import java.io.PrintStream; | |
import java.io.StreamTokenizer; | |
import java.util.ArrayList; | |
import java.util.Iterator; | |
import java.util.List; | |
import javax.vecmath.Point3d; | |
import javax.vecmath.Vector3d; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
/** | |
* 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 { | |
private static final Logger Logger = LoggerFactory.getLogger(QuickHull3D.class); | |
/** | |
* 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 Vertex[] pointBuffer = new Vertex[0]; | |
protected int[] vertexPointIndices = new int[0]; | |
private final Face[] discardedFaces = new Face[3]; | |
private final Vertex[] maxVtxs = new Vertex[3]; | |
private final Vertex[] minVtxs = new Vertex[3]; | |
protected List<Face> faces = new ArrayList<Face>(16); | |
protected List<HalfEdge> horizon = new ArrayList<HalfEdge>(16); | |
private final FaceList newFaces = new FaceList(); | |
private final VertexList unclaimed = new VertexList(); | |
private final VertexList claimed = new VertexList(); | |
protected int numVertices; | |
protected int numFaces; | |
protected int numPoints; | |
protected double explicitTolerance = AUTOMATIC_TOLERANCE; | |
protected double tolerance; | |
/** | |
* 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 this.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) { | |
this.explicitTolerance = tol; | |
} | |
/** | |
* Returns the explicit distance tolerance. | |
* | |
* @return explicit tolerance | |
* @see #setExplicitDistanceTolerance | |
*/ | |
public double getExplicitDistanceTolerance() { | |
return this.explicitTolerance; | |
} | |
private void addPointToFace(Vertex vtx, Face face) { | |
vtx.face = face; | |
if (face.outside == null) { | |
this.claimed.add(vtx); | |
} else { | |
this.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; | |
} | |
} | |
this.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; | |
} | |
this.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(Vector3d[] points) throws IllegalArgumentException { | |
build(points, points.length); | |
} | |
private HalfEdge findHalfEdge(Vertex tail, Vertex head) { | |
// brute force ... OK, since setHull is not used much | |
for (Iterator<Face> it = this.faces.iterator(); it.hasNext();) { | |
HalfEdge he = it.next().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(this.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); | |
this.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.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.exit(1); | |
} | |
indexList.add(0, new Integer((int) stok.nval)); | |
} | |
faceIndices[i] = new int[indexList.size()]; | |
int k = 0; | |
for (Iterator<Integer> it = indexList.iterator(); it.hasNext();) { | |
faceIndices[i][k++] = it.next().intValue(); | |
} | |
} | |
setHull(coords, nump, faceIndices, numf); | |
} catch (Exception e) { | |
Logger.error(e.getMessage(), e); | |
System.exit(1); | |
} | |
} | |
/** | |
* 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(Vector3d[] 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(Vector3d[] 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 * this.charLength * DOUBLE_PREC; | |
this.newFaces.clear(); | |
for (Iterator<Face> it = this.faces.iterator(); it.hasNext();) { | |
Face face = it.next(); | |
if (face.mark == Face.VISIBLE) { | |
face.triangulate(this.newFaces, minArea); | |
// splitFace (face); | |
} | |
} | |
for (Face face = this.newFaces.first(); face != null; face = face.next) { | |
this.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 (this.pointBuffer.length < nump) { | |
Vertex[] newBuffer = new Vertex[nump]; | |
this.vertexPointIndices = new int[nump]; | |
for (int i = 0; i < this.pointBuffer.length; i++) { | |
newBuffer[i] = this.pointBuffer[i]; | |
} | |
for (int i = this.pointBuffer.length; i < nump; i++) { | |
newBuffer[i] = new Vertex(); | |
} | |
this.pointBuffer = newBuffer; | |
} | |
this.faces.clear(); | |
this.claimed.clear(); | |
this.numFaces = 0; | |
this.numPoints = nump; | |
} | |
protected void setPoints(double[] coords, int nump) { | |
for (int i = 0; i < nump; i++) { | |
Vertex vtx = this.pointBuffer[i]; | |
vtx.pnt.set(coords[i * 3 + 0], coords[i * 3 + 1], coords[i * 3 + 2]); | |
vtx.index = i; | |
} | |
} | |
protected void setPoints(Vector3d[] pnts, int nump) { | |
for (int i = 0; i < nump; i++) { | |
Vertex vtx = this.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++) { | |
this.maxVtxs[i] = this.minVtxs[i] = this.pointBuffer[0]; | |
} | |
max.set(this.pointBuffer[0].pnt); | |
min.set(this.pointBuffer[0].pnt); | |
for (int i = 1; i < this.numPoints; i++) { | |
Vector3d pnt = this.pointBuffer[i].pnt; | |
if (pnt.x > max.x) { | |
max.x = pnt.x; | |
this.maxVtxs[0] = this.pointBuffer[i]; | |
} else if (pnt.x < min.x) { | |
min.x = pnt.x; | |
this.minVtxs[0] = this.pointBuffer[i]; | |
} | |
if (pnt.y > max.y) { | |
max.y = pnt.y; | |
this.maxVtxs[1] = this.pointBuffer[i]; | |
} else if (pnt.y < min.y) { | |
min.y = pnt.y; | |
this.minVtxs[1] = this.pointBuffer[i]; | |
} | |
if (pnt.z > max.z) { | |
max.z = pnt.z; | |
this.maxVtxs[2] = this.pointBuffer[i]; | |
} else if (pnt.z < min.z) { | |
min.z = pnt.z; | |
this.maxVtxs[2] = this.pointBuffer[i]; | |
} | |
} | |
// this epsilon formula comes from QuickHull, and I'm | |
// not about to quibble. | |
this.charLength = Math.max(max.x - min.x, max.y - min.y); | |
this.charLength = Math.max(max.z - min.z, this.charLength); | |
if (this.explicitTolerance == AUTOMATIC_TOLERANCE) { | |
this.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 { | |
this.tolerance = this.explicitTolerance; | |
} | |
} | |
/** | |
* Creates the initial simplex from which the hull will be built. | |
*/ | |
protected void createInitialSimplex() throws IllegalArgumentException { | |
double max = 0; | |
int imax = 0; | |
double[] maxPiData = new double[3]; | |
double[] minPiData = new double[3]; | |
for (int i = 0; i < 3; i++) { | |
this.maxVtxs[i].pnt.get(maxPiData); | |
this.minVtxs[i].pnt.get(minPiData); | |
double diff = maxPiData[i] - minPiData[i]; | |
if (diff > max) { | |
max = diff; | |
imax = i; | |
} | |
} | |
if (max <= this.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] = this.maxVtxs[imax]; | |
vtx[1] = this.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 < this.numPoints; i++) { | |
diff02.sub(this.pointBuffer[i].pnt, vtx[0].pnt); | |
xprod.cross(u01, diff02); | |
double lenSqr = xprod.lengthSquared(); | |
if (lenSqr > maxSqr && this.pointBuffer[i] != vtx[0] && // paranoid | |
this.pointBuffer[i] != vtx[1]) { | |
maxSqr = lenSqr; | |
vtx[2] = this.pointBuffer[i]; | |
nrml.set(xprod); | |
} | |
} | |
if (Math.sqrt(maxSqr) <= 100 * this.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 < this.numPoints; i++) { | |
double dist = Math.abs(this.pointBuffer[i].pnt.dot(nrml) - d0); | |
if (dist > maxDist && this.pointBuffer[i] != vtx[0] && // paranoid | |
this.pointBuffer[i] != vtx[1] && this.pointBuffer[i] != vtx[2]) { | |
maxDist = dist; | |
vtx[3] = this.pointBuffer[i]; | |
} | |
} | |
if (Math.abs(maxDist) <= 100 * this.tolerance) { | |
throw new IllegalArgumentException("Input points appear to be coplanar"); | |
} | |
Logger.debug("Initial vertices:"); | |
Logger.debug(vtx[0].index + ": " + vtx[0].pnt); | |
Logger.debug(vtx[1].index + ": " + vtx[1].pnt); | |
Logger.debug(vtx[2].index + ": " + vtx[2].pnt); | |
Logger.debug(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++) { | |
this.faces.add(tris[i]); | |
} | |
for (int i = 0; i < this.numPoints; i++) { | |
Vertex v = this.pointBuffer[i]; | |
if (v == vtx[0] || v == vtx[1] || v == vtx[2] || v == vtx[3]) { | |
continue; | |
} | |
maxDist = this.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 this.numVertices; | |
} | |
/** | |
* Returns the vertex points in this hull. | |
* | |
* @return array of vertex points | |
* @see QuickHull3D#getVertices(double[]) | |
* @see QuickHull3D#getFaces() | |
*/ | |
public Vector3d[] getVertices() { | |
Vector3d[] vtxs = new Vector3d[this.numVertices]; | |
for (int i = 0; i < this.numVertices; i++) { | |
vtxs[i] = this.pointBuffer[this.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 < this.numVertices; i++) { | |
Vector3d pnt = this.pointBuffer[this.vertexPointIndices[i]].pnt; | |
coords[i * 3 + 0] = pnt.x; | |
coords[i * 3 + 1] = pnt.y; | |
coords[i * 3 + 2] = pnt.z; | |
} | |
return this.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[this.numVertices]; | |
for (int i = 0; i < this.numVertices; i++) { | |
indices[i] = this.vertexPointIndices[i]; | |
} | |
return indices; | |
} | |
/** | |
* Returns the number of faces in this hull. | |
* | |
* @return number of faces | |
*/ | |
public int getNumFaces() { | |
return this.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[this.faces.size()][]; | |
int k = 0; | |
for (Iterator<Face> it = this.faces.iterator(); it.hasNext();) { | |
Face face = it.next(); | |
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 < this.numVertices; i++) { | |
Vector3d pnt = this.pointBuffer[this.vertexPointIndices[i]].pnt; | |
ps.println("v " + pnt.x + " " + pnt.y + " " + pnt.z); | |
} | |
for (Iterator<Face> fi = this.faces.iterator(); fi.hasNext();) { | |
Face face = fi.next(); | |
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 = this.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 = this.unclaimed.first(); | |
for (Vertex vtx = vtxNext; vtx != null; vtx = vtxNext) { | |
vtxNext = vtx.next; | |
double maxDist = this.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 * this.tolerance) { | |
break; | |
} | |
} | |
} | |
if (maxFace != null) { | |
addPointToFace(vtx, maxFace); | |
if (vtx.index == this.findIndex) { | |
Logger.debug(this.findIndex + " CLAIMED BY " + maxFace.getVertexString()); | |
} | |
} else { | |
if (vtx.index == this.findIndex) { | |
Logger.debug(this.findIndex + " DISCARDED"); | |
} | |
} | |
} | |
} | |
protected void deleteFacePoints(Face face, Face absorbingFace) { | |
Vertex faceVtxs = removeAllPointsFromFace(face); | |
if (faceVtxs != null) { | |
if (absorbingFace == null) { | |
this.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 > this.tolerance) { | |
addPointToFace(vtx, absorbingFace); | |
} else { | |
this.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; | |
if (mergeType == NONCONVEX) { // then merge faces if they are | |
// definitively non-convex | |
if (oppFaceDistance(hedge) > -this.tolerance || oppFaceDistance(hedge.opposite) > -this.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 ((oppFaceDistance(hedge)) > -this.tolerance) { | |
merge = true; | |
} else if (oppFaceDistance(hedge.opposite) > -this.tolerance) { | |
convex = false; | |
} | |
} else { | |
if (oppFaceDistance(hedge.opposite) > -this.tolerance) { | |
merge = true; | |
} else if (oppFaceDistance(hedge) > -this.tolerance) { | |
convex = false; | |
} | |
} | |
} | |
if (merge) { | |
Logger.debug("Merging " + face.getVertexString() + " and " + oppFace.getVertexString()); | |
int numd = face.mergeAdjacentFace(hedge, this.discardedFaces); | |
for (int i = 0; i < numd; i++) { | |
deleteFacePoints(this.discardedFaces[i], face); | |
} | |
Logger.debug(" result: " + face.getVertexString()); | |
return true; | |
} | |
hedge = hedge.next; | |
} while (hedge != face.he0); | |
if (!convex) { | |
face.mark = Face.NON_CONVEX; | |
} | |
return false; | |
} | |
protected void calculateHorizon(Vector3d eyePnt, HalfEdge edge0, Face face, List<HalfEdge> horizon) { | |
// oldFaces.add (face); | |
deleteFacePoints(face, null); | |
face.mark = Face.DELETED; | |
Logger.debug("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) > this.tolerance) { | |
calculateHorizon(eyePnt, edge.getOpposite(), oppFace, horizon); | |
} else { | |
horizon.add(edge); | |
Logger.debug("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()); | |
this.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 (Iterator<HalfEdge> it = horizon.iterator(); it.hasNext();) { | |
HalfEdge horizonHe = it.next(); | |
HalfEdge hedgeSide = addAdjoiningFace(eyeVtx, horizonHe); | |
Logger.debug("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 (!this.claimed.isEmpty()) { | |
Face eyeFace = this.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) { | |
this.horizon.clear(); | |
this.unclaimed.clear(); | |
Logger.debug("Adding point: " + eyeVtx.index); | |
Logger.debug(" which is " + eyeVtx.face.distanceToPlane(eyeVtx.pnt) + " above face " | |
+ eyeVtx.face.getVertexString()); | |
removePointFromFace(eyeVtx, eyeVtx.face); | |
calculateHorizon(eyeVtx.pnt, null, eyeVtx.face, this.horizon); | |
this.newFaces.clear(); | |
addNewFaces(this.newFaces, eyeVtx, this.horizon); | |
// first merge pass ... merge faces which are non-convex | |
// as determined by the larger face | |
for (Face face = this.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 = this.newFaces.first(); face != null; face = face.next) { | |
if (face.mark == Face.NON_CONVEX) { | |
face.mark = Face.VISIBLE; | |
while (doAdjacentMerge(face, NONCONVEX)) | |
; | |
} | |
} | |
resolveUnclaimedPoints(this.newFaces); | |
} | |
protected void buildHull() { | |
Vertex eyeVtx; | |
computeMaxAndMin(); | |
createInitialSimplex(); | |
while ((eyeVtx = nextPointToAdd()) != null) { | |
addPointToHull(eyeVtx); | |
} | |
reindexFacesAndVertices(); | |
} | |
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 < this.numPoints; i++) { | |
this.pointBuffer[i].index = -1; | |
} | |
// remove inactive faces and mark active vertices | |
this.numFaces = 0; | |
for (Iterator<Face> it = this.faces.iterator(); it.hasNext();) { | |
Face face = it.next(); | |
if (face.mark != Face.VISIBLE) { | |
it.remove(); | |
} else { | |
markFaceVertices(face, 0); | |
this.numFaces++; | |
} | |
} | |
// re-index vertices | |
this.numVertices = 0; | |
for (int i = 0; i < this.numPoints; i++) { | |
Vertex vtx = this.pointBuffer[i]; | |
if (vtx.index == 0) { | |
this.vertexPointIndices[this.numVertices] = i; | |
vtx.index = this.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 (Iterator<Face> it = this.faces.iterator(); it.hasNext();) { | |
Face face = it.next(); | |
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(this.tolerance, ps)) { | |
return false; | |
} | |
// check point inclusion | |
for (int i = 0; i < this.numPoints; i++) { | |
Vector3d pnt = this.pointBuffer[i].pnt; | |
for (Iterator<Face> it = this.faces.iterator(); it.hasNext();) { | |
Face face = it.next(); | |
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; | |
} | |
} |