package com.vitco.low.triangulate; import org.poly2tri.Poly2Tri; import org.poly2tri.geometry.polygon.Polygon; import org.poly2tri.geometry.polygon.PolygonPoint; import org.poly2tri.triangulation.delaunay.DelaunayTriangle; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; /** * Helper class that converts a grid into triangles. * * Uses a variant of the monotone polygon approach (optimized). * * Reference: * http://0fps.net/2012/07/07/meshing-minecraft-part-2/ */ public class Grid2TriMono { // slower (and more general) implementation that doesn't require a monotone polygon private static java.util.List<DelaunayTriangle> triangulateSlow(ArrayList<byte[]> polyPoints) { ArrayList<PolygonPoint> pp = new ArrayList<PolygonPoint>(); for (byte[] point : polyPoints) { pp.add(new PolygonPoint(point[0], point[1])); } Polygon poly = new Polygon(pp); Poly2Tri.triangulate(poly); return poly.getTriangles(); } // helper - check if c is in between a and b private static boolean inBetween(byte[] a, byte[] b, byte[] c) { return (a != c && b != c) && // not the same points ((b[0] - a[0]) * (c[1] - a[1]) == (c[0] - a[0]) * (b[1] - a[1])) && // on one line ((a[0] < c[0] == c[0] < b[0]) && (a[1] < c[1] == c[1] < b[1])); // in between on that line } // helper to add a triangle private static void addTriangle(java.util.List<byte[][]> result, byte[] p1, byte[] p2, byte[] p3, ArrayList<byte[]> skippedPoints) { // check if we need to split the triangle on a given point boolean skipped = false; for (byte[] s : skippedPoints) { if (inBetween(p1, p2, s)) { skippedPoints.remove(s); addTriangle(result,s,p2,p3,skippedPoints); addTriangle(result, p1, s, p3, skippedPoints); skipped = true; break; } if (inBetween(p1, p3, s)) { skippedPoints.remove(s); addTriangle(result,s,p2,p3,skippedPoints); addTriangle(result, p1, p2, s, skippedPoints); skipped = true; break; } if (inBetween(p2, p3, s)) { skippedPoints.remove(s); addTriangle(result,p1,s,p3,skippedPoints); addTriangle(result, p1, p2, s, skippedPoints); skipped = true; break; } } // the triangle was not split and can be added if (!skipped) { result.add(new byte[][] {p1, p2, p3}); } } // loop over previous triangles and split if point // is on the edge of one triangle (it is assumed that exactly one triangle // in the previous triangles can be split) private static void splitPreviousTri(byte[] p, ArrayList<byte[][]> result) { // reverse loop (this is one of the recently added triangles) for (int i = result.size() - 1; i >= 0; i--) { byte[][] tri = result.get(i); if (inBetween(tri[0], tri[1], p)) { result.remove(tri); result.add(new byte[][]{p, tri[1], tri[2]}); result.add(new byte[][]{tri[0], p, tri[2]}); return; } if (inBetween(tri[0], tri[2], p)) { result.remove(tri); result.add(new byte[][]{p, tri[1], tri[2]}); result.add(new byte[][]{tri[0], tri[1], p}); return; } if (inBetween(tri[1], tri[2], p)) { result.remove(tri); result.add(new byte[][]{tri[0], p, tri[2]}); result.add(new byte[][]{tri[0], tri[1], p}); return; } } // no triangle found ?! assert false; } // expects the mono polygon points to be sorted by x coordinate and // by y coordinate in "poly direction" private static java.util.List<DelaunayTriangle> triangulate(ArrayList<byte[]> orderedPoints, boolean fixEdges) { ArrayList<byte[][]> result = new ArrayList<byte[][]>(); LinkedList<byte[]> stack = new LinkedList<byte[]>(); stack.push(orderedPoints.remove(0)); stack.push(orderedPoints.remove(0)); // contains list with "skipped" pixel (i.e. pixel that have missed a triangulation ArrayList<byte[]> skippedPoints = new ArrayList<byte[]>(); for (byte[] vi : orderedPoints) { byte[] top = stack.pop(); byte[] upper = top; byte[] lower = top; if (vi[2] == top[2]) { boolean convex = true; while (convex && !stack.isEmpty()) { lower = stack.pop(); // check angles and create triangles int cross = (upper[0]-lower[0])*(vi[1]-upper[1]) - (upper[1]-lower[1])*(vi[0]-upper[0]); // check if angle is convex convex = Math.signum(cross) == -upper[2] || cross == 0; if (convex) { if (cross != 0) { if (upper[2] == -1) { addTriangle(result, lower, upper, vi, skippedPoints); } else { addTriangle(result, upper, lower, vi, skippedPoints); } } else if (fixEdges) { boolean xdiffer, ydiffer; if (((xdiffer = lower[0] != upper[0]) && (lower[0] < upper[0] == upper[0] < vi[0])) || ((ydiffer = lower[1] != upper[1]) && (lower[1] < upper[1] == upper[1] < vi[1]))) { skippedPoints.add(upper); } else if (((xdiffer) && (lower[0] < vi[0] == vi[0] < upper[0])) || ((ydiffer) && (lower[1] < vi[1] == vi[1] < upper[1]))) { // we need to consider old triangles now (!) splitPreviousTri(vi, result); } else { skippedPoints.add(lower); } } } else { stack.push(lower); stack.push(upper); } upper = lower; } if (convex) { stack.push(lower); } } else { // connect to all points on stack while (!stack.isEmpty()) { lower = stack.pop(); // test if points form a line // note: this is only ok b/c we work with voxel (!) boolean areLineX = (upper[0] == lower[0] && lower[0] == vi[0]); boolean areLineY = (upper[1] == lower[1] && lower[1] == vi[1]); if (!areLineX && !areLineY) { if (upper[2] == -1) { addTriangle(result, lower, upper, vi, skippedPoints); } else { addTriangle(result, upper, lower, vi, skippedPoints); } } else if (fixEdges) { // Note: case where vi lies in the middle doesn't exist if ((areLineX && (lower[1] < upper[1] == upper[1] < vi[1])) || (areLineY && (lower[0] < upper[0] == upper[0] < vi[0]))) { skippedPoints.add(upper); } else { skippedPoints.add(lower); } } upper = lower; } stack.push(top); } stack.push(vi); } assert skippedPoints.isEmpty(); // convert to triangles List<DelaunayTriangle> resultObjects = new ArrayList<DelaunayTriangle>(); for (byte[][] tri : result) { resultObjects.add(new DelaunayTriangle(new PolygonPoint(tri[0][0], tri[0][1]), new PolygonPoint(tri[1][0], tri[1][1]), new PolygonPoint(tri[2][0], tri[2][1]))); } return resultObjects; } // clone a 2D boolean array public static boolean[][] clone2DArray(boolean[][] array) { int rows = array.length; boolean[][] newArray = array.clone(); for(int row=0; row < rows; row++){ newArray[row]=array[row].clone(); } return newArray; } // triangulate the bit array (treat as voxel) public static ArrayList<DelaunayTriangle> triangulate(boolean[][] bits, boolean fixEdges) { byte lenX = (byte) bits.length; byte lenY = (byte) bits[0].length; byte pyP,pyN,pyPprev,pyNprev,start; boolean cleanup; byte[] p; boolean[][] grayList = clone2DArray(bits); ArrayList<DelaunayTriangle> result = new ArrayList<DelaunayTriangle>(); ArrayList<byte[]> orderedPoints = new ArrayList<byte[]>(); // split out voxel area in mono polygons // loop over all points for (byte x = 0; x < lenX; x++) { for (byte y = 0; y < lenY; y++) { // check if this position is set if (bits[x][y]) { // clear point list orderedPoints.clear(); // set the previous points pyPprev = -1; pyNprev = -1; // initial starting position start = y; // loop over all columns for (byte i = x; i < lenX; ) { // initialize borders pyN = 0; pyP = lenY; // -- handle positive y direction for (byte j = start; j < lenY; j++) { if (bits[i][j]) { bits[i][j] = false; } else { pyP = j; break; } } // -- handle negative y direction for (byte j = start; --j > -1;) { if (bits[i][j]) { bits[i][j] = false; } else { pyN = ++j; break; } } // ---------- // add missing poly points if (pyNprev != pyN) { if (pyNprev != -1) { p = new byte[] {i, pyNprev, -1}; orderedPoints.add(p); } else { pyNprev = y; } if (fixEdges) { // >>> // check forward y overlap (negative) boolean prevFound = grayList[i][pyN]; for (byte j = pyNprev; ++j < pyN;) { if (grayList[i][j] && !prevFound) { p = new byte[] {i, j, -1}; orderedPoints.add(p); } else if (!grayList[i][j] && prevFound) { p = new byte[] {i, j, -1}; orderedPoints.add(p); } prevFound = grayList[i][j]; } // <<< // >>> if (i > 0) { // check backward y overlap (negative) prevFound = pyNprev > 0 && grayList[i-1][pyNprev-1]; for (byte j = (byte) (pyNprev-2); j >= pyN; j--) { if (grayList[i-1][j] && !prevFound) { p = new byte[] {i, (byte) (j+1), -1}; orderedPoints.add(p); } else if (!grayList[i-1][j] && prevFound) { p = new byte[] {i, (byte) (j+1), -1}; orderedPoints.add(p); } prevFound = grayList[i-1][j]; } } // <<< } p = new byte[] {i, pyN, -1}; orderedPoints.add(p); pyNprev = pyN; } if (pyPprev != pyP) { if (pyPprev != -1) { p = new byte[] {i, pyPprev, 1}; orderedPoints.add(p); } else { pyPprev = y; } if (fixEdges) { // >>> // check forward y overlap (positive) boolean prevFound = pyPprev > 0 && grayList[i][pyPprev-1]; for (byte j = pyPprev; --j > pyP;) { if (grayList[i][j-1] && !prevFound) { p = new byte[] {i, j, 1}; orderedPoints.add(p); } else if (!grayList[i][j-1] && prevFound) { p = new byte[] {i, j, 1}; orderedPoints.add(p); } prevFound = grayList[i][j-1]; } // <<< // >>> if (i > 0) { // check backward y overlap (positive) prevFound = pyPprev < lenY && grayList[i-1][pyPprev]; for (byte j = pyPprev; ++j < pyP;) { if (grayList[i-1][j] && !prevFound) { p = new byte[] {i, j, 1}; orderedPoints.add(p); } else if (!grayList[i-1][j] && prevFound) { p = new byte[] {i, j, 1}; orderedPoints.add(p); } prevFound = grayList[i-1][j]; } } // <<< } p = new byte[] {i, pyP, 1}; orderedPoints.add(p); pyPprev = pyP; } // ----------- // find new start position cleanup = true; if (++i < lenX) { for (byte j = pyN; j < pyP; j++) { if (bits[i][j]) { start = j; cleanup = false; break; } } } // stop searching (no connection found) if (cleanup) { // add closure points p = new byte[] {i, pyN, -1}; orderedPoints.add(p); p = new byte[] {i, pyP, 1}; orderedPoints.add(p); if (fixEdges) { // >>> // add final edge points if (i < lenX) { boolean prevFound = grayList[i][pyP-1]; for (byte j = (byte) (pyP - 2); j >= pyN; j--) { if (grayList[i][j] && !prevFound) { p = new byte[] {i, (byte) (j+1), 1}; orderedPoints.add(p); } else if (!grayList[i][j] && prevFound) { p = new byte[] {i, (byte) (j+1), 1}; orderedPoints.add(p); } prevFound = grayList[i][j]; } } // <<< } break; } } result.addAll(triangulate(orderedPoints, fixEdges)); } } } return result; } }