/* * Copyright (c) 2010 Stephen A. Pratt * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.critterai.nmgen; import java.util.ArrayList; import java.util.Hashtable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Builds an convex polygon mesh consisting of variable sized polygons. * The mesh is generated from contour data contained by a {@link ContourSet} * object. * <p> * <a href= * "http://www.critterai.org/projects/nmgen/images/stage_polygon_mesh.png" * target="_parent"> <img class="insert" height="465" src= * "http://www.critterai.org/projects/nmgen/images/stage_polygon_mesh.jpg" * width="620" /> </a> * </p> * * @see <a href="http://www.critterai.org/nmgen_polygen" target="_parent">Convex * Polygon Generation</a> * @see PolyMeshField */ public final class PolyMeshFieldBuilder { /* * Design notes: * * Recast Reference: rcBuildPolyMesh in RecastMesh.cpp * * Never add setters. Configuration should remain immutable to keep * the class thread friendly. */ private static final Logger logger = LoggerFactory.getLogger(PolyMeshFieldBuilder.class); /* * Flag and associated deflag. Used during triangulation. */ private static final int FLAG = 0x80000000; private static final int DEFLAG = 0x0fffffff; /** * IMPORTANT: Only use this value during creation of the * {@link PolyMeshField} objects. After that, use the value from the * object since the object may alter the value. */ private final int mMaxVertsPerPoly; /** * Constructor. * * @param maxVertsPerPoly * The maximum vertices per polygon. The builder * will not create polygons with more than this number of vertices. */ public PolyMeshFieldBuilder(final int maxVertsPerPoly) { this.mMaxVertsPerPoly = maxVertsPerPoly; } /** * Builds a convex polygon mesh from the provided contour set. * <p> * This build algorithm will fail and return null if the {@link ContourSet} * contains any single contour with more than 0x0fffffff vertices. * * @param contours * A properly populated contour set. * @return The result of the build operation. */ public PolyMeshField build(final ContourSet contours) { // Initialize if ((contours == null) || (contours.size() == 0)) { return null; } // Construct the result object. final PolyMeshField result = new PolyMeshField(contours.boundsMin(), contours.boundsMax(), contours.cellSize(), contours.cellHeight(), this.mMaxVertsPerPoly); // Number of vertices found in the source. int sourceVertCount = 0; // The maximum possible number of polygons assuming that all will // be triangles. int maxPossiblePolygons = 0; // The maximum vertices found in a single contour. int maxVertsPerContour = 0; // Loop through all contours. Determine the values for the // variables above. for (int contourIndex = 0; contourIndex < contours.size(); contourIndex++) { final int count = contours.get(contourIndex).vertCount; sourceVertCount += count; maxPossiblePolygons += count - 2; maxVertsPerContour = Math.max(maxVertsPerContour, count); } if ((sourceVertCount - 1) > DEFLAG) { // Too man vertices to be able to process. Will run into the // the flag value. logger.warn("Polygon mesh generation failed: One or more" + " input contours contain more than the maximum" + " allowed vertices. (" + DEFLAG + ")"); return null; } /* * Holds the unique vertices found during triangulation. * This array is sized to hold the maximum possible vertices. * The actual number of vertices will be smaller due to duplication * in the source contours. */ final int[] globalVerts = new int[sourceVertCount * 3]; int globalVertCount = 0; /* * Holds polygon indices. * * The array is sized to hold the maximum possible polygons. * The actual number will be significantly smaller once polygons * are merged. * * Where mvpp = maximum vertices per polygon: * * Each polygon entry is mvpp. The first instance of NULL_INDEX means * the end of poly indices. * * Example: If nvp = 6 and the the polygon has 4 vertices -> * (1, 3, 4, 8, NULL_INDEX, NULL_INDEX) * then (1, 3, 4, 8) defines the polygon. */ final int[] globalPolys = new int[maxPossiblePolygons * this.mMaxVertsPerPoly]; // Fill with null index. for (int i = 0; i < globalPolys.length; i++) { globalPolys[i] = PolyMeshField.NULL_INDEX; } final int[] globalRegions = new int[maxPossiblePolygons]; int globalPolyCount = 0; /* * Holds information that allows mapping of contour vertex indices to * shared vertex indices. (i.e. Index of vertex in contour.verts[] * to index of vertex in within this operation.) * * index (key): The original vertex index of the contour. * value in array: The vertex index in the shared vertex array. * * This is a working variable whose content is meaningless between * iterations. It will contain cross-iteration trash. But that is * OK because of the way the array is used. (I.e. Trash data left * over from a previous iteration will never be accessed in the * current iteration.) */ final int[] contourToGlobalIndicesMap = new int[maxVertsPerContour]; /* * Key = Hash representing a unique vertex location. * Value = The index of the vertex in the global vertices array. * When a new vertex is found, it is added to the vertices array and * its global index stored in this hash table. If a duplicate is * found, the value from this table is used. * There will always be duplicate vertices since different contours * are connected by these duplicate vertices. */ final Hashtable<Integer, Integer> vertIndices = new Hashtable<Integer, Integer>(); // Each list is initialized to a size that will minimize resizing. final ArrayList<Integer> workingIndices = new ArrayList<Integer>(maxVertsPerContour); final ArrayList<Integer> workingTriangles = new ArrayList<Integer>(maxVertsPerContour); // Various working variables. // (Values are meaningless outside of the iteration.) final int[] workingPolys = new int[(maxVertsPerContour + 1) * this.mMaxVertsPerPoly]; int workingPolyCount = 0; final int[] mergeInfo = new int[3]; final int[] mergedPoly = new int[this.mMaxVertsPerPoly]; // Process all contours. for (int contourIndex = 0; contourIndex < contours.size(); contourIndex++) { final Contour contour = contours.get(contourIndex); if (contour.verts.length < (3 * 4)) { // This indicates a problem with contour creation // since the contour builder should detect for this. logger.warn("Polygon generation failure: Contour has " + "too few vertices. Bad input data. Region " + contour.regionID); continue; } // Create working indices for the contour vertices. workingIndices.clear(); for (int i = 0; i < contour.vertCount; i++) { workingIndices.add(i); } // Triangulate the contour. final int triangleCount = triangulate(contour.verts, workingIndices, workingTriangles); if (triangleCount <= 0) { /* * Failure of the triangulation. * This is known to occur if the source polygon is * self-intersecting or the source region contains internal * holes. In both cases, the problem is likely due to bad * region formation. */ logger.warn("Polygon generation failure: Could not" + " triangulate contour. Region " + contour.regionID); continue; } /* * Loop through the vertices in this contour. * For new vertices (not seen in previous contours) get a new * index and add it to the global vertices array. */ for (int iContourVert = 0; iContourVert < contour.vertCount; iContourVert++) { final int pContourVert = iContourVert * 4; final int vertHash = getHashCode(contour.verts[pContourVert], contour.verts[pContourVert + 1], contour.verts[pContourVert + 2]); Integer iGlobalVert = vertIndices.get(vertHash); if (iGlobalVert == null) { // This is the first time this vertex has been seen. // Assign it an index and add it to the vertex array. iGlobalVert = globalVertCount; globalVertCount++; vertIndices.put(vertHash, iGlobalVert); globalVerts[iGlobalVert * 3] = contour.verts[pContourVert]; globalVerts[(iGlobalVert * 3) + 1] = contour.verts[pContourVert + 1]; globalVerts[(iGlobalVert * 3) + 2] = contour.verts[pContourVert + 2]; } // Creat the map entry. Contour vertex index -> global // vertex index. contourToGlobalIndicesMap[iContourVert] = iGlobalVert; } // Initialize the working polygon array. for (int i = 0; i < workingPolys.length; i++) { workingPolys[i] = PolyMeshField.NULL_INDEX; } // Load the triangles into to the working polygon array, updating // indices in the process. workingPolyCount = 0; for (int i = 0; i < triangleCount; i++) { /* * The working triangles list contains vertex index data * from the contour. The working polygon array needs the * global vertex index. So the indices mapping array created * above is used to do the conversion. */ workingPolys[workingPolyCount * this.mMaxVertsPerPoly] = contourToGlobalIndicesMap[workingTriangles.get(i * 3)]; workingPolys[(workingPolyCount * this.mMaxVertsPerPoly) + 1] = contourToGlobalIndicesMap[workingTriangles.get((i * 3) + 1)]; workingPolys[(workingPolyCount * this.mMaxVertsPerPoly) + 2] = contourToGlobalIndicesMap[workingTriangles.get((i * 3) + 2)]; workingPolyCount++; } if (this.mMaxVertsPerPoly > 3) { // Merging of triangles into larger polygons is permitted. // Continue until no polygons can be found to merge. // http://www.critterai.org/nmgen_polygen#mergepolys while (true) { int longestMergeEdge = -1; int pBestPolyA = -1; int iPolyAVert = -1; // Start of the shared edge. int pBestPolyB = -1; int iPolyBVert = -1; // Start of the shared edge. // Loop through all but the last polygon looking for the // best polygons to merge in this iteration. for (int iPolyA = 0; iPolyA < (workingPolyCount - 1); iPolyA++) { for (int iPolyB = iPolyA + 1; iPolyB < workingPolyCount; iPolyB++) { // Can polyB merge with polyA? getPolyMergeInfo(iPolyA * this.mMaxVertsPerPoly, iPolyB * this.mMaxVertsPerPoly, workingPolys, globalVerts, result.maxVertsPerPoly(), mergeInfo); if (mergeInfo[0] > longestMergeEdge) { // polyB has the longest shared edge with // polyA found so far. Save the merge // information. longestMergeEdge = mergeInfo[0]; pBestPolyA = iPolyA * this.mMaxVertsPerPoly; iPolyAVert = mergeInfo[1]; pBestPolyB = iPolyB * this.mMaxVertsPerPoly; iPolyBVert = mergeInfo[2]; } } } if (longestMergeEdge <= 0) { // No valid merges found during this iteration. break; } // Found polygons to merge. Perform the merge. // Prepare the merged polygon array. for (int i = 0; i < mergedPoly.length; i++) { mergedPoly[i] = PolyMeshField.NULL_INDEX; } // Get the size of each polygon. final int vertCountA = PolyMeshField.getPolyVertCount(pBestPolyA, workingPolys, result.maxVertsPerPoly()); final int vertCountB = PolyMeshField.getPolyVertCount(pBestPolyB, workingPolys, result.maxVertsPerPoly()); int position = 0; /* * Fill the mergedPoly array. * Start the vertex at the end of polygon A's shared edge. * Add all vertices until looping back to the vertex just * before the start of the shared edge. Repeat for * polygon B. * * Duplicate vertices are avoided, while ensuring we get * all vertices, since each loop drops the vertex that * starts its polygon's shared edge and: * * PolyAStartVert == PolyBEndVert and * PolyAEndVert == PolyBStartVert. */ for (int i = 0; i < (vertCountA - 1); i++) { mergedPoly[position++] = workingPolys[pBestPolyA + ((iPolyAVert + 1 + i) % vertCountA)]; } for (int i = 0; i < (vertCountB - 1); i++) { mergedPoly[position++] = workingPolys[pBestPolyB + ((iPolyBVert + 1 + i) % vertCountB)]; } // Copy the merged polygon over the top of polygon A. System.arraycopy(mergedPoly, 0, workingPolys, pBestPolyA, this.mMaxVertsPerPoly); // Remove polygon B by shifting all information to the // left by one polygon, starting at polygon B. System.arraycopy(workingPolys, pBestPolyB + this.mMaxVertsPerPoly, workingPolys, pBestPolyB, workingPolys.length - pBestPolyB - this.mMaxVertsPerPoly); workingPolyCount--; } } // Polygon creation for this contour is complete. // Add polygons to the global polygon array and store region // information. for (int i = 0; i < workingPolyCount; i++) { // Copy the polygon from the working array to the // correct position in the global array. System.arraycopy(workingPolys, i * this.mMaxVertsPerPoly, globalPolys, globalPolyCount * this.mMaxVertsPerPoly, this.mMaxVertsPerPoly); globalRegions[globalPolyCount] = contour.regionID; globalPolyCount++; } } /* * Transfer global array information into instance fields. * Could have loaded data directly into instance fields and saved this * processing cost. But this method has memory benefits since it is * not necessary to oversize the instance arrays. */ // Transfer vertex data. result.verts = new int[globalVertCount * 3]; System.arraycopy(globalVerts, 0, result.verts, 0, globalVertCount * 3); /* * Transfer polygon indices data. * * The global polygon array is half the size of the instance polygon * array since the instance polygon array also contains edge adjacency * information. So array copy can't be used. * * Instead, copy the global polygon array over to instance polygon * array in blocks and initialize the instance polygon array's * adjacency information. */ result.polys = new int[globalPolyCount * this.mMaxVertsPerPoly * 2]; for (int iPoly = 0; iPoly < globalPolyCount; iPoly++) { final int pPoly = iPoly * this.mMaxVertsPerPoly; for (int offset = 0; offset < this.mMaxVertsPerPoly; offset++) { // Transfer index information. result.polys[(pPoly * 2) + offset] = globalPolys[pPoly + offset]; // Initialize edge's adjacency field. result.polys[(pPoly * 2) + this.mMaxVertsPerPoly + offset] = PolyMeshField.NULL_INDEX; } } // Transfer region data. result.polyRegions = new int[globalPolyCount]; System.arraycopy(globalRegions, 0, result.polyRegions, 0, globalPolyCount); // Build polygon adjacency information. buildAdjacencyData(result); return result; } /** * The maximum vertices per polygon. The builder will not create * polygons with more than this number of vertices. * * @return The maximum vertices per polygon. */ public int maxVertsPerPoly() { return this.mMaxVertsPerPoly; } /** * Searches all polygons and adds adjacency data to the * {@link PolyMeshField#polys} array. * <p> * All other data initialization must have been completed before calling * this operation. It is expected that all adjacency fields within the * {@link PolyMeshField#polys} array have been initialized to NULL_INDEX * before calling this operation. * * @param mesh * The mesh to use. */ private static void buildAdjacencyData(final PolyMeshField mesh) { final int vertCount = mesh.verts.length / 3; // Purposely using the region count to avoid the division. final int polyCount = mesh.polyRegions.length; final int maxEdgeCount = polyCount * mesh.maxVertsPerPoly(); /* * Holds edge information * * IMPORTANT: This array does not catalog all edges. It is only * guaranteed to catalog all shared edges. It will contain only a * sub-set of border edges, which are edges only connected to a * single polygon. * * Format: * 0: Index of primary vertex connected to the edge. This index's * value will always be less than the value of the secondary index. * 1: Index of secondary vertex connected to the edge. * 2: Index of polygon A connected to this edge. * 3: Polygon A vertex offset. * 4: Index of polygon B connected to this edge. * (Or NULL_INDEX if this is a border edge.) * 5: Polygon B vertex offset. * (Only meaningful if 4 != NULL_INDEX.) */ final int[] edges = new int[maxEdgeCount * 6]; int edgeCount = 0; /* * An array used in edge searches based on an edge's primary index. * * Index: Vertex index * Value: An index to an edge in the edges array that has the vertex * as its primary vertex. * * Example of use: * * Vertex index = 10; * startEdge[10] = 8 -> This vertex is the primary vertex for edge 8. * edges[8 * 6] <- Edge definition. * nextEdge[8] = 12 -> This vertex is also the primary vertex for * edge 12. * nextEdge[12] = 15 -> This vertex is also the primary vertex for * edge 15. * nextEdge[15] = NULL_INDEX -> This edge is not the primary index * for any further edges. * * If the value for a vertex is NULL_INDEX then the vertex is not a * primary vertex for any known edge. (This can occur because not * all border edges are cataloged by this algorithm.) */ final int[] startEdge = new int[vertCount]; for (int i = 0; i < startEdge.length; i++) { startEdge[i] = PolyMeshField.NULL_INDEX; } /* * An array used in edge searches. * * Use the startEdge array to get an index to a value in this array * in order to start an edge search. See doc for startEdge for details. * * Index: Edge Index Same index used for the edges array and as * values in the startEdge array. * * Value: Edge index of next edge attacked to the same vertex. * (Or NULL_INDEX if there are no more connected edges.) */ final int[] nextEdge = new int[maxEdgeCount]; /* * Loop through all polygons. * Find all shared edges. Populate all data arrays. * At the end of this loop, all data will be gathered except for * fields 4 and 5 in the edge array entries. */ for (int iPoly = 0; iPoly < polyCount; iPoly++) { final int pPoly = iPoly * mesh.maxVertsPerPoly() * 2; // Loop through each polygon vertex index. for (int vertOffset = 0; vertOffset < mesh.maxVertsPerPoly(); vertOffset++) { final int iVert = mesh.polys[pPoly + vertOffset]; if (iVert == PolyMeshField.NULL_INDEX) { // Reached the end of this polygon. break; } int iNextVert; if (((vertOffset + 1) >= mesh.maxVertsPerPoly()) || (mesh.polys[pPoly + vertOffset + 1] == PolyMeshField.NULL_INDEX)) { // Need to wrap to the beginning. This will only happen // once per iteration since the loop will be forced to // end during the next iteration. iNextVert = mesh.polys[pPoly]; } else { // The next vertex in the array is a valid vertex in // the polygon. iNextVert = mesh.polys[pPoly + vertOffset + 1]; } /* * This next check does several useful things: * - It ensures that a particular edge is never selected * twice since, for shared edges, this condition will * only exist for one of the polygons. * - Some border edges will be skipped entirely, saving * some processing time. */ if (iVert < iNextVert) { // This is an edge's primary vertex. // Set the vertices connected to this edge. edges[edgeCount * 6] = iVert; edges[(edgeCount * 6) + 1] = iNextVert; // Set the polygons associated with this edge. edges[(edgeCount * 6) + 2] = iPoly; edges[(edgeCount * 6) + 3] = vertOffset; // Default to unconnected. (Border edge.) edges[(edgeCount * 6) + 4] = PolyMeshField.NULL_INDEX; edges[(edgeCount * 6) + 5] = PolyMeshField.NULL_INDEX; /* * Update the search arrays. * The first time a vertex is assigned as an edge's * primary vertex, the NULL_INDEX will be copied into * nextEdge, indicating the end of a vertex chain. * * The 2nd time a vertex is assign as an edge's primary * vertex, the original edge pointer from startEdge * is added to nextEdge, and startEdge is assigned * the new starting edge. This results in a stack-like * storage mechanism. * * The process is repeated every time the vertex is * assigned as a primary vertex, creating a chain. */ nextEdge[edgeCount] = startEdge[iVert]; startEdge[iVert] = edgeCount; edgeCount++; } } } /* * Loop through all polygons. * Find the the 2nd polygon's information for all shared edges. * (Fields 4 and 5 of the edge array entries.) */ for (int iPoly = 0; iPoly < polyCount; iPoly++) { final int pPoly = iPoly * mesh.maxVertsPerPoly() * 2; // Loop through each polygon vertex index. for (int vertOffset = 0; vertOffset < mesh.maxVertsPerPoly(); ++vertOffset) { final int iVert = mesh.polys[pPoly + vertOffset]; if (iVert == PolyMeshField.NULL_INDEX) { // Reached the end of this polygon. break; } int iNextVert; if (((vertOffset + 1) >= mesh.maxVertsPerPoly()) || (mesh.polys[pPoly + vertOffset + 1] == PolyMeshField.NULL_INDEX)) { // Need to wrap to the beginning. This will only happen // once per iteration since the loop will be forced to // end during the next iteration. iNextVert = mesh.polys[pPoly]; } else { // The next vertex in the array is a valid vertex in // the polygon. iNextVert = mesh.polys[pPoly + vertOffset + 1]; } // Note that this next conditional is reversed from that // used in the previous loop. if (iVert > iNextVert) { /* * iVert is NOT a primary vertex in this case. We are * looking for the "other" polygon that shares this edge. * If there is another polygon sharing this edge, its * primary vertex will be iNextVert. * * Climb the edge chain for iNextVert, looking for an * edge that has iVert as its secondary vertex. */ // Loop halts at the end of the chain. for (int edgeIndex = startEdge[iNextVert]; edgeIndex != PolyMeshField.NULL_INDEX; edgeIndex = nextEdge[edgeIndex]) { if (edges[(edgeIndex * 6) + 1] == iVert) { // Found a shared edge. Assign this polygon // as the secondary connected polygon. edges[(edgeIndex * 6) + 4] = iPoly; edges[(edgeIndex * 6) + 5] = vertOffset; break; } } } } } // All necessary data has been gathered. Any edge in the edge array // that has both polygons assigned is a shared edge. // Store adjacency information. // Loop through all edges. for (int pEdge = 0; pEdge < edgeCount; pEdge += 6) { if (edges[pEdge + 4] != PolyMeshField.NULL_INDEX) { // The second polygon in this edge is set. // So this is a shared edge. final int pPolyA = edges[pEdge + 2] * mesh.maxVertsPerPoly() * 2; final int pPolyB = edges[pEdge + 4] * mesh.maxVertsPerPoly() * 2; // In the second section of the polygon definition, where // connection information is stored, put the polygon index // into the same position as the edge's primary vertex. mesh.polys[pPolyA + mesh.maxVertsPerPoly() + edges[pEdge + 3]] = edges[pEdge + 4]; mesh.polys[pPolyB + mesh.maxVertsPerPoly() + edges[pEdge + 5]] = edges[pEdge + 2]; } } } /** * Provides a hash value unique to the combination of values. * * @param x * The vertices x-value. (x, y, z) * @param y * The vertices y-value. (x, y, z) * @param z * The vertices z-value. (x, y, z) * @return A hash that is unique to the vertex. */ private static int getHashCode(final int x, final int y, final int z) { /* * Note: Tried the standard eclipse hash generation method. But * it resulted in non-unique hash values during testing. Switched * to this method. * Hex values are arbitrary prime numbers. */ return (0x8da6b343 * x) + (0xd8163841 * y) + (0xcb1ab31f * z); } /** * Returns the index incremented by one, or if the increment causes * an out of range high the minimum allowed index is returned. * (e.g. Wrapping) * * @param i * The index. * @param n * The size of the array the index belongs to. * @return Returns the index incremented by one with wrapping. */ private static int getNextIndex(final int i, final int n) { return (i + 1) < n ? i + 1 : 0; } /** * Checks two polygons to see if they can be merged. If a merge is * allowed, provides data via the outResult argument. * <p> * outResult will be an array of size 3 with the following information: * </p> * <p> * 0: The lenghtSq of the edge shared between the polygons.<br/> * 1: The index (not pointer) of the start of the shared edge in polygon A.<br/> * 2: The index (not pointer) of the start of the shared edge in polygon B.<br/> * </p> * <p> * A value of -1 at index zero indicates one of the following: * </p> * <ul> * <li>The polygons cannot be merged because they would contain too many * vertices.</li> * <li>The polygons do not have a shared edge.</li> * <li>Merging the polygons would result in a concave polygon.</li> * </ul> * <p> * To convert the values at indices 1 and 2 to pointers: (polyPointer + * value) * </p> * * @param polyAPointer * The pointer to the start of polygon A in the * polys argument. * @param polyBPointer * The pointer to the start of polygon B in the * polys argument. * @param polys * An array of polygons in the form: * (vert1, vert2, vert3, ..., vertN, NULL_INDEX). * The null index terminates every polygon. This permits polygons * with different vertex counts. * @param verts * The vertex data associated with the polygons. * @param outResult * An array of size three which contains merge information. */ private static void getPolyMergeInfo(final int polyAPointer, final int polyBPointer, final int[] polys, final int[] verts, final int maxVertsPerPoly, final int[] outResult) { outResult[0] = -1; // Default to invalid merge outResult[1] = -1; outResult[2] = -1; final int vertCountA = PolyMeshField.getPolyVertCount(polyAPointer, polys, maxVertsPerPoly); final int vertCountB = PolyMeshField.getPolyVertCount(polyBPointer, polys, maxVertsPerPoly); // If the merged polygon would would have to many vertices, do not // merge. Subtracting two since to take into account the effect of // a merge. if (((vertCountA + vertCountB) - 2) > maxVertsPerPoly) { return; } /* * Check if the polygons share an edge. * * Loop through all of vertices for polygonA and extract its edge. * (vertA -> vertANext) Then loop through all vertices for polygonB * and check to see if any of its edges use the same vertices as * polygonA. */ for (int iPolyVertA = 0; iPolyVertA < vertCountA; iPolyVertA++) { // Get the vertex indices for the polygonA edge final int iVertA = polys[polyAPointer + iPolyVertA]; final int iVertANext = polys[polyAPointer + getNextIndex(iPolyVertA, vertCountA)]; // Search polygonB for matches. for (int iPolyVertB = 0; iPolyVertB < vertCountB; iPolyVertB++) { // Get the vertex indices for the polygonB edge. final int iVertB = polys[polyBPointer + iPolyVertB]; final int iVertBNext = polys[polyBPointer + getNextIndex(iPolyVertB, vertCountB)]; if ((iVertA == iVertBNext) && (iVertANext == iVertB)) { // The vertex indices for this edge are the same and // sequenced in opposite order. So the edge is shared. outResult[1] = iPolyVertA; outResult[2] = iPolyVertB; } } } if (outResult[1] == -1) { // No common edge, cannot merge. return; } /* * Check to see if the merged polygon would be convex. * * Gets the vertices near the section where the merge would occur. * Do they form a concave section? If so, the merge is invalid. * * Note that the following algorithm is only valid for clockwise * wrapped convex polygons. */ int pSharedVertMinus, pSharedVert, pSharedVertPlus; pSharedVertMinus = polys[polyAPointer + getPreviousIndex(outResult[1], vertCountA)] * 3; pSharedVert = polys[polyAPointer + outResult[1]] * 3; pSharedVertPlus = polys[polyBPointer + ((outResult[2] + 2) % vertCountB)] * 3; if (!isLeft(verts[pSharedVert], verts[pSharedVert + 2], verts[pSharedVertMinus], verts[pSharedVertMinus + 2], verts[pSharedVertPlus], verts[pSharedVertPlus + 2])) { /* * The shared vertex (center) is not to the left of segment * vertMinus->vertPlus. For a clockwise wrapped polygon, this * indicates a concave section. Merged polygon would be concave. * Invalid merge. */ return; } pSharedVertMinus = polys[polyBPointer + getPreviousIndex(outResult[2], vertCountB)] * 3; pSharedVert = polys[polyBPointer + outResult[2]] * 3; pSharedVertPlus = polys[polyAPointer + ((outResult[1] + 2) % vertCountA)] * 3; if (!isLeft(verts[pSharedVert], verts[pSharedVert + 2], verts[pSharedVertMinus], verts[pSharedVertMinus + 2], verts[pSharedVertPlus], verts[pSharedVertPlus + 2])) { /* * The shared vertex (center) is not to the left of segment * vertMinus->vertPlus. For a clockwise wrapped polygon, this * indicates a concave section. Merged polygon would be concave. * Invalid merge. */ return; } // Get the vertex indices that form the shared edge. pSharedVertMinus = polys[polyAPointer + outResult[1]] * 3; pSharedVert = polys[polyAPointer + getNextIndex(outResult[1], vertCountA)] * 3; // Store the lengthSq of the shared edge. final int deltaX = verts[pSharedVertMinus + 0] - verts[pSharedVert + 0]; final int deltaZ = verts[pSharedVertMinus + 2] - verts[pSharedVert + 2]; outResult[0] = (deltaX * deltaX) + (deltaZ * deltaZ); } /** * Returns the index decremented by one, or if the decrement causes an * out of range low the maximum allowed index is returned. (e.g. Wrapping) * * @param i * The index. * @param n * The size of the array the index belongs to. * @return Returns the index decremented by one with wrapping. */ private static int getPreviousIndex(final int i, final int n) { return (i - 1) >= 0 ? i - 1 : n - 1; } /** * The absolute value of the returned value is two times the area of the * triangle defined by points (A, B, C). * <p> * A positive value indicates: * </p> * <ul> * <li>Counterclockwise wrapping of the points.</li> * <li>Point B lies to the right of line AC, looking from A to C.</li> * </ul> * <p> * A negative value indicates: * </p> * <ul> * <li>Clockwise wrapping of the points.</li> * <li>Point B lies to the left of line AC, looking from A to C.</li> * </ul> * <p> * A value of zero indicates that all points are collinear or represent the * same point. * </p> * <p> * This is a fast operation. * <p> * * @param ax * The x-value for point (ax, ay) for vertex A of the triangle. * @param ay * The y-value for point (ax, ay) for vertex A of the triangle. * @param bx * The x-value for point (bx, by) for vertex B of the triangle. * @param by * The y-value for point (bx, by) for vertex B of the triangle. * @param cx * The x-value for point (cx, cy) for vertex C of the triangle. * @param cy * The y-value for point (cx, cy) for vertex C of the triangle. * @return The signed value of two times the area of the triangle defined * by the points (A, B, C). */ private static int getSignedAreaX2(final int ax, final int ay, final int bx, final int by, final int cx, final int cy) { /* * References: * * http://softsurfer.com/Archive/algorithm_0101/algorithm_0101.htm * #Modern%20Triangles * http://mathworld.wolfram.com/TriangleArea.html (Search for "signed".) */ return ((bx - ax) * (cy - ay)) - ((cx - ax) * (by - ay)); } /** * Returns TRUE if the line segment AB intersects any edges not already * connected to one of the two vertices. * <p> * The test is only performed on the xz-plane. * </p> * <p> * Assumptions: * </p> * <ul> * <li>The vertices and indices arguments define a valid simple polygon with * vertices wrapped clockwise.</li> * <li>indexA != indexB</li> * </ul> * <p> * Behavior is undefined if the arguments to not meet these assumptions * </p> * * @param indexA * An polygon index of a vertex that will form segment AB. * @param indexB * An polygon index of a vertex that will form segment AB. * @param verts * The vertices array in the form (x, y, z, id). The value * stored at the id position is not relevant to this operation. * @param indices * A simplpe polygon wrapped clockwise. * @return TRUE if the line segment AB intersects any edges not already * connected to one of the two vertices. Otherwise FALSE. */ private static boolean hasIllegalEdgeIntersection(final int indexA, final int indexB, final int[] verts, final ArrayList<Integer> indices) { // Get pointers to the primary vertices being tested. final int pVertA = (indices.get(indexA) & DEFLAG) * 4; final int pVertB = (indices.get(indexB) & DEFLAG) * 4; // Loop through the polygon edges. for (int iPolyEdgeBegin = 0; iPolyEdgeBegin < indices.size(); iPolyEdgeBegin++) { final int iPolyEdgeEnd = getNextIndex(iPolyEdgeBegin, indices.size()); if (!((iPolyEdgeBegin == indexA) || (iPolyEdgeBegin == indexB) || (iPolyEdgeEnd == indexA) || (iPolyEdgeEnd == indexB))) { // Neither of the test indices are endpoints of this edge. // Get pointers for this edge's verts. final int pEdgeVertBegin = (indices.get(iPolyEdgeBegin) & DEFLAG) * 4; final int pEdgeVertEnd = (indices.get(iPolyEdgeEnd) & DEFLAG) * 4; if (((verts[pEdgeVertBegin] == verts[pVertA]) && (verts[pEdgeVertBegin + 2] == verts[pVertA + 2])) || ((verts[pEdgeVertBegin] == verts[pVertB]) && (verts[pEdgeVertBegin + 2] == verts[pVertB + 2])) || ((verts[pEdgeVertEnd] == verts[pVertA]) && (verts[pEdgeVertEnd + 2] == verts[pVertA + 2])) || ((verts[pEdgeVertEnd] == verts[pVertB]) && (verts[pEdgeVertEnd + 2] == verts[pVertB + 2]))) { /* * One of the test vertices is co-located on the xz plane * with one of the endpoints of this edge. (This is a * test of the actual position of the verts rather than * simply the index check performed earlier.) * Skip this edge. */ continue; } /* * This edge is not connected to either of the test vertices. * If line segment AB intersects with this edge, then the * intersection is illegal. * I.e. New edges cannot cross existing edges. */ if (Geometry.segmentsIntersect(verts[pVertA], verts[pVertA + 2], verts[pVertB], verts[pVertB + 2], verts[pEdgeVertBegin], verts[pEdgeVertBegin + 2], verts[pEdgeVertEnd], verts[pEdgeVertEnd + 2])) { return true; } } } return false; } /** * Returns TRUE if point P is to the left of line AB when looking * from A to B. * * @param px * The x-value of the point to test. * @param py * The y-value of the point to test. * @param ax * The x-value of the point (ax, ay) that is point A on line AB. * @param ay * The y-value of the point (ax, ay) that is point A on line AB. * @param bx * The x-value of the point (bx, by) that is point B on line AB. * @param by * The y-value of the point (bx, by) that is point B on line AB. * @return TRUE if point P is to the left of line AB when looking * from A to B. Otherwise FALSE. */ private static boolean isLeft(final int px, final int py, final int ax, final int ay, final int bx, final int by) { return getSignedAreaX2(ax, ay, px, py, bx, by) < 0; } /** * Returns TRUE if point P is to the left of line AB when looking * from A to B or is collinear with line AB. * * @param px * The x-value of the point to test. * @param py * The y-value of the point to test. * @param ax * The x-value of the point (ax, ay) that is point A on line AB. * @param ay * The y-value of the point (ax, ay) that is point A on line AB. * @param bx * The x-value of the point (bx, by) that is point B on line AB. * @param by * The y-value of the point (bx, by) that is point B on line AB. * @return TRUE if point P is to the left of line AB when looking * from A to B, or is collinear with line AB. Otherwise FALSE. */ private static boolean isLeftOrCollinear(final int px, final int py, final int ax, final int ay, final int bx, final int by) { return getSignedAreaX2(ax, ay, px, py, bx, by) <= 0; } /** * Returns TRUE if point P is to the right of line AB when looking * from A to B. * * @param px * The x-value of the point to test. * @param py * The y-value of the point to test. * @param ax * The x-value of the point (ax, ay) that is point A on line AB. * @param ay * The y-value of the point (ax, ay) that is point A on line AB. * @param bx * The x-value of the point (bx, by) that is point B on line AB. * @param by * The y-value of the point (bx, by) that is point B on line AB. * @return TRUE if point P is to the right of line AB when looking * from A to B. */ private static boolean isRight(final int px, final int py, final int ax, final int ay, final int bx, final int by) { return getSignedAreaX2(ax, ay, px, py, bx, by) > 0; } /** * Returns TRUE if point P is to the right of or on line AB when looking * from A to B. * * @param px * The x-value of the point to test. * @param py * The y-value of the point to test. * @param ax * The x-value of the point (ax, ay) that is point A on line AB. * @param ay * The y-value of the point (ax, ay) that is point A on line AB. * @param bx * The x-value of the point (bx, by) that is point B on line AB. * @param by * The y-value of the point (bx, by) that is point B on line AB. * @return TRUE if point P is to the right of or on line AB when looking * from A to B. */ private static boolean isRightOrCollinear(final int px, final int py, final int ax, final int ay, final int bx, final int by) { return getSignedAreaX2(ax, ay, px, py, bx, by) >= 0; } /** * Returns TRUE if the line segment formed by vertex A and vertex B will * form a valid partition of the polygon. * <p> * I.e. New line segment AB is internal to the polygon and will not cross * existing line segments. * </p> * <p> * The test is only performed on the xz-plane. * </p> * <p> * Assumptions: * </p> * <ul> * <li>The vertices and indices arguments define a valid simple polygon with * vertices wrapped clockwise.</li> * <li>indexA != indexB</li> * </ul> * <p> * Behavior is undefined if the arguments to not meet these assumptions * </p> * * @param indexA * An polygon index of a vertex that will form segment AB. * @param indexB * An polygon index of a vertex that will form segment AB. * @param verts * The vertices array in the form (x, y, z, id). The value * stored at the id position is not relevant to this operation. * @param indices * A simplpe polygon wrapped clockwise. * @return TRUE if the line segment formed by vertex A and vertex B will * form a valid partition of the polygon. Otherwise false. */ private static boolean isValidPartition(final int indexA, final int indexB, final int[] verts, final ArrayList<Integer> indices) { /* * First check whether the segment AB lies within the internal * angle formed at A. (This is the faster check.) * If it does, then perform the more costly check. */ return liesWithinInternalAngle(indexA, indexB, verts, indices) && !hasIllegalEdgeIntersection(indexA, indexB, verts, indices); } /** * Returns TRUE if vertex B lies within the internal angle of the polygon * at vertex A. * * <p> * Vertex B does not have to be within the polygon border. It just has be be * within the area encompassed by the internal angle formed at vertex A. * </p> * * <p> * This operation is a fast way of determining whether a line segment can * possibly form a valid polygon partition. If this test returns FALSE, then * more expensive checks can be skipped. * </p> * <a href="http://www.critterai.org/nmgen_polygen#anglecheck" * >Visualizations</a> * <p> * Special case: FALSE is returned if vertex B lies directly on either of * the rays cast from vertex A along its associated polygon edges. So the * test on vertex B is exclusive of the polygon edges. * </p> * <p> * The test is only performed on the xz-plane. * </p> * <p> * Assumptions: * </p> * <ul> * <li>The vertices and indices arguments define a valid simple polygon with * vertices wrapped clockwise.</li> * <li>indexA != indexB</li> * </ul> * <p> * Behavior is undefined if the arguments to not meet these assumptions * </p> * * @param indexA * An polygon index of a vertex that will form segment AB. * @param indexB * An polygon index of a vertex that will form segment AB. * @param verts * The vertices array in the form (x, y, z, id). The value * stored at the id position is not relevant to this operation. * @param indices * A simplpe polygon wrapped clockwise. * @return Returns TRUE if vertex B lies within the internal angle of * the polygon at vertex A. */ private static boolean liesWithinInternalAngle(final int indexA, final int indexB, final int[] verts, final ArrayList<Integer> indices) { // Get pointers to the main vertices being tested. final int pVertA = (indices.get(indexA) & DEFLAG) * 4; final int pVertB = (indices.get(indexB) & DEFLAG) * 4; // Get poitners to the vertices just before and just after vertA. final int pVertAMinus // The vertex just before A. = (indices.get(getPreviousIndex(indexA, indices.size())) & DEFLAG) * 4; final int pVertAPlus // The vert just after A. = (indices.get(getNextIndex(indexA, indices.size())) & DEFLAG) * 4; /* * First, find which of the two angles formed by the line segments * AMinus->A->APlus is internal to (pointing towards) the polygon. * Then test to see if B lies within the area formed by that angle. */ // TRUE if A is left of or on line AMinus->APlus if (isLeftOrCollinear(verts[pVertA], verts[pVertA + 2], verts[pVertAMinus], verts[pVertAMinus + 2], verts[pVertAPlus], verts[pVertAPlus + 2])) { return isLeft( // TRUE if B is left of line A->AMinus verts[pVertB], verts[pVertB + 2], verts[pVertA], verts[pVertA + 2], verts[pVertAMinus], verts[pVertAMinus + 2]) // TRUE if B is right of line A->APlus && isRight(verts[pVertB], verts[pVertB + 2], verts[pVertA], verts[pVertA + 2], verts[pVertAPlus], verts[pVertAPlus + 2]); } /* * The angle internal to the polygon is > 180 degrees (reflex angle). * Test to see if B lies within the external (<= 180 degree) angle and * flip the result. (If B lies within the external angle, it can't * lie within the internal angle.) */ return !( // TRUE if B is left of or on line A->APlus isLeftOrCollinear(verts[pVertB], verts[pVertB + 2], verts[pVertA], verts[pVertA + 2], verts[pVertAPlus], verts[pVertAPlus + 2]) // TRUE if B is right of or on line A->AMinus && isRightOrCollinear(verts[pVertB], verts[pVertB + 2], verts[pVertA], verts[pVertA + 2], verts[pVertAMinus], verts[pVertAMinus + 2])); } /** * Attempts to triangluate a polygon. * <p> * Assumes the verts and indices arguments define a valid simple (concave or * convex) polygon with vertices wrapped clockwise. Otherwise behavior is * undefined. * </p> * * @param verts * The vertices that make up the polygon in the format * (x, y, z, id). The value stored at the id position is not relevant * to * this operation. * @param inoutIndices * A working array of indices that define the * polygon to be triangluated. The content is manipulated during the * operation and it will be left in an undefined state at the end of * the operation. (I.e. Its content will no longer be of any use.) * @param outTriangles * The indices which define the triangles derived * from the original polygon in the form * (t1a, t1b, t1c, t2a, t2b, t2c, ..., tna, tnb, tnc). The original * content of this argument is discarded prior to use. * @return The number of triangles generated. Or, if triangluation * failed, a negative number. */ private static int triangulate(final int[] verts, final ArrayList<Integer> inoutIndices, final ArrayList<Integer> outTriangles) { outTriangles.clear(); /* * Terminology, concepts and such: * * This algorithm loops around the edges of a polygon looking for * new internal edges to add that will partition the polygon into a * new valid triangle internal to the starting polygon. During each * iteration the shortest potential new edge is selected to form that * iteration's new triangle. * * Triangles will only be formed if a single new edge will create * a triangle. Two new edges will never be added during a single * iteration. This means that the triangulated portions of the * original polygon will only contain triangles and the only * non-triangle polygon will exist in the untrianglulated portion * of the original polygon. * * "Partition edge" refers to a potential new edge that will form a * new valid triangle. * * "Center" vertex refers to the vertex in a potential new triangle * which, if the triangle is formed, will be external to the * remaining untriangulated portion of the polygon. Since is * is now external to the polygon, it can't be used to form any * new triangles. * * Some documentation refers to "iPlus2" even though the variable is * not in scope or does not exist for that section of code. For * documentation purposes, iPlus2 refers to the 2nd vertex after the * primary vertex. * E.g.: i, iPlus1, and iPlus2. * * Visualizations: http://www.critterai.org/nmgen_polygen#triangulation */ // Loop through all vertices, flagging all indices that represent // a center vertex of a valid new triangle. for (int i = 0; i < inoutIndices.size(); i++) { final int iPlus1 = getNextIndex(i, inoutIndices.size()); final int iPlus2 = getNextIndex(iPlus1, inoutIndices.size()); if (isValidPartition(i, iPlus2, verts, inoutIndices)) { // A triangle formed by i, iPlus1, and iPlus2 will result // in a valid internal triangle. // Flag the center vertex (iPlus1) to indicate a valid triangle // location. inoutIndices.set(iPlus1, inoutIndices.get(iPlus1) | FLAG); } } /* * Loop through the vertices creating triangles. When there is only a * single triangle left, the operation is complete. * * When a valid triangle is formed, remove its center vertex. So for * each loop, a single vertex will be removed. * * At the start of each iteration the indices list is in the following * state: * - Represents a simple polygon representing the un-triangulated * portion of the original polygon. * - All valid center vertices are flagged. */ while (inoutIndices.size() > 3) { // Find the shortest new valid edge. // The minimum length found. int minLengthSq = -1; // The index for the start of the minimum length edge. int iMinLengthSqVert = -1; // NOTE: i and iPlus1 are defined in two different scopes in // this section. So be careful. // Loop through all indices in the remaining polygon. for (int i = 0; i < inoutIndices.size(); i++) { final int iPlus1 = getNextIndex(i, inoutIndices.size()); if ((inoutIndices.get(iPlus1) & FLAG) == FLAG) { // Indices i, iPlus1, and iPlus2 are known to form a // valid triangle. final int vert = (inoutIndices.get(i) & DEFLAG) * 4; final int vertPlus2 = (inoutIndices.get(getNextIndex(iPlus1, inoutIndices.size())) & DEFLAG) * 4; // Determine the length of the partition edge. // (i -> iPlus2) final int deltaX = verts[vertPlus2] - verts[vert]; final int deltaZ = verts[vertPlus2 + 2] - verts[vert + 2]; final int lengthSq = (deltaX * deltaX) + (deltaZ * deltaZ); if ((minLengthSq < 0) || (lengthSq < minLengthSq)) { // This is either the first valid new edge, or an edge // that is shorter than others previously found. // Select it. minLengthSq = lengthSq; iMinLengthSqVert = i; } } } if (iMinLengthSqVert == -1) { /* * Could not find a new triangle. Triangulation failed. * This happens if there are three or more vertices * left, but none of them are flagged as being a * potential center vertex. */ return -(outTriangles.size() / 3); } int i = iMinLengthSqVert; int iPlus1 = getNextIndex(i, inoutIndices.size()); // Add the new triangle to the output. outTriangles.add(inoutIndices.get(i) & DEFLAG); outTriangles.add(inoutIndices.get(iPlus1) & DEFLAG); outTriangles.add(inoutIndices.get(getNextIndex(iPlus1, inoutIndices.size())) & DEFLAG); /* * iPlus1, the "center" vert in the new triangle, is now external * to the untriangulated portion of the polygon. Remove it from * the indices list since it cannot be a member of any new * triangles. */ inoutIndices.remove(iPlus1); if ((iPlus1 == 0) || (iPlus1 >= inoutIndices.size())) { /* * The vertex removal has invalidated iPlus1 and/or i. So * force a wrap, fixing the indices so they reference the * correct indices again. This only occurs when the new * triangle is formed across the wrap location of the polygon. * Case 1: i = 14, iPlus1 = 15, iPlus2 = 0 * Case 2: i = 15, iPlus1 = 0, iPlus2 = 1; */ i = inoutIndices.size() - 1; iPlus1 = 0; } /* * At this point i and iPlus1 refer to the two indices from a * successful triangluation that will be part of another new * triangle. We now need to re-check these indices to see if they * can now be the center index in a potential new partition. */ if (isValidPartition(getPreviousIndex(i, inoutIndices.size()), iPlus1, verts, inoutIndices)) { inoutIndices.set(i, inoutIndices.get(i) | FLAG); } else { inoutIndices.set(i, inoutIndices.get(i) & DEFLAG); } if (isValidPartition(i, getNextIndex(iPlus1, inoutIndices.size()), verts, inoutIndices)) { inoutIndices.set(iPlus1, inoutIndices.get(iPlus1) | FLAG); } else { inoutIndices.set(iPlus1, inoutIndices.get(iPlus1) & DEFLAG); } } // Only three vertices remain. Add their triangle to the output list. outTriangles.add(inoutIndices.get(0) & DEFLAG); outTriangles.add(inoutIndices.get(1) & DEFLAG); outTriangles.add(inoutIndices.get(2) & DEFLAG); return outTriangles.size() / 3; } }