/*
* Copyright (c) 2016 Fraunhofer IGD
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution. If not, see <http://www.gnu.org/licenses/>.
*
* Contributors:
* Fraunhofer IGD <http://www.igd.fraunhofer.de/>
*/
package de.fhg.igd.geom.algorithm;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import de.fhg.igd.geom.Point3D;
/**
* Triangulates FaceSets using a 3D version of the ear cutting algorithm. This
* class is also able to triangulate faces which are not flat.
*
* @author Michel Kraemer
*/
public class FaceTriangulation {
private static final int CONVEX_NOTCHECKED = 0;
private static final int CONVEX_TRUE = 1;
private static final int CONVEX_FALSE = 2;
/**
* An array that saves, if a point is convex or not (see constants above)
*/
private int[] _convexCache;
/**
* Default constructor
*/
public FaceTriangulation() {
super();
}
/**
* Checks if the vertex p2 is convex by calculating the determinant of
* p1,p2,p3 which is negative if p2 convex and positive if it is not.
*
* @param p1 the point prior to p2
* @param p2 the point to check
* @param p3 the point next to p2
* @return true if p2 is convex, false otherwise
*/
private boolean isConvex(Point3D p1, Point3D p2, Point3D p3) {
// get vectors P2P3 and P2P1
double a1x = p3.getX() - p2.getX();
double a1y = p3.getY() - p2.getY();
double a1z = p3.getZ() - p2.getZ();
double a2x = p1.getX() - p2.getX();
double a2y = p1.getY() - p2.getY();
double a2z = p1.getZ() - p2.getZ();
// calculate determinant of the cross product
double cx = a1y * a2z - a1z * a2y;
double cy = a1z * a2x - a1x * a2z;
double cz = a1x * a2y - a1y * a2x;
double det = cx + cy + cz;
// if the determinant is negative, then p2 is convex
return (det < 0.0);
}
/**
* Checks if vertex p2 is an ear
*
* @param p1 the point prior to p2
* @param p2 the point to check
* @param p3 the point next to p2
* @param points the array of all vertices
* @return true if p2 is an ear, false otherwise
*/
private boolean isEar(Point3D p1, Point3D p2, Point3D p3, List<Point3D> points) {
for (int i = 0; i < points.size() - 1; ++i) {
Point3D a1, a2, a3;
if (i == 0) {
a1 = points.get(points.size() - 1);
a2 = points.get(0);
a3 = points.get(1);
}
else {
a1 = points.get(i - 1);
a2 = points.get(i);
a3 = points.get(i + 1);
}
// don't check triangle points
if (a2 == p1 || a2 == p2 || a2 == p3) {
continue;
}
if (_convexCache[i] == CONVEX_NOTCHECKED) {
_convexCache[i] = (isConvex(a1, a2, a3) ? CONVEX_TRUE : CONVEX_FALSE);
}
if (_convexCache[i] == CONVEX_FALSE) {
// if this point is concave and
// the triangle p1,p2,p3 contains it
// the p1,p2,p3 is no ear!
boolean c1 = isConvex(p1, p2, a2);
boolean c2 = isConvex(p2, p3, a2);
boolean c3 = isConvex(p3, p1, a2);
if ((c1 == c2) && (c2 == c3)) {
return false;
}
}
}
return true;
}
/**
* Cuts an ear from a face. The ear will be removed from the given list of
* points and the indices of the real vertices will also be truncated.
*
* @param points all vertices of the face
* @param indices an array containing indices for the real vertices
* @return the indices of the ear
*/
private int[] earCutting(List<Point3D> points, int[] indices) {
if (points.size() == 3) {
points.clear();
return indices;
}
// check all points (we don't need to check the last one, because
// one triangle always remains)
for (int i = 0; i < points.size() - 1; ++i) {
Point3D v1, v2, v3;
int i1, i2, i3;
if (i == 0) {
v1 = points.get(points.size() - 1);
i1 = indices[points.size() - 1];
v2 = points.get(0);
i2 = indices[0];
v3 = points.get(1);
i3 = indices[1];
}
else {
v1 = points.get(i - 1);
i1 = indices[i - 1];
v2 = points.get(i);
i2 = indices[i];
v3 = points.get(i + 1);
i3 = indices[i + 1];
}
// is the vertex convex?
if (_convexCache[i] == CONVEX_NOTCHECKED) {
if (isConvex(v1, v2, v3)) {
_convexCache[i] = CONVEX_TRUE;
}
else {
_convexCache[i] = CONVEX_FALSE;
}
}
// if the vertex is concave it cannot be an ear
if (_convexCache[i] == CONVEX_FALSE) {
continue;
}
boolean ear = isEar(v1, v2, v3, points);
if (ear) {
// cut ear:
// create new Face
int[] result = new int[] { i1, i2, i3 };
// remove ear vertex from old Face
if (i == 0) {
System.arraycopy(indices, 1, indices, 0, indices.length - 1);
}
else {
System.arraycopy(indices, 0, indices, 0, i);
System.arraycopy(indices, i + 1, indices, i, indices.length - i - 1);
}
points.remove(i);
return result;
}
}
// should never happen, because a polygon always
// contains at least one ear
return null;
}
/**
* This method calculates the signed 2D area of a Polygon. If the result is
* < 0.0 the Polygon is oriented clockwise, otherwise it's counterclockwise.
*
* @param points the Polygon, whereas the first point MUST NOT equal the
* last one
* @return true if the Polygon is clockwise, false otherwise
*/
private static boolean isClockwise(List<Point3D> points) {
double d = 0.0;
int n = points.size();
for (int i = 0; i < n; ++i) {
d += points.get(i).getX()
* (points.get((i + 1) % n).getY() - points.get((i - 1 + n) % n).getY());
}
return (d < 0.0);
}
/**
* Reverses a list of points
*
* @param a the list
* @param indices the index array that connects the points with the vertices
* in the corresponding face
*/
private static void reversePoints(List<Point3D> a, int[] indices) {
for (int i = 0; i < a.size() / 2; ++i) {
int j = a.size() - 1 - i;
Point3D p1 = a.get(i);
Point3D p2 = a.get(j);
a.set(i, p2);
a.set(j, p1);
int i1 = indices[i];
int i2 = indices[j];
indices[i] = i2;
indices[j] = i1;
}
}
/**
* Normalizes a vector
*
* @param normal the vector to normalize
*/
private static void normalize(Point3D normal) {
double x = normal.getX();
double y = normal.getY();
double z = normal.getZ();
double length = Math.sqrt(x * x + y * y + z * z);
normal.setX(x / length);
normal.setY(y / length);
normal.setZ(z / length);
}
/**
* Projects a Face onto a 2D plane. Also compacts it (removes consecutive
* duplicate points).
*
* @param f the Face
* @param points a valid list that will receive the new projected points
* @return the index array that connects the new projected points with the
* vertex array in the given Face
* @throws IllegalArgumentException if the list of points is null
*/
private static int[] projectAndCompactFace(List<Point3D> f, List<Point3D> points) {
// calculate average normal of the face
Point3D k = Point3D.calcNormal(f);
return projectAndCompactFace(f, points, k);
}
/**
* Projects a Face onto a 2D plane using a given normal. Also compacts the
* face (removes consecutive duplicate points).
*
* @param f the Face
* @param k the normal
* @param points a valid list that will receive the new projected points
* @return the index array that connects the new projected points with the
* vertex array in the given Face
* @throws IllegalArgumentException if the list of points is null
*/
private static int[] projectAndCompactFace(List<Point3D> f, List<Point3D> points, Point3D k) {
if (points == null) {
throw new IllegalArgumentException("points must not be null");
}
// handle degenerated faces
if (f.size() == 0) {
return new int[0];
}
else if (f.size() == 1) {
points.add(f.get(0));
return new int[] { 0 };
}
else if (f.size() == 2) {
points.add(f.get(0));
points.add(f.get(1));
return new int[] { 0, 1 };
}
// calculate projected coordinate system
Point3D i = new Point3D();
Point3D j = new Point3D();
if ((Math.abs(k.getX()) > 0.1) || (Math.abs(k.getY()) > 0.1)) {
i.setX(-k.getY());
i.setY(k.getX());
i.setZ(k.getZ());
}
else {
i.setX(k.getZ());
i.setZ(-k.getX());
i.setY(k.getY());
}
normalize(i);
j.setX(i.getY() * k.getZ() - i.getZ() * k.getY());
j.setY(i.getZ() * k.getX() - i.getX() * k.getZ());
j.setZ(i.getX() * k.getY() - i.getY() * k.getX());
normalize(j);
// project face onto a plane:
// transform points and create index array
int len = f.size();
if (f.get(0).equals(f.get(len - 1))) {
// create a triangle from a face with 4 points, if the
// first one and the last one are equal
--len;
}
List<Integer> indexList = new ArrayList<Integer>();
for (int v = 0; v < len; ++v) {
if (f.get((len + v - 1) % len).equals(f.get(v))) {
// skip consecutive duplicate points
continue;
}
double vx = f.get(v).getX();
double vy = f.get(v).getY();
double vz = f.get(v).getZ();
double x = vx * i.getX() + vy * i.getY() + vz * i.getZ();
double y = vx * j.getX() + vy * j.getY() + vz * j.getZ();
double z = vx * k.getX() + vy * k.getY() + vz * k.getZ();
points.add(new Point3D(x, y, z));
// add the original index, so we can refer to it later
indexList.add(v);
}
// copy indexes
int[] indices = new int[indexList.size()];
int p = 0;
for (Integer index : indexList) {
indices[p++] = index;
}
return indices;
}
/**
* Triangulates a face and returns a list of triangles
*
* @param f the face to triangulate
* @return a list of triangulated faces containing the original vertices
*/
public List<List<Point3D>> triangulateFace(List<Point3D> f) {
List<List<Point3D>> result = new ArrayList<>();
// don't triangulate triangles
if (f.size() == 3) {
result.add(f);
return result;
}
// prepare cache for convex vertices
if (_convexCache == null || _convexCache.length < f.size()) {
_convexCache = new int[f.size()];
}
else {
Arrays.fill(_convexCache, CONVEX_NOTCHECKED);
}
// project face onto a plane
List<Point3D> points = new ArrayList<Point3D>();
int[] indices = projectAndCompactFace(f, points);
// the projected face must be clockwise in order to
// let the ear cutting algorithm work correctly
boolean reversed = false;
if (!isClockwise(points)) {
reversed = true;
reversePoints(points, indices);
}
// cut ears and create new faces
while (points.size() > 0) {
// cut an ear
int[] newindices = earCutting(points, indices);
if (newindices == null) {
// should never happen, because a polygon always
// contains at least one ear
break;
}
// create new face
List<Point3D> newface = new ArrayList<>();
if (!reversed) {
newface.add(f.get(newindices[0]));
newface.add(f.get(newindices[1]));
newface.add(f.get(newindices[2]));
}
else {
// preserve vertex order. The list of points
// has been reversed before, so reverse it again
newface.add(f.get(newindices[2]));
newface.add(f.get(newindices[1]));
newface.add(f.get(newindices[0]));
}
result.add(newface);
// clear convex cache
Arrays.fill(_convexCache, 0, points.size(), CONVEX_NOTCHECKED);
}
return result;
}
}