/* * 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.ArrayDeque; import java.util.ArrayList; import org.slf4j.LoggerFactory; /** * Builds an triangle mesh from {@link OpenHeightfield } and * {@link PolyMeshField} data. The polygon mesh field is triangulated and * detail added as needed to match the surface of the mesh to the surface * defined in the open heightfield. * <p> * <a href= * "http://www.critterai.org/projects/nmgen/images/stage_detail_mesh.png" * target="_parent"> <img class="insert" height="465" src= * "http://www.critterai.org/projects/nmgen/images/stage_detail_mesh.jpg" * width="620" /> </a> * </p> * * @see <a href="http://www.critterai.org/nmgen_detailgen" * target="_parent">Detail Mesh Generation</a> * @see TriangleMesh */ public final class DetailMeshBuilder { /* * Design notes: * * Recast Reference: rcBuildPolyMeshDetail in RecastMeshDetail.cpp * * Not adding configuration getters until they are needed. * Never add setters. Configuration should remain immutable to keep * the class thread friendly. */ /* * TODO: EVAL: Need to review the process of loading and getting values * from the height patch. * * Current design assumes that there are natural special cases that * result in vertices improperly showing as outside of the patch. But * it may be a design issue that needs fixing rather than true special * cases such as floating point errors. See getHeightWithinField() for * descriptions of the special cases currently being taken into account. */ /** * Represents height information for a portion (or patch) of a larger * heightfield. * <p> * Various fields indicate which portion of the source heightfield is * represented. * </p> * <p> * Unlike normal heightfields, this class cannot represent overlaps. Each * grid location can only hold a single data point. * </p> * <p> * Data is filled using a flood method. I.e. Start at a particular span in * the source height field, and flood outward to the patch's boundaries. * </p> * <p> * Since the value {@link #UNSET} is used to indicate the lack of data at a * particular location, the effective maximum height value is * {@link Integer#MAX_VALUE} - 1. * </p> * <p> * WARNING: It is critical that public fields be set correctly. Otherwise * operation exceptions may be thrown. * </p> */ private class HeightPatch { /** * Indicates that data at a particular grid location has not been set. * (Has no value.) After data has been loaded, this means * that there is no valid height information for the particular grid * location. */ public static final int UNSET = Integer.MAX_VALUE; /** * The width index for the origin of this patch. * (Should be a valid width index within the larger height field.) */ int minWidthIndex; /** * The depth index for the origin of this patch. * (Should be a valid depth index within the larger height field.) */ int minDepthIndex; /** * The width of this patch as measured from the patch's origin. */ int width; /** * The depth of this patch as measured from the patch's origin. */ int depth; /** * Height data for this patch. * <p> * Should normally use the {@link #setData(int, int, int) setData} and * {@link #getData(int, int) getData} operations to access data in this * array. * </p> * <p> * Storage: [(Depth Data) (Depth Data) ... (Depth Data)] So the location * of a particular data point is widthIndex * {@link #depth} + * depthIndex * </p> * <p> * Array should be sized such that it can hold at least {@link #width} x * {@link #depth} worth of data. * </p> */ int[] data; /** * Gets the height data for the patch location. * <p> * The indices are auto-clamped. * </p> * * @param globalWidthIndex * The width index of the grid location * from the source height field. * @param globalDepthIndex * The depth index of the grid location * from the source height field. * @return The data stored a the grid location. */ public int getData(final int globalWidthIndex, final int globalDepthIndex) { // Do not remove auto-clamping. It is an important feature. // See special case comments in getHeightWithinField() for // details on why. final int idx = (Math.min(Math.max(globalWidthIndex - this.minWidthIndex, 0), this.width - 1) * this.depth) + Math.min(Math.max(globalDepthIndex - this.minDepthIndex, 0), this.depth - 1); return this.data[idx]; } /** * Indicates whether or not a global index (width, height) is * within the bounds of the patch. * * @param globalWidthIndex * The width index from the patch's * source heightfield. * @param globalDepthIndex * The depth index from the patch's * source heightfield. * @return If the global index (width, height) is within the * bounds of the patch. */ public boolean isInPatch(final int globalWidthIndex, final int globalDepthIndex) { return ((globalWidthIndex >= this.minWidthIndex) && (globalDepthIndex >= this.minDepthIndex) && (globalWidthIndex < (this.minWidthIndex + this.width)) && (globalDepthIndex < (this.minDepthIndex + this.depth))); } /** * Sets all values in the data array to {@link #UNSET}. */ public void resetData() { if (this.data == null) { return; } for (int i = 0; i < this.data.length; i++) { this.data[i] = UNSET; } } /** * Sets the height data for the patch location. * <p> * WARNING: No argument validation is performed. Calling code should * perform all needed validations prior to calling this operation. * </p> * * @param globalWidthIndex * The width index of the grid location * from the source height field. * @param globalDepthIndex * The depth index of the grid location * from the source height field. * @param value * The data to store at the grid location. */ public void setData(final int globalWidthIndex, final int globalDepthIndex, final int value) { this.data[(((globalWidthIndex - this.minWidthIndex) * this.depth) + globalDepthIndex) - this.minDepthIndex] = value; } } private static final org.slf4j.Logger logger = LoggerFactory.getLogger(DetailMeshBuilder.class); /** * Information is undefined. (Not yet set.) */ private static final int UNDEFINED = -1; /** * This side of the edge is external to the main polygon. (It is part * of the polygon's hull.) * <p> * Edges internal to the polygon (i.e. those created during triangulation) * should never have this value. * </p> */ private static final int HULL = -2; /** * The maximum number of vertices allowed for a triangulated polygon mesh. * <p> * This value is arbitrary. * </p> */ private static final int MAX_VERTS = 256; /** * The maximum number of edges that a single edge can be broken into * during edge sampling. * <p> * This value is arbitrary. * </p> */ private static final int MAX_EDGES = 64; /** * Contour matching sample resolution. * <p> * Impacts how well the final mesh conforms to surface data in the * {@link OpenHeightfield} Higher values, closer conforming, higher final * triangle count. * </p> */ private final float mContourSampleDistance; /** * Contour matching maximum deviation. * <p> * Impacts how well the final mesh conforms to the original meshes surface * contour. Lower values, closer conforming, higher final triangle count. * </p> */ private final float mContourMaxDeviation; /** * * @param contourSampleDistance * Sets the sampling distance to use when * matching the final mesh to the surface defined by the * {@link OpenHeightfield}. * <p> * Impacts how well the final mesh conforms to surface data in the * {@link OpenHeightfield} Higher values, closer conforming, higher * final triangle count. * </p> * <p> * Setting this argument to zero will disable this functionality. * </p> * <p> * Constraints: >= 0 * </p> * * @param contourMaxDeviation * The maximum distance the surface of the * navmesh may deviate from the surface data in the * {@link OpenHeightfield}. * <p> * The accuracy of the algorithm which uses this value is impacted by * the value of the contour sample distance argument. * </p> * <p> * The value of this argument has no meaning if the contour sample * distance argument is set to zero. * </p> * <p> * Setting the value to zero is not recommended since it can result * in a large increase in the number of triangles in the final * navmesh at a high processing cost. * </p> * <p> * Constraints: >= 0 * </p> */ public DetailMeshBuilder(final float contourSampleDistance, final float contourMaxDeviation) { this.mContourSampleDistance = Math.max(0, contourSampleDistance); this.mContourMaxDeviation = Math.max(0, contourMaxDeviation); } /** * Build a triangle mesh with detailed height information from the * provided polygon mesh. * <p> * Concerning sampling: Sampling functionality will only work correctly if * the y-values in the source mesh are accurate to within * {@link OpenHeightfield#cellHeight()} of an associated span within the * provided height field. Otherwise the algorithms used by this operation * may not be able to find accurate height information from the * {@link OpenHeightfield}. * * @param sourceMesh * The source polygon mesh to build the triangle * mesh from. * @param heightField * The heightfield from which the {@link PolyMeshField} was derived. * @return The generated triangle mesh. Or null if there were errors * which prevented triangulation. */ public TriangleMesh build(final PolyMeshField sourceMesh, final OpenHeightfield heightField) { if ((sourceMesh == null) || (sourceMesh.vertCount() == 0) || (sourceMesh.polyCount() == 0)) { return null; } // Create result object. final TriangleMesh mesh = new TriangleMesh(); // Saves on divisions within loops. final int sourcePolyCount = sourceMesh.polyCount(); // Convenience variables. final float cellSize = sourceMesh.cellSize(); final float cellHeight = sourceMesh.cellHeight(); final float[] minBounds = sourceMesh.boundsMin(); final int maxVertsPerPoly = sourceMesh.maxVertsPerPoly(); final int[] sourceVerts = sourceMesh.verts; final int[] sourcePolys = sourceMesh.polys; /* * Contains the xz-plane bounds of each polygon. * Entry format: (xmin, xmax, zmin, zmax) * Uses the same index as sourcePolys. */ final int[] polyXZBounds = new int[sourcePolyCount * 4]; // The total number of polygon vertices. // Needs to be calculated since the vertex count of the source // polygons vary. int totalPolyVertCount = 0; // The maximum width and depth found for the source polygons. int maxPolyWidth = 0; int maxPolyDepth = 0; /* * Gather data. * Loop through each polygon and find its xz bounds, the number of * vertices, and the overall maximum polygon width and depth. */ for (int iPoly = 0; iPoly < sourcePolyCount; iPoly++) { final int pPoly = iPoly * maxVertsPerPoly * 2; // These next variables are pointers to this poly's bound fields // in polyXZBounds. final int pxmin = iPoly * 4; final int pxmax = (iPoly * 4) + 1; final int pzmin = (iPoly * 4) + 2; final int pzmax = (iPoly * 4) + 3; // Initialize the bounds fields to their extremes. polyXZBounds[pxmin] = heightField.width(); polyXZBounds[pxmax] = 0; polyXZBounds[pzmin] = heightField.depth(); polyXZBounds[pzmax] = 0; // Loop through each vertex in the polygon, searching for // minimum/maximum vertex values. for (int vertOffset = 0; vertOffset < maxVertsPerPoly; vertOffset++) { if (sourcePolys[pPoly + vertOffset] == PolyMeshField.NULL_INDEX) { // Reached the end of this polygon's vertices. break; } final int pVert = sourcePolys[pPoly + vertOffset] * 3; // Adjust the values of the bounds if this vertex // represents a new min/max. polyXZBounds[pxmin] = Math.min(polyXZBounds[pxmin], sourceVerts[pVert]); polyXZBounds[pxmax] = Math.max(polyXZBounds[pxmax], sourceVerts[pVert]); polyXZBounds[pzmin] = Math.min(polyXZBounds[pzmin], sourceVerts[pVert + 2]); polyXZBounds[pzmax] = Math.max(polyXZBounds[pzmax], sourceVerts[pVert + 2]); // Increment the total vertex count. totalPolyVertCount++; } /* * Clamp the values to one less than the minimum and one more * than the maximum while staying within the valid field bounds. * This ensures that when a height patch is created later, it is * guaranteed to encompass the entire polygon. */ polyXZBounds[pxmin] = Math.max(0, polyXZBounds[pxmin] - 1); polyXZBounds[pxmax] = Math.min(heightField.width(), polyXZBounds[pxmax] + 1); polyXZBounds[pzmin] = Math.max(0, polyXZBounds[pzmin] - 1); polyXZBounds[pzmax] = Math.min(heightField.depth(), polyXZBounds[pzmax] + 1); if ((polyXZBounds[pxmin] >= polyXZBounds[pxmax]) || (polyXZBounds[pzmin] >= polyXZBounds[pzmax])) { // NO chance of this polygon being the max width/depth polygon. continue; } // Record whether this polygon has the largest width of depth // found so far. maxPolyWidth = Math.max(maxPolyWidth, polyXZBounds[pxmax] - polyXZBounds[pxmin]); maxPolyDepth = Math.max(maxPolyDepth, polyXZBounds[pzmax] - polyXZBounds[pzmin]); } // Holds the vertices of the current polygon to be triangulated. final float[] poly = new float[maxVertsPerPoly * 3]; int polyVertCount = 0; /* * Holds the triangle indices generated for the current polygon. * Form: (vertAIndex, vertBIndex, vertCIndex) * Initialized to allow for an increase in vertices during sampling. */ final ArrayList<Integer> polyTriangles = new ArrayList<Integer>(maxVertsPerPoly * 2 * 3); /* * Hold the vertex information for the triangles. * If sampling occurs, there will be more vertices in this array * than in the polygon array. Otherwise the content will be the same. */ final float[] polyTriangleVerts = new float[MAX_VERTS * 3]; int polyTriangleVertCount = 0; final HeightPatch hfPatch = new HeightPatch(); if (this.mContourSampleDistance > 0) { /* * The height patch is only used in the polygon detail * operation if the sample distance is > 0. * Set the bounds of the patch to be searched. * Sized to hold the largest possible polygon. */ hfPatch.data = new int[maxPolyWidth * maxPolyDepth]; } /* * Values within working variables are irrelevant outside the * operations in which they are used. Their only purpose is to * save on object creation cost during loop iterations. * Most initialization sizes are arbitrary. */ final ArrayDeque<Integer> workingStack = new ArrayDeque<Integer>(256); final ArrayDeque<OpenHeightSpan> workingSpanStack = new ArrayDeque<OpenHeightSpan>(128); final ArrayList<Integer> workingEdges = new ArrayList<Integer>(MAX_EDGES * 4); final ArrayList<Integer> workingSamples = new ArrayList<Integer>(512); /* * A working array used while building the height path. * Its data is not used within this operation. */ final int[] workingWidthDepth = new int[2]; /* * Holds the aggregate vertices for the entire mesh. * Form: (x, y, z) */ final ArrayList<Float> globalVerts = new ArrayList<Float>(totalPolyVertCount * 2 * 3); /* * Holds the aggregate triangles for the entire mesh. * Format (vertAIndex, vertBIndex, vertCIndex, regionID) * where vertices are wrapped clockwise and regionID is the region * id of the source polygon the triangles were generated from. */ final ArrayList<Integer> globalTriangles = new ArrayList<Integer>(totalPolyVertCount * 2 * 4); // Triangluate all polygons. for (int iPoly = 0; iPoly < sourcePolyCount; iPoly++) { final int pPoly = iPoly * maxVertsPerPoly * 2; // Loop through all vertices in the current polygon and // load the working polygon array. polyVertCount = 0; for (int vertOffset = 0; vertOffset < maxVertsPerPoly; vertOffset++) { if (sourcePolys[pPoly + vertOffset] == PolyMeshField.NULL_INDEX) { // Reached the end of the polygon's verts. break; } final int pVert = sourcePolys[pPoly + vertOffset] * 3; // Load the vertex information for the current polygon // into the working poly array. poly[(vertOffset * 3) + 0] = sourceVerts[pVert] * cellSize; poly[(vertOffset * 3) + 1] = sourceVerts[pVert + 1] * cellHeight; poly[(vertOffset * 3) + 2] = sourceVerts[pVert + 2] * cellSize; polyVertCount++; } if (this.mContourSampleDistance > 0) { // The height patch is only used if the sample // distance is > 0. // Load height patch data for this polygon. // Set the bounds to the min/max for current polygon. hfPatch.minWidthIndex = polyXZBounds[iPoly * 4]; hfPatch.minDepthIndex = polyXZBounds[(iPoly * 4) + 2]; hfPatch.width = polyXZBounds[(iPoly * 4) + 1] - polyXZBounds[(iPoly * 4) + 0]; hfPatch.depth = polyXZBounds[(iPoly * 4) + 3] - polyXZBounds[(iPoly * 4) + 2]; // Load the height data. loadHeightPatch(pPoly, polyVertCount, sourcePolys, sourceVerts, heightField, hfPatch, workingStack, workingSpanStack, workingWidthDepth); } // Triangulate this polygon. polyTriangleVertCount = buildPolyDetail(poly, polyVertCount, heightField, hfPatch, polyTriangleVerts, polyTriangles, workingEdges, workingSamples); if (polyTriangleVertCount < 3) { logger.warn("Generation of detail polygon failed:" + " Polygon lost. Region: " + sourceMesh.getPolyRegion(iPoly) + ", Polygon index: " + iPoly); continue; } // Make sure the global lists are able to handle the new data. globalVerts.ensureCapacity(globalVerts.size() + (polyTriangleVertCount * 3)); globalTriangles.ensureCapacity(globalTriangles.size() + ((polyTriangles.size() * 4) / 3)); // Represents the next available vertex index. final int indexOffset = globalVerts.size() / 3; // Add all new vertices to the global vertices list. for (int iVert = 0; iVert < polyTriangleVertCount; iVert++) { // Note: Converting from height field to world coordinates. globalVerts.add(polyTriangleVerts[iVert * 3] + minBounds[0]); globalVerts.add(polyTriangleVerts[(iVert * 3) + 1] + minBounds[1]); globalVerts.add(polyTriangleVerts[(iVert * 3) + 2] + minBounds[2]); } // Add all new triangles to the global triangles list. for (int pTriangle = 0; pTriangle < polyTriangles.size(); pTriangle += 3) { // Offset the original vertex index to match the index in // the global vertex list. globalTriangles.add(polyTriangles.get(pTriangle) + indexOffset); globalTriangles.add(polyTriangles.get(pTriangle + 1) + indexOffset); globalTriangles.add(polyTriangles.get(pTriangle + 2) + indexOffset); // Record the region. globalTriangles.add(sourceMesh.getPolyRegion(iPoly)); } } // Transfer the final results to the mesh object. // Load mesh object with vertex data. mesh.vertices = new float[globalVerts.size()]; for (int i = 0; i < globalVerts.size(); i++) { mesh.vertices[i] = globalVerts.get(i); } // Load mesh object with the triangle indices and region information. mesh.indices = new int[(globalTriangles.size() * 3) / 4]; final int tcount = globalTriangles.size() / 4; mesh.triangleRegions = new int[tcount]; for (int i = 0; i < tcount; i++) { // The index and region information is split and set to two // different locations in the mesh object. final int sourcePointer = i * 4; final int destinationPointer = i * 3; mesh.indices[destinationPointer] = globalTriangles.get(sourcePointer); mesh.indices[destinationPointer + 1] = globalTriangles.get(sourcePointer + 1); mesh.indices[destinationPointer + 2] = globalTriangles.get(sourcePointer + 2); mesh.triangleRegions[i] = globalTriangles.get(sourcePointer + 3); } return mesh; } /** * Performs sampling and triangulation of the polygon. * <p> * Sampling increases the detail of the polygon so that the height of the * final triangle mesh better follows the height of its section of the * heightfield. * </p> * <p> * If the sample distance is > 0, sampling will occur. Otherwise sampling * will not occur and the number of vertices in the final mesh will equal * the number of vertices in the source polygon. * </p> * * @param sourcePoly * Represents a list of vertices in the format * (x, y, z) that represent a clockwise wrapped convex polygon. * @param sourceVertCount * The number of vertices in the source polygon. * @param heightField * The height field from which the polygon was derived. * @param patch * The loaded height patch to use for sample vertex * height data. * <p> * This parameter can be null if the the sample distance is <= 0. * <p/> * @param outVerts * The vertices for the triangle mesh generated from * the polygon. * <p> * The array must be sized to be able to fit the maximum number of * vertices that can be generated. * </p> * <p> * The output may contain trash information. Only vertex locations * which have an associated index in the output triangles list * represent real vertices. * @param outTriangles * The indices of the triangle mesh generated from * the polygon. Its content is cleared before use. * @param workingEdges * A working list used by this operation. Its * content is undefined outside of this operation. Its content is * cleared prior to use. * @param workingSamples * A working list used by this operation. Its * content is undefined outside of this operation. Its content is * cleared prior to use. * @return The number of vertices in the outVerts array. * <p> * For successful completion: Will equal the source vertex count if * not new vertices were added. Otherwise be greater than the source * vertex count by the number of vertices added. * </p> * <p> * For a failure: The value will be zero. The polygon should be * discarded. * </p> */ private int buildPolyDetail(final float[] sourcePoly, final int sourceVertCount, final OpenHeightfield heightField, final HeightPatch patch, final float[] outVerts, final ArrayList<Integer> outTriangles, final ArrayList<Integer> workingEdges, final ArrayList<Integer> workingSamples) { // There is no early exit for a source vertex count of 3 // (a triangle) since sampling may increase the vertex count. // TODO: EVAL: There is a lot of array object creation going on here. // Investigate optimization. // TODO: EVAL: Should the height field reference be removed in favor // of integer parameters for cell size and such? Parameter count is // getting a bit unruly. Then again, this is a private operation. // Holds potential vertices for an edge that may be broken up into // smaller edges. final float[] workingVerts = new float[(MAX_EDGES + 1) * 3]; // Holds the indices to the actual smoothed edge definition. // The indices reference vertices in the working vertices array. final int[] workingIndices = new int[MAX_EDGES]; int workingIndicesCount = 0; /* * Contains indices of vertices that represent the hull of the * polygon which the triangulation cannot alter. The content can be * considered as "seed" edges for the triangulation and is not * used unless the sample distance is > zero. * Points to vertices in the output vertex array. */ final int[] hullIndices = new int[MAX_VERTS]; int hullIndicesCount = 0; // Convenience variable and a variable to reduce number of divisions. final float cellSize = heightField.cellSize(); final float inverseCellSize = 1.0f / heightField.cellSize(); // Seed the output vertices array with the source vertices. System.arraycopy(sourcePoly, 0, outVerts, 0, sourceVertCount * 3); // The number of vertices in the final detail polygon. Will equal // the source vertex count if no new vertices are added. This is // the return value. int outVertCount = sourceVertCount; // Any value sample vertex height value equal to or greater than // this value means a height could not be found in the patch. final float heightPathLimit = HeightPatch.UNSET * heightField.cellHeight(); if (this.mContourSampleDistance > 0) { /* * Create the mandatory hull edges. * * The purpose of this algorithm is to better match the height * of the polygon edges to the height of the source field. * Vertices are added to edges so that the polygon's edges * better fit the contour of the height field. * * This is the first of two sampling passes. In this pass only * the edges are sampled. In the second pass, the inside of the * polygon is sampled. * * See: http://www.critterai.org/nmgen_detailgen#edgedetail */ // Loop through all source polygon edges. for (int iSourceVertB = 0, iSourceVertA = sourceVertCount - 1; iSourceVertB < sourceVertCount; iSourceVertA = iSourceVertB++) { int pSourceVertA = iSourceVertA * 3; int pSourceVertB = iSourceVertB * 3; boolean swapped = false; /* * Next section applies a consistent sort to the vertices so * that segments are always processed in the same order. * * I.e. If VertexA and VertexB represent an edge shared * between two polygons, then when this operation is called * for each polygon, the edge vertices are processed in the * same order no matter the order they are defined within * each polygon. * * This prevents seams from forming between polygons due to * floating point errors. */ if (Math.abs(sourcePoly[pSourceVertA] - sourcePoly[pSourceVertB]) < Float.MIN_VALUE) { if (sourcePoly[pSourceVertA + 2] > sourcePoly[pSourceVertB + 2]) { pSourceVertA = iSourceVertB * 3; pSourceVertB = iSourceVertA * 3; swapped = true; } } else if (sourcePoly[pSourceVertA] > sourcePoly[pSourceVertB]) { pSourceVertA = iSourceVertB * 3; pSourceVertB = iSourceVertA * 3; swapped = true; } // Note: The ordering of the subtraction in these deltas // is important. Later code depends on this ordering. final float deltaX = sourcePoly[pSourceVertB] - sourcePoly[pSourceVertA]; final float deltaZ = sourcePoly[pSourceVertB + 2] - sourcePoly[pSourceVertA + 2]; final float edgeXZLength = (float) Math.sqrt((deltaX * deltaX) + (deltaZ * deltaZ)); // Get the maximum edge index. (This is purposely not an // edge count.) This value is based on how many edge's this // edge is allowed to be broken into. int iMaxEdge = 1 + (int) Math.floor(edgeXZLength / this.mContourSampleDistance); // Clamp to max allowed edges. iMaxEdge = Math.min(iMaxEdge, MAX_EDGES); if ((iMaxEdge + outVertCount) >= MAX_VERTS) { // The addition of these new edges would result in // too many vertices in the polygon. Adjust edge count // so we don't exceed maximum allowed verts. iMaxEdge = MAX_VERTS - 1 - outVertCount; } /* * Split the source edge into equally sized segments based * on the maximum number of new edges allowed. * * The edge is being build from vertex A toward vertex B. */ for (int iEdgeVert = 0; iEdgeVert <= iMaxEdge; iEdgeVert++) { // This section of code depends on the delta's being A->B. final float percentOffset = (float) iEdgeVert / iMaxEdge; final int pEdge = iEdgeVert * 3; workingVerts[pEdge] = sourcePoly[pSourceVertA] + (deltaX * percentOffset); workingVerts[pEdge + 2] = sourcePoly[pSourceVertA + 2] + (deltaZ * percentOffset); // Snap the y-value to a valid height in the height field. workingVerts[pEdge + 1] = getHeightWithinField(workingVerts[pEdge], workingVerts[pEdge + 2], cellSize, inverseCellSize, patch) * heightField.cellHeight(); } /* * The edges array now has a list of vertices that would * represent the new edges if the source edge was to be * broken up into the maximum number of edges allowed. */ // Seed with the first and last vertices. // The source edge's vertex A. workingIndices[0] = 0; // The sample vertex just before vertex B. workingIndices[1] = iMaxEdge; workingIndicesCount = 2; /* * This loop incrementally inserts vertices into the * working indices array when one is found to exceed the * maximum allowed distance from the edge. Since the * initialization of the working vertices array ensures * that x and z-values will not deviate from the edge, this * process only acts based on the y-value. * * Note that the increment for this loop happens internal * to the loop. */ for (int iWorkingIndex = 0; iWorkingIndex < (workingIndicesCount - 1);) { // Define the end points of the current working segment. final int iWorkingVertA = workingIndices[iWorkingIndex]; final int iWorkingVertB = workingIndices[iWorkingIndex + 1]; final int pWorkingVertA = iWorkingVertA * 3; final int pWorkingVertB = iWorkingVertB * 3; // Search the vertices that are between these end points. // Find the vertex that is farthest from the segment. // (Has the maximum deviation from the segment.) float maxDistanceSq = 0; int iMaxDistanceVert = -1; for (int iTestVert = iWorkingVertA + 1; iTestVert < iWorkingVertB; iTestVert++) { if (workingVerts[(iTestVert * 3) + 1] >= heightPathLimit) { /* * This vertex cannot be used. * * Special case: No valid height could be derived * for the point. This is not necessarily an error, * though it does indicate a potential problem * with the configuration used for generation * of the navigation mesh. * * Several potential causes: * - The line segment crosses through a true null * region (a region with no spans). E.g. * polygon mesh doesn't have enough detail * at null region border. * - Error in input data. * - Error in height patch build process. */ logger.warn("Potential loss of polygon height" + "detail on polygon edge: Could not" + "determine height for sample vertex at (" + workingVerts[(iTestVert * 3) + 0] + ", " + workingVerts[(iTestVert * 3) + 2] + ")." + " Heightpatch data not availalable."); continue; } final float distanceSq = Geometry.getPointSegmentDistanceSq(workingVerts[iTestVert * 3], workingVerts[(iTestVert * 3) + 1], workingVerts[(iTestVert * 3) + 2], workingVerts[pWorkingVertA], workingVerts[pWorkingVertA + 1], workingVerts[pWorkingVertA + 2], workingVerts[pWorkingVertB], workingVerts[pWorkingVertB + 1], workingVerts[pWorkingVertB + 2]); if (distanceSq > maxDistanceSq) { // Found a new maximum. maxDistanceSq = distanceSq; iMaxDistanceVert = iTestVert; } } if ((iMaxDistanceVert != -1) && (maxDistanceSq > (this.mContourMaxDeviation * this.mContourMaxDeviation))) { // A vertex was found which exceeded the maximum // allowed deviation from the current segment. // Insert the vertex. for (int i = workingIndicesCount; i > iWorkingIndex; i--) { workingIndices[i] = workingIndices[i - 1]; } workingIndices[iWorkingIndex + 1] = iMaxDistanceVert; workingIndicesCount++; } else { iWorkingIndex++; } } // The working indices array now contains a list of vertex // indices for an edge smoothed based on height. /* * Add new vertices to the polygon. * Build the hull. * * Notes: * * Remember that the output vertices array has already been * seeded with the source indices in the same order as the * source indices array. So the indices match between * the source and output arrays for all original vertices. * * The new vertices are not added to the output vertex array * in any particular order. That is why the hull array is * required to determine proper ordering for the polygon. */ // First add the start vertex for this new group of edges. hullIndices[hullIndicesCount++] = iSourceVertA; if (swapped) { // The original vertices for this edge had to be // reversed for the previous calculations. So they need // to be added to the hull array in reverse order. for (int iWorkingIndex = workingIndicesCount - 2; iWorkingIndex > 0; iWorkingIndex--) { outVerts[outVertCount * 3] = workingVerts[workingIndices[iWorkingIndex] * 3]; outVerts[(outVertCount * 3) + 1] = workingVerts[(workingIndices[iWorkingIndex] * 3) + 1]; outVerts[(outVertCount * 3) + 2] = workingVerts[(workingIndices[iWorkingIndex] * 3) + 2]; hullIndices[hullIndicesCount++] = outVertCount; outVertCount++; } } else { for (int iWorkingIndex = 1; iWorkingIndex < (workingIndicesCount - 1); iWorkingIndex++) { outVerts[outVertCount * 3] = workingVerts[workingIndices[iWorkingIndex] * 3]; outVerts[(outVertCount * 3) + 1] = workingVerts[(workingIndices[iWorkingIndex] * 3) + 1]; outVerts[(outVertCount * 3) + 2] = workingVerts[(workingIndices[iWorkingIndex] * 3) + 2]; hullIndices[hullIndicesCount++] = outVertCount; outVertCount++; } } } } else { // There will be no adjustment to the edges of the polygon. // Just use the order of the output vertices array since it // contains a duplicate of the source polygon. for (int i = 0; i < outVertCount; i++) { hullIndices[i] = i; } hullIndicesCount = outVertCount; } if (outVertCount > 3) { /* * Perform the triangulation. * Note: The only output expected is outTriangles. * The rest of the variables with the prefix "out" are only inputs * to this operation. */ performDelaunayTriangulation(outVerts, outVertCount, hullIndices, hullIndicesCount, workingEdges, outTriangles); } else if (outVertCount == 3) { // The output vertices form a triangle. // Just copy it over. outTriangles.clear(); outTriangles.add(0); outTriangles.add(1); outTriangles.add(2); } else { // Invalid output polygon due to bad input data. // Logging is handled by the caller. outTriangles.clear(); return 0; } // Check validity of indices. int badIndicesCount = getInvalidIndicesCount(outTriangles, outVertCount); if (badIndicesCount > 0) { logger.warn("Delaunay triangulation failure: Invalid indices" + " detected edge detail step. Bad indices" + " detected: " + badIndicesCount); outTriangles.clear(); return 0; } if (this.mContourSampleDistance > 0) { /* * The purpose of this second pass is to sample the inside of * the polygon and add internal triangles where the height field * deviates too far from the mesh. * * This process has to be performed after the initial * triangulation in order to get accurate mesh distance values. */ // TODO: EVAL: Can time be saved by using the patch bounds instead // of running these bounds calculations? // Get the bounds of the polygon in polygon space. float minX = sourcePoly[0]; float minZ = sourcePoly[2]; float maxX = minX; float maxZ = minZ; for (int iVert = 1; iVert < sourceVertCount; iVert++) { final int pVert = iVert * 3; minX = Math.min(minX, sourcePoly[pVert]); minZ = Math.min(minZ, sourcePoly[pVert + 2]); maxX = Math.max(maxX, sourcePoly[pVert]); maxZ = Math.max(maxZ, sourcePoly[pVert + 2]); } /* * Build the sample grid. * The next looping process builds a grid of points (x, y, z). * The x and z-values are snapped to a grid that encompasses * the entire source polygon and is incremented by the sample * distance. * The y-value is snapped to the closest height found in the * height patch at the grid's (x, z) location. */ // Convert the polygon bounds to sample grid space bounds. final int x0 = (int) Math.floor(minX / this.mContourSampleDistance); final int z0 = (int) Math.floor(minZ / this.mContourSampleDistance); final int x1 = (int) Math.ceil(maxX / this.mContourSampleDistance); final int z1 = (int) Math.ceil(maxZ / this.mContourSampleDistance); workingSamples.clear(); // Loop through all locations within the sample grid space // and create a vertex for each location that is inside the // source polygon. for (int z = z0; z < z1; z++) { for (int x = x0; x < x1; x++) { // Need to figure out whether this grid location is // outside or very close to the edge of the actual polygon. // Converts back to polygon space. final float vx = x * this.mContourSampleDistance; // Converts back to polygon space. final float vz = z * this.mContourSampleDistance; if (getSignedDistanceToPolygonSq(vx, vz, sourcePoly, sourceVertCount) > (-this.mContourSampleDistance / 2)) { // This location is either outside the polygon or // very close to the the internal edge. Skip it. continue; } // Add the sample vertex to the grid. workingSamples.add(x); workingSamples.add(getHeightWithinField(vx, vz, cellSize, inverseCellSize, patch)); workingSamples.add(z); } } final int sampleCount = workingSamples.size() / 3; // The only purpose of this outer loop is to provide a certain // number of iterations. The inner loop does not depend in any way // on the iteration count of the outer loop. for (int iterationCount = 0; iterationCount < sampleCount; iterationCount++) { float selectedX = 0; float selectedY = 0; float selectedZ = 0; float maxDistance = 0; // Loop through all sample vertices. for (int iSampleVert = 0; iSampleVert < sampleCount; iSampleVert++) { /* * Design note: * * There is a potential that the y-value for a sample * vertex will be > Integer.MAX_VALUE because a height * value for the vertex could not be found in the height * patch. * * Unlike for the edge vertices earlier, there is no known * valid reason for this to occur, so it is not being * checked and handled here. * * If it does occur, the symptom will be the insertion of * a vertex with a very high y-value that will disrupt * the mesh at its location. */ // Get the position of the sample in polygon space and // its distance from the current mesh. final float sampleX = workingSamples.get(iSampleVert * 3) * this.mContourSampleDistance; final float sampleY = workingSamples.get((iSampleVert * 3) + 1) * heightField.cellHeight(); final float sampleZ = workingSamples.get((iSampleVert * 3) + 2) * this.mContourSampleDistance; final float sampleDistance = getInternalDistanceToMesh(sampleX, sampleY, sampleZ, outVerts, outTriangles); if (sampleDistance == UNDEFINED) { // This sample vertex is outside of the triangle mesh. continue; } if (sampleDistance > maxDistance) { // This sample vertex is farther from the mesh than // any other found so far. maxDistance = sampleDistance; selectedX = sampleX; selectedY = sampleY; selectedZ = sampleZ; } } if (maxDistance <= this.mContourMaxDeviation) { // No sample vertex was found to be too far from the mesh. // Can stop iterating early. break; } // Add this vertex to the output vertices. // Note that since this is an internal vertex it is not part of // a mandatory edge. (Not part of the hull.) outVerts[outVertCount * 3] = selectedX; outVerts[(outVertCount * 3) + 1] = selectedY; outVerts[(outVertCount * 3) + 2] = selectedZ; outVertCount++; // Re-perform the triangulation with the new vertex. // TODO: EVAL: A good candidate for optimizing. // E.g. Insert rather than full rebuild. performDelaunayTriangulation(outVerts, outVertCount, hullIndices, hullIndicesCount, workingEdges, outTriangles); // Check validity of indices. badIndicesCount = getInvalidIndicesCount(outTriangles, outVertCount); if (badIndicesCount > 0) { logger.warn("Delaunay triangulation failure: Invalid " + "indices detected during internal detail" + " iteration. Iteration: " + iterationCount + ", Bad indices detected: " + badIndicesCount); outTriangles.clear(); return 0; } } } return outVertCount; } /** * Generates data which represents the circumcircle of the triangle * formed by the three points (A, B, C). * * @param triangleAreaX2 * 2x the area of the triangle formed by the * points. The only reason for this parameter is for optimization. * The only operation that calls this operation has already performed * the area calculation, so why repeat it? * @param outCircle * The definition of the circumcircle in the form * (x, y, r) where (x, y) is the center point and r is the radius. * The value of (x, y) will be (0, 0) and r will be * {@link #UNDEFINED} if * this operation returns FALSE. * @return TRUE if the circumcircle was successfully created. * Otherwise FALSE. */ private static boolean buildCircumcircle(final float ax, final float ay, final float bx, final float by, final float cx, final float cy, final float triangleAreaX2, final float[] outCircle) { /* * References: * http://en.wikipedia.org/wiki/Circumcenter#Coordinates_of_circumcenter * http://mathworld.wolfram.com/Circumcircle.html */ final float epsilon = 1e-6f; if (Math.abs(triangleAreaX2) > epsilon) { // Triangle has an area. Calculate center point of circle. final float aLenSq = (ax * ax) + (ay * ay); final float bLenSq = (bx * bx) + (by * by); final float cLenSq = (cx * cx) + (cy * cy); outCircle[0] = ((aLenSq * (by - cy)) + (bLenSq * (cy - ay)) + (cLenSq * (ay - by))) / (2 * triangleAreaX2); outCircle[1] = ((aLenSq * (cx - bx)) + (bLenSq * (ax - cx)) + (cLenSq * (bx - ax))) / (2 * triangleAreaX2); // Calculate the radius of the circle. (Distance from center to // one of the supplied points.) outCircle[2] = (float) Math.sqrt(getDistanceSq(outCircle[0], outCircle[1], ax, ay)); return true; } // Invalid triangle. outCircle[0] = 0; outCircle[1] = 0; outCircle[2] = UNDEFINED; return false; } /** * Attempts to form a new triangle on an UNDEFINED side of the specified * edge. * <p> * Will only attempt to form a new triangle for the first UNDEFINED side * that is found. * </p> * <p> * If a new triangle cannot be formed for the selected UNDEFINED side, then * that side will be set to the value HULL. * </p> * <p> * If a new triangle is formed, it is guaranteed to be complete. (I.e. All * necessary data to form the triangle will exist in the edges list.) * </p> * * @param iEdge * The index of the edge to perform the operation on. * @param verts * The available vertices in the format: (x, y, z) * @param vertCount * The number of vertices in the vertices array. * @param currentTriangleCount * The current number of triangles in the * edges list. * @param edges * The edges list in the form: * (vertA, vertB, valueA, valueB) where valueA is the side to the * left * of line segment vertA->vertB and valueB is the side to the left of * line segment vertB->vertA. * @return The new triangle count. If the return value is the same as * currentTriangleCount then no new triangle could be formed. */ private static int completeTriangle(final int iEdge, final float[] verts, final int vertCount, int currentTriangleCount, final ArrayList<Integer> edges) { int iVertA; int iVertB; if (edges.get((iEdge * 4) + 2) == UNDEFINED) { // The side to the left of segment A->B is undefined. iVertA = edges.get(iEdge * 4); iVertB = edges.get((iEdge * 4) + 1); } else if (edges.get((iEdge * 4) + 3) == UNDEFINED) { // The side to the left of segment B-A is undefined. iVertA = edges.get((iEdge * 4) + 1); iVertB = edges.get(iEdge * 4); } else { // Edge is already completed. No new faces. return currentTriangleCount; } final int pVertA = iVertA * 3; final int pVertB = iVertB * 3; // The index of the best vertex on the left side of the edge. int iSelectedVert = UNDEFINED; /* * The definition of the selected circle in the format (x, z, r) * where (x, z) is the center point and r is the radius. * TODO: EVAL: Object creation. Convert to a working parameter? */ final float[] selectedCircle = { 0, 0, -1 }; // Values used to take into account floating point errors. final float tolerance = 0.001f; final float epsilon = 1e-5f; /* * Loop through all the vertices. Find the vertex that is to the * left of the edge (vertA->vertB) and forms the triangle with the * smallest circumcircle. * * This process is difficult to optimize due to floating point * errors. Especially when the source polygon is small in area. * So the optimizations were abandoned. */ for (int iPotentialVert = 0; iPotentialVert < vertCount; iPotentialVert++) { if ((iPotentialVert == iVertA) || (iPotentialVert == iVertB)) { // This vertex is one of the edge's vertices. Skip it. continue; } final int pPotentialVert = iPotentialVert * 3; final float area = getSignedAreaX2(verts[pVertA], verts[pVertA + 2], verts[pVertB], verts[pVertB + 2], verts[pPotentialVert], verts[pPotentialVert + 2]); if (area > epsilon) { // The three vertices form a triangle of adequate size AND the // current vertex is to the left of the line segment // vertA->vertB. if (selectedCircle[2] < 0) { // This is the first potentially valid vertex combination // found so far. if (overlapsExistingEdge(iVertA, iPotentialVert, verts, edges) || overlapsExistingEdge(iVertB, iPotentialVert, verts, edges)) { // An overlap was found. Can't use this vertex. continue; } // Vertex combination is valid. Try to use it. if (buildCircumcircle(verts[pVertA], verts[pVertA + 2], verts[pVertB], verts[pVertB + 2], verts[pPotentialVert], verts[pPotentialVert + 2], area, selectedCircle)) { // Successfully created a circumcircle. // Select this vertex. iSelectedVert = iPotentialVert; } continue; } // This is not the first valid combination found. // Is it better than the existing? // Get the distance from the origin of the current // circumcircle to this vertex. final float distanceToOrigin = (float) Math.sqrt(getDistanceSq(selectedCircle[0], selectedCircle[1], verts[pPotentialVert], verts[pPotentialVert + 2])); if (distanceToOrigin > (selectedCircle[2] * (1 + tolerance))) { // This vertex is outside the current circumcircle and // can be ignored. continue; } else { /* * The vertex is within, on, or almost on, the current * circumcircle. * * If it were not for floating point errors, we could * automatically accept all vertices that showed as * within the current circumcircle. But floating point * errors for small polygons prevent such a shortcut. * * Need to check if new edges formed by the use of this * vertex will conflict with other edges already created. */ if (overlapsExistingEdge(iVertA, iPotentialVert, verts, edges) || overlapsExistingEdge(iVertB, iPotentialVert, verts, edges)) { // An overlap was found. Can't use this vertex. continue; } // Using this vertex is valid. if (buildCircumcircle(verts[pVertA], verts[pVertA + 2], verts[pVertB], verts[pVertB + 2], verts[pPotentialVert], verts[pPotentialVert + 2], area, selectedCircle)) { // Successfully created a circumcircle. // Select this vertex. iSelectedVert = iPotentialVert; } } } } if (iSelectedVert != UNDEFINED) { // A new triangle can be formed. // Update triangle information of edge being completed. updateLeftFace(iEdge, iVertA, currentTriangleCount, edges); // Add a new edge (selectedVert->vertA) or update face info // of existing edge. int iSelectedEdge = getEdgeIndex(edges, iSelectedVert, iVertA); if (iSelectedEdge == UNDEFINED) { // This is a new edge. edges.add(iSelectedVert); edges.add(iVertA); edges.add(currentTriangleCount); edges.add(UNDEFINED); } else { // Update the existing edge. updateLeftFace(iSelectedEdge, iSelectedVert, currentTriangleCount, edges); } // Add a new edge (vertB->selectedVert) or update face info // of existing edge. iSelectedEdge = getEdgeIndex(edges, iVertB, iSelectedVert); if (iSelectedEdge == UNDEFINED) { // This is a new edge. edges.add(iVertB); edges.add(iSelectedVert); edges.add(currentTriangleCount); edges.add(UNDEFINED); } else { // Update the existing edge. updateLeftFace(iSelectedEdge, iVertB, currentTriangleCount, edges); } // Indicate a new face was created. currentTriangleCount++; } else { // A new face cannot be formed. Set the indicate the side of // the edge is a hull. updateLeftFace(iEdge, iVertA, HULL, edges); } return currentTriangleCount; } /** * Attempts to find a span in the heightfield associated with the * provided vertex. * <p> * Spans that are within {@link OpenHeightfield#cellHeight()} of the * vertices y-value take precedence in the search. Otherwise the span * closest in height is returned. * </p> * * @param vertX * The x-value of the vertex (x, y, z). * @param vertY * The y-value of the vertex (x, y, z). * @param vertZ * The z-value of the vertex (x, y, z). * @param heightField * The heightfield to search. * @param outWidthDepth * The actual width and depth index of the selected * span in the form: (widthIndex, depthIndex). The array must be * at least 2 in size. Content is undefined if the return value * of the operation is null. * @return The span in the feightfield that is best associated with * the provided vertex. */ private static OpenHeightSpan getBestSpan(final int vertX, final int vertY, final int vertZ, final OpenHeightfield heightField, final int[] outWidthDepth) { /* * There are special cases which can result in the wrong span being * returned by a simple column search. These special cases are what * result in this a more complex search algorithm. * * In the best case search: Search up the height field column * corresponding to the vertices x and z-values and return the * span whose floor is within the correct tolerance of the y-value. * * While, technically, the best case search should always be * successful, floating point errors and other special cases can * result in a search failure. In such cases the search is expanded * to the 8-neighbor cells, resulting in a slower search. * * If a search if forced to include neighbors, the search will * always be a full 8-neighbor search. (No early exists.) * * Known special case when this occurs: * * The vertex lies on the outer edge of a border span. (The span * edge across which there is no other span, at any height.) * * The vertex lies on the outer edge of a span without a connected * neighbor. (The cell across the span edge contains spans, but * none are connected as a neighbor to the span.) * * The scope of these special cases may not be fully known since * a search failure can be hidden. E.g. When building a height * patch, as long as one vertex in a polygon gets a good result, * the flood fill algoritm can still succeed. The search failures * will never be visible. There may be other special cases * which are hidden in a similar manner. * * The known special cases are much less likely to exhibit when * the source height field contains null (zero) region borders. * This is because a border forces all vertices in from the region * edges, which is where problems occur. */ /* * Search order starts with zero offset. */ final int[] targetOffset = { 0, 0, -1, 0, 0, -1, -1, -1, 1, -1, -1, 1, 1, 0, 1, 1, 0, 1 }; OpenHeightSpan resultSpan = null; int minDistance = Integer.MAX_VALUE; // Loop through the offsets trying to find the best span match. // Priority and potential early exit is given to spans at zero offset. for (int p = 0; p < 17; p += 2) { final int widthIndex = vertX + targetOffset[p]; final int depthIndex = vertZ + targetOffset[p + 1]; if (!heightField.isInBounds(widthIndex, depthIndex)) { // This neighbor is outside of the height field. continue; } // Get the base span for this vertex from the height field. OpenHeightSpan span = heightField.getData(widthIndex, depthIndex); // Find the best span in the column. (Closest height match.) span = getBestSpan(span, vertY); if (span == null) { // No spans at the target location. continue; } else { // Found a span. final int distance = Math.abs(vertY - span.floor()); if ((p == 0) && (distance <= heightField.cellHeight())) { // Found a span at a good height at the zero offset // grid location. Don't search further. (Early exit.) outWidthDepth[0] = widthIndex; outWidthDepth[1] = depthIndex; return span; } else if (distance < minDistance) { /* * Could not find the "perfect" match. So dropping back * to the best possible match. * This span's floor is the closest to the vertex found * so far. */ resultSpan = span; outWidthDepth[0] = widthIndex; outWidthDepth[1] = depthIndex; minDistance = distance; } } } return resultSpan; } /** * Returns the span within the column whose floor is closest to the * provided height. * * @param baseSpan * Where to start the search. * @param targetHeight * The height to find the closest match for. * @return The span whose floor is closest to the target height, or null * if no base span was provided. */ private static OpenHeightSpan getBestSpan(final OpenHeightSpan baseSpan, final int targetHeight) { int minDistance = Integer.MAX_VALUE; OpenHeightSpan result = null; // Loop up the column, starting at the base span. for (OpenHeightSpan span = baseSpan; span != null; span = span.next()) { final int distance = Math.abs(targetHeight - span.floor()); if (distance < minDistance) { // This span's floor is the closest to the vertex found so far. result = span; minDistance = distance; } } return result; } /** * Returns the square of the distance between two points. * * @param ax * The x-value of point (ax, ay). * @param ay * The y-value of point (ax, ay). * @param bx * The x-value of point (bx, by). * @param by * The y-value of point (bx, by). * @return The square of the distance between two points. */ private static float getDistanceSq(final float ax, final float ay, final float bx, final float by) { final float deltaX = (ax - bx); final float deltaY = (ay - by); return (deltaX * deltaX) + (deltaY * deltaY); } /** * Gets the index of the edge defined by two indices. * * @param edges * The edge list where each edge is in the form: * (vertIndex, vertIndex, value, value) * @param vertAIndex * The index of one of the edge's vertices. * @param vertBIndex * The index of the other of the edge's vertices. * @return The index of the edge in the edges list which matches the * provided vertices. Or {@link #UNDEFINED} if there is no * corresponding * edge. */ private static int getEdgeIndex(final ArrayList<Integer> edges, final int vertAIndex, final int vertBIndex) { final int edgeCount = edges.size() / 4; for (int i = 0; i < edgeCount; i++) { final int u = edges.get(i * 4); final int v = edges.get((i * 4) + 1); if (((u == vertAIndex) && (v == vertBIndex)) || ((u == vertBIndex) && (v == vertAIndex))) { return i; } } return UNDEFINED; } /** * Get the height of the point within the height patch. * * @param x * The world x position. * @param z * The world y position. * @param minBounds * The minimum bounds of the source field. * @param cellSize * The cell size of the source field. * @param inverseCellSize * The inverse of the cell size of the source * field. (Included only to improve performance by avoiding a * division * within this operation.) * @param patch * The height patch to search. * @return The height of the location within the patch. Or * {@link Float#MAX_VALUE} if the * search for a height fails. */ private static int getHeightWithinField(final float x, final float z, final float cellSize, final float inverseCellSize, final HeightPatch patch) { /* * There are two special cases when getting the height value: * * The x value is on the upper width edge of the source height field. * The z value is on the upper depth edge of the source height field. * * In these cases this algorithm will create invalid indices for * the height patch. (Out of bounds high.) Since the height patch * is a private class (under strict control) it is being left up to * the height patch class to clamp the index values so no exceptions * are thrown. * * Note that these special cases refer to the source height field * edges, not the height patch edges. This is because the height * patch creation process always creates the patch to be slightly * larger than the polygon being processed. The problem is that * this slight expansion cannot occur when the height patch is up * against the edges of source height field. * * The impact of these special cases is as follows: * * If x is on the upper width edge, then the same height value * will be returned as is returned for (x - cellSize). * * If z is on the upper height edge, then the same value will be * returned as is returned for (z - cellSize). * * This is not a significant issue. */ // Convert world coordinates to height field indices. final int widthIndex = (int) Math.floor((x * inverseCellSize) + 0.01f); final int depthIndex = (int) Math.floor((z * inverseCellSize) + 0.01f); // Get the height. int height = patch.getData(widthIndex, depthIndex); if (height == HeightPatch.UNSET) { /* * One of the following special cases exist: * - Data is bad. * - Floating point calculation errors. * - The vertex is in a true null region. (A region without * any spans.) This can happen since sloppiness is permitted * around null regions. * Find nearest neighbor which has valid height. * This is an 8 neighbor search. */ final int[] neighborOffset = { -1, 0, -1, -1, 0, -1, 1, -1, 1, 0, 1, 1, 0, 1, -1, 1 }; float minNeighborDistanceSq = Float.MAX_VALUE; for (int p = 0; p < 16; p += 2) { final int nWidthIndex = widthIndex + neighborOffset[p]; final int nDepthIndex = depthIndex + neighborOffset[p + 1]; if (!patch.isInPatch(nWidthIndex, nDepthIndex)) { // This neighbor is outside of the height patch. continue; } final int nNeighborHeight = patch.getData(nWidthIndex, nDepthIndex); if (nNeighborHeight == HeightPatch.UNSET) { // This neighbor doesn't have a value either. continue; } // Get distance from this neighbor to the target location. final float deltaWidth = ((nWidthIndex + 0.5f) * cellSize) - x; final float deltaDepth = ((nDepthIndex + 0.5f) * cellSize) - z; final float neighborDistanceSq = (deltaWidth * deltaWidth) + (deltaDepth * deltaDepth); if (neighborDistanceSq < minNeighborDistanceSq) { // This is the closest neighbor found so far. height = nNeighborHeight; minNeighborDistanceSq = neighborDistanceSq; } } } return height; } /** * Returns the approximate y-axis distance of a point from the triangle * mesh. * <p> * If the point is not within the (x, z) plane projection of the mesh then * {@link #UNDEFINED} will be returned. * </p> * * @param px * The x-value of the point to be tested. (px, py, pz) * @param py * The y-value of the point to be tested. (px, py, pz) * @param pz * The z-value of the point to be tested. (px, py, pz) * @param verts * The vertices of the mesh to test against. * @param indices * The indices for the mesh to test against. * @return The approximate y-axis distance of a point from the * triangle mesh. */ private static float getInternalDistanceToMesh(final float px, final float py, final float pz, final float[] verts, final ArrayList<Integer> indices) { float minDistance = Float.MAX_VALUE; final int triangleCount = indices.size() / 3; // Loop through all triangles in the mesh and get the point's y-distance // from any triangles the point lies within. The goal is to find // the minimum (closest to the mesh) y-distance. for (int iTriangle = 0; iTriangle < triangleCount; iTriangle++) { final int pVertA = indices.get(iTriangle * 3) * 3; final int pVertB = indices.get((iTriangle * 3) + 1) * 3; final int pVertC = indices.get((iTriangle * 3) + 2) * 3; float distance = Float.MAX_VALUE; final float deltaACx = verts[pVertC] - verts[pVertA]; final float deltaACy = verts[pVertC + 1] - verts[pVertA + 1]; final float deltaACz = verts[pVertC + 2] - verts[pVertA + 2]; final float deltaABx = verts[pVertB] - verts[pVertA]; final float deltaABy = verts[pVertB + 1] - verts[pVertA + 1]; final float deltaABz = verts[pVertB + 2] - verts[pVertA + 2]; final float deltaAPx = px - verts[pVertA]; final float deltaAPz = pz - verts[pVertA + 2]; final float dotACAC = (deltaACx * deltaACx) + (deltaACz * deltaACz); final float dotACAB = (deltaACx * deltaABx) + (deltaACz * deltaABz); final float dotACAP = (deltaACx * deltaAPx) + (deltaACz * deltaAPz); final float dotABAB = (deltaABx * deltaABx) + (deltaABz * deltaABz); final float dotABAP = (deltaABx * deltaAPx) + (deltaABz * deltaAPz); // Compute barycentric coordinates final float inverseDenominator = 1.0f / ((dotACAC * dotABAB) - (dotACAB * dotACAB)); final float u = ((dotABAB * dotACAP) - (dotACAB * dotABAP)) * inverseDenominator; final float v = ((dotACAC * dotABAP) - (dotACAB * dotACAP)) * inverseDenominator; final float tolerance = 1e-4f; if ((u >= -tolerance) && (v >= -tolerance) && ((u + v) <= (1 + tolerance))) { // The point lies inside the (x, z) plane projection of // the triangle. Interpolate the y value. final float y = verts[pVertA + 1] + (deltaACy * u) + (deltaABy * v); distance = Math.abs(y - py); } if (distance < minDistance) { minDistance = distance; } } if (minDistance == Float.MAX_VALUE) { // The point does not lie within the (x, z) plane projection // of the mesh. So it is invalid. return UNDEFINED; } return minDistance; } /** * Detects whether an indices list contains any obviously invalid * values. * <p> * An invalid index: index < 0 or index >= vertCount. * <p> * This check exists because the triangulation can be a bit dodgy when * dealing with very small triangles. It helps detect these issues so * crashes can be avoided. * </p> * * @param indices * The detailed polygon indices in the form: * (vertA, vertB, vertC) * @param vertCount * The number of vertices in the vertices array * which the indices refer to. * @return The number of invalid indices detected. */ private static int getInvalidIndicesCount(final ArrayList<Integer> indices, final int vertCount) { int badIndicesCount = 0; for (int i = 0; i < indices.size(); i++) { final int index = indices.get(i); if ((index < 0) || (index >= vertCount)) { badIndicesCount++; } } return badIndicesCount; } /** * Returns the distance squared from the point to the line segment. * * @param px * The x-value of point (px, py). * @param py * The y-value of point (px, py) * @param ax * The x-value of the line segment's vertex A. * @param ay * The y-value of the line segment's vertex A. * @param bx * The x-value of the line segment's vertex B. * @param by * The y-value of the line segment's vertex B. * @return The distance squared from the point (px, py) to line segment AB. */ private static float getPointSegmentDistanceSq(final float px, final float py, final float ax, final float ay, final float bx, final float by) { /* * Reference: http://local.wasp.uwa.edu.au/~pbourke/geometry/pointline/ * * The goal of the algorithm is to find the point on line segment * AB that is closest to P and then calculate the distance between * P and that point. */ final float deltaABx = bx - ax; final float deltaABy = by - ay; final float deltaAPx = px - ax; final float deltaAPy = py - ay; final float segmentABLengthSq = (deltaABx * deltaABx) + (deltaABy * deltaABy); if (segmentABLengthSq == 0) { // AB is not a line segment. So just return // distanceSq from P to A return (deltaAPx * deltaAPx) + (deltaAPy * deltaAPy); } final float u = ((deltaAPx * deltaABx) + (deltaAPy * deltaABy)) / segmentABLengthSq; if (u < 0) { // Closest point on line AB is outside outside segment AB and // closer to A. So return distanceSq from P to A. return (deltaAPx * deltaAPx) + (deltaAPy * deltaAPy); } else if (u > 1) { // Closest point on line AB is outside segment AB and closer to B. // So return distanceSq from P to B. return ((px - bx) * (px - bx)) + ((py - by) * (py - by)); } // Closest point on lineAB is inside segment AB. So find the exact // point on AB and calculate the distanceSq from it to P. // The calculation in parenthesis is the location of the point on // the line segment. final float deltaX = (ax + (u * deltaABx)) - px; final float deltaY = (ay + (u * deltaABy)) - py; return (deltaX * deltaX) + (deltaY * deltaY); } /** * 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> * Each call to this operation results in 2 multiplications and 5 * subtractions. * <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 float getSignedAreaX2(final float ax, final float ay, final float bx, final float by, final float cx, final float 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 the distance squared from the point to the closest polygon * segment on the (x, z) plane. * If the return value is positive, the point is outside the polygon. * * @param x * The x-value of the test point (x, z). * @param z * The y-value of the test point (x, z). * @param verts * The polygon vertices in the form * (ax, ay, az, bx, by, bz, ..., nx, ny, nz, trash) * @param vertCount * The number of vertices in the polygon. * @return The distanceSq from the point to the closest polygon * segment on the (x, z) plane. */ private static float getSignedDistanceToPolygonSq(final float x, final float z, final float[] verts, final int vertCount) { float minDistance = Float.MAX_VALUE; int iVertB; int iVertA; boolean isInside = false; // Loop through all edges of the polygon and determine the distance // from (x, y) to the edge. for (iVertB = 0, iVertA = vertCount - 1; iVertB < vertCount; iVertA = iVertB++) { final int pVertB = iVertB * 3; final int pVertA = iVertA * 3; if (((verts[pVertB + 2] > z) != (verts[pVertA + 2] > z)) && (x < ((((verts[pVertA] - verts[pVertB]) * (z - verts[pVertB + 2])) / (verts[pVertA + 2] - verts[pVertB + 2])) + verts[pVertB]))) { // The point is inside the polygons (x,z) plane's column. isInside = true; } // Get the distance from the point to this edge and compare it // to the current minimum distance. minDistance = Math.min( minDistance, getPointSegmentDistanceSq(x, z, verts[pVertA], verts[pVertA + 2], verts[pVertB], verts[pVertB + 2])); } return isInside ? -minDistance : minDistance; } /** * Fills the data array of a height patch with height data. Height * data is chosen from the heightfield based on the provided polygon's * vertices. * <p> * The closest floor for each vertex is recorded, then this operation floods * fills outward to all neighbors, recording neighbor floor heights, out to * the edges of the height patch. * * @param polyPointer * A pointer to the polygon whose vertices will be * used as seed information when building the height data. * @param vertCount * The number of vertices in the polygon. * @param indices * Polygon indices data. * @param verts * Vertex data. * @param inoutPatch * The section of the height field to find height * data for. Expects that the bounds data has been set. * Expects data array to be pre-sized such that it can fit the * maximum * possible data. The data array values will be initialized to UNSET * before being filled. * @param gridIndexStack * A working stack. Its data will be discarded * prior to use. Content after the operation completes is undefined. * @param spanStack * A working stack. Its data will be discarded prior * to use. Content after the operation completes is undefined. * @param widthDepth * A working array. Expected to be of size 2. * Its content is undefined after operation completes. */ private static void loadHeightPatch(final int polyPointer, final int vertCount, final int[] indices, final int[] verts, final OpenHeightfield heightField, final HeightPatch inoutPatch, final ArrayDeque<Integer> gridIndexStack, final ArrayDeque<OpenHeightSpan> spanStack, final int[] widthDepth) { // Initialization inoutPatch.resetData(); gridIndexStack.clear(); spanStack.clear(); /* * For each vertex, locate the span in the height field that is * closest to it. Push the spans onto the stack. * Only searching spans at the grid location of the vertex. * (E.g. In the height column of the vertex.) */ for (int vertOffset = 0; vertOffset < vertCount; vertOffset++) { // The width within the height field. final int vertX = verts[indices[polyPointer + vertOffset] * 3]; // The height within the height field. final int vertY = verts[(indices[polyPointer + vertOffset] * 3) + 1]; // The depth within the height field. final int vertZ = verts[(indices[polyPointer + vertOffset] * 3) + 2]; // Search for the best span in the height field for this vertex. // Best span is the span whose floor area is closest to the // vertex location. final OpenHeightSpan selectedSpan = getBestSpan(vertX, vertY, vertZ, heightField, widthDepth); if (selectedSpan != null) { // Found a span for this vertex. Push in onto the stack. gridIndexStack.push(widthDepth[0]); gridIndexStack.push(widthDepth[1]); spanStack.push(selectedSpan); } } // NOTE: If the polygon mesh was properly built, the stack should // always have a size greater than zero. /* * Using the spans that have been seeded into the stacks, flood * outward, recording the heights found for each grid location * within the bounds of the patch. */ while (spanStack.size() > 0) { final int depthIndex = gridIndexStack.pop(); final int widthIndex = gridIndexStack.pop(); final OpenHeightSpan span = spanStack.pop(); if (inoutPatch.getData(widthIndex, depthIndex) != HeightPatch.UNSET) { // This grid location was processed in an earlier iteration. continue; } if (inoutPatch.isInPatch(widthIndex, depthIndex)) { /* * This span is in the height patch. Record the span's height. * * Note: It is a valid situation for the span to NOT be in * the height patch. This can occur in the special cases * described in detail in getHeightWithinField(). When this * occurs, the algorithm depends on the neighbor search * below to ensure the necessary flooding succeeds. * E.g. One of the neighbors should end up within the * bounds of the patch and continue the flooding. */ inoutPatch.setData(widthIndex, depthIndex, span.floor()); } // "Flood" to the neighbors of this span. If a neighbor is within // the patch's grid, then put it in the stacks for processing. for (int dir = 0; dir < 4; dir++) { final OpenHeightSpan nSpan = span.getNeighbor(dir); if (nSpan == null) { // No neighbor in this direction. continue; } final int nWidthIndex = widthIndex + BoundedField.getDirOffsetWidth(dir); final int nDepthIndex = depthIndex + BoundedField.getDirOffsetDepth(dir); if (!inoutPatch.isInPatch(nWidthIndex, nDepthIndex)) { // This neighbor is outside the bounds of the patch. // So skip it. continue; } if (inoutPatch.getData(nWidthIndex, nDepthIndex) != HeightPatch.UNSET) { // This grid location was processed in an earlier iteration. continue; } // Need to process this neighbor. gridIndexStack.push(nWidthIndex); gridIndexStack.push(nDepthIndex); spanStack.push(nSpan); } } } /** * Checks whether or not a potential new edge intersects with any * existing edge. Same and connected edges are ignored. * * @param iVertA * The first vertex index of the potential new edge. * @param iVertB * The second vertex index of the potential new edge. * @param verts * The available vertices in the form (x, y, z) * @param edges * The edge definitions in the form * (vertAIndex, vertBIndex, valueA, valueB) * (valueA and valueB are not used by this operation.) * @return TRUE if the potential new edge inappropriately intersects * with an existing edge. * Otherwise FALSE. */ private static boolean overlapsExistingEdge(final int iVertA, final int iVertB, final float verts[], final ArrayList<Integer> edges) { // Loop through all edges. for (int pEdge = 0; pEdge < edges.size(); pEdge += 4) { final int iEdgeVertA = edges.get(pEdge); final int iEdgeVertB = edges.get(pEdge + 1); if ((iEdgeVertA == iVertA) || (iEdgeVertA == iVertB) || (iEdgeVertB == iVertA) || (iEdgeVertB == iVertB)) { // Is same or connected edge. Ignore this edge. continue; } if (segmentsOverlap(verts[iEdgeVertA * 3], verts[(iEdgeVertA * 3) + 2], verts[iEdgeVertB * 3], verts[(iEdgeVertB * 3) + 2], verts[iVertA * 3], verts[(iVertA * 3) + 2], verts[iVertB * 3], verts[(iVertB * 3) + 2])) { // The new edge overlaps this edge. return true; } } // No intersections detected. return false; } /** * Attempts to perform a Delaunay triangulation on a group of vertices, * potentially restricted by the content of the hull argument. * * @param verts * The vertices to triangulate in the form (x, y, z). * @param vertCount * The number of vertices in the vertices array. * @param immutableHull * The indices that make up the required hull * edges. These edges are guaranteed to be in the final * triangulation. * <p> * The indices in the hull array are expected to define a clockwise * wrapped convex polygon. Behavior of the operation is undefined if * this is not the case. * </p> * @param hullEdgeCount * The number of indices in the hull array. * If zero, then there will be no guaranteed edges. * @param workingEdges * A working list used for internal purposes. Its * only purpose as an argument is to save on object creation time. * Its content is cleared before use. * @param outTriangles * The indices of the output triangle mesh in the * form (vertAIndex, vertBIndex, vertCIndex). The indices refer to * vertices in the vertices array. * <p> * The list is cleared prior to use by this operation. * </p> */ private static void performDelaunayTriangulation(final float[] verts, final int vertCount, final int[] immutableHull, final int hullEdgeCount, final ArrayList<Integer> workingEdges, final ArrayList<Integer> outTriangles) { int triangleCount = 0; workingEdges.clear(); /* * General reference: * http://en.wikipedia.org/wiki/Delaunay_triangulation * More references at: * http://digestingduck.blogspot.com/2009/10/ * delaunay-triangulations.html */ /* * Entries in the working edges list is as follows: * (vertA, vertB, valueA, valueB) * where valueA is the side to the left of line segment vertA->vertB * and valueB is the side to the left of line segment vertB->vertA */ // Create an working edge entry for each hull edge. for (int iHullVertB = 0, iHullVertA = hullEdgeCount - 1; iHullVertB < hullEdgeCount; iHullVertA = iHullVertB++) { workingEdges.add(immutableHull[iHullVertA]); workingEdges.add(immutableHull[iHullVertB]); // Since hull is expected to be clockwise wrapped, mark the // left side of the edge as a hull. workingEdges.add(HULL); // Don't know what is on the right side of the edge yet. // So default to undefined. workingEdges.add(UNDEFINED); } /* * Loop through edges until all UNDEFINED sides have been defined. * Notes: * - The looping is expected to continue for longer than the original * edge count since new edges will be created by the triangle * completion operation. * - No edge will ever be added to the edge list without at least * one side already defined. So each edge will, at most, need a * single triangle built for it. */ int iCurrentEdge = 0; while ((iCurrentEdge * 4) < workingEdges.size()) { if ((workingEdges.get((iCurrentEdge * 4) + 2) == UNDEFINED) || (workingEdges.get((iCurrentEdge * 4) + 3) == UNDEFINED)) { // Need to create a triangle for one of the sides. triangleCount = completeTriangle(iCurrentEdge, verts, vertCount, triangleCount, workingEdges); } iCurrentEdge++; } /* * Unless there is a logic error, at this point no side value in the * edges array should have a value of UNDEFINED. They should all * either be set to a triangle index or HULL. * The strict HULL equality tests below, rather than just testing * for >= 0, are meant to force logic errors to the surface. */ // Fill the triangle list with the UNDEFINED value for each // expected entry. outTriangles.clear(); outTriangles.ensureCapacity(triangleCount * 3); for (int i = 0; i < (triangleCount * 3); i++) { outTriangles.add(UNDEFINED); } // Loop through all edges. for (int pEdge = 0; pEdge < workingEdges.size(); pEdge += 4) { /* * This algorithm is based on the following assumptions: * * Two of the three triangle vertices are known as soon as a * triangle is first detected. Only the third vertex needs to * be found. * * The edge building process guarantees that no partial * triangles exist in the data. */ // This strict test is meant to force logic errors to the surface. // E.g. If an UNDEFINED value creeps into the working edges list. if (workingEdges.get(pEdge + 3) != HULL) { /* * The right side of edge A->B has a associated triangle. * This will always be the case for hull edges. * Also indicates that A->B is the clockwise wrapping direction. */ // Get a pointer to the triangle. final int pTriangle = workingEdges.get(pEdge + 3) * 3; if (outTriangles.get(pTriangle) == UNDEFINED) { /* * This is the first time this triangle has been seen. * * Initialize this triangle by adding the edge's vertices * to the triangle definition. * * Wrap A->B for clockwise. */ outTriangles.set(pTriangle, workingEdges.get(pEdge)); outTriangles.set(pTriangle + 1, workingEdges.get(pEdge + 1)); } else if (outTriangles.get(pTriangle + 2) == UNDEFINED) { // This triangle's first two vertices have already been // set. Need to figure out which vertex in this edge // is the final vertex. if (workingEdges.get(pEdge).equals(outTriangles.get(pTriangle)) || workingEdges.get(pEdge).equals(outTriangles.get(pTriangle + 1))) { // The first vertex of this edge is already in the // triangle. Add the second vertex of this edge to // the triangle. outTriangles.set(pTriangle + 2, workingEdges.get(pEdge + 1)); } else { /* * The first vertex of this edge is NOT already in * the triangle, so the 2nd vertex must already be * in it. Add the first vertex of this edge to the * triangle. */ outTriangles.set(pTriangle + 2, workingEdges.get(pEdge)); } } } if (workingEdges.get(pEdge + 2) != HULL) { /* * The left side of edge A->B has an associated triangle. * * Indicates that B->A is the clockwise wrapping direction. * * This will never be the case for an original edge since * original edges always have their left sides set to HULL. */ final int pTriangle = workingEdges.get(pEdge + 2) * 3; if (outTriangles.get(pTriangle) == UNDEFINED) { /* * This is the first time this triangle has been seen. * * Trivia: Will only get here for internal triangles that * don't have a hull edge. * * Initialize this triangle by adding the edge's vertices * to the triangle definition. * * Wrap B->A for clockwise. */ outTriangles.set(pTriangle, workingEdges.get(pEdge + 1)); outTriangles.set(pTriangle + 1, workingEdges.get(pEdge)); } else if (outTriangles.get(pTriangle + 2) == UNDEFINED) { // This triangle's first two vertices have already been // set. Need to figure out which vertex in this edge // is the final vertex. if (workingEdges.get(pEdge).equals(outTriangles.get(pTriangle)) || workingEdges.get(pEdge).equals(outTriangles.get(pTriangle + 1))) { // The first vertex of this edge is already in the // triangle. Add the second vertex of this edge to // the triangle. outTriangles.set(pTriangle + 2, workingEdges.get(pEdge + 1)); } else { /* * The first vertex of this edge is NOT already in * the triangle, so the 2nd vertex must already be * in it. Add the first vertex of this edge to * the triangle. */ outTriangles.set(pTriangle + 2, workingEdges.get(pEdge)); } } } } } /** * Returns TRUE if the line segments AB and CD intersect at one or * more points. Otherwise FALSE. * * @param ax * The x-value of point (ax, ay) for the line segment AB * @param ay * The y-value of point (ax, ay) for the line segment AB * @param bx * The x-value of point (bx, by) for the line segment AB * @param by * The y-value of point (bx, by) for the line segment AB * @param cx * The x-value of point (cx, cy) for the line segment CD * @param cy * The y-value of point (cx, cy) for the line segment CD * @param dx * The x-value of point (dx, dy) for the line segment CD * @param dy * The y-value of point (dx, dy) for the line segment CD * @return TRUE if the line segments AB and CD intersect at one or * more points. Otherwise FALSE. */ private static boolean segmentsOverlap(final float ax, final float ay, final float bx, final float by, final float cx, final float cy, final float dx, final float dy) { final float deltaABx = bx - ax; final float deltaABy = by - ay; final float deltaCDx = dx - cx; final float deltaCDy = dy - cy; final float deltaCAx = ax - cx; final float deltaCAy = ay - cy; final float numerator = (deltaCAy * deltaCDx) - (deltaCAx * deltaCDy); final float denominator = (deltaABx * deltaCDy) - (deltaABy * deltaCDx); final float tolerance = 0.001f; if (denominator == 0) { if (numerator != 0) { // Parallel and not colinear return false; } /* * Lines are colinear. But do the segments overlap? * * Note: This design takes into account that it is a * logic error to call this operation for segments that share * end points. * * Note: Since we know they are colinear, we only need to * check one axis for overlap. */ if (Math.abs(cx - dx) < tolerance) { // Line is horizontal. Use y-axis. if ((Math.max(cy, dy) < Math.min(ay, by)) || (Math.max(ay, by) < Math.min(cy, dy))) { // The end points of the segments don't overlap. // No intersection. return false; } else { // The end points of the segments overlap. Intersection. return true; } } else { if ((Math.max(cx, dx) < Math.min(ax, bx)) || (Math.max(ax, bx) < Math.min(cx, dx))) { // The end points of the segments don't overlap. // No intersection. return false; } else { // The end points of the segments overlap. Intersection. return true; } } } // Lines definitely intersect at a single point. final float factorAB = numerator / denominator; final float factorCD = ((deltaCAy * deltaABx) - (deltaCAx * deltaABy)) / denominator; // Determine the type of intersection if ((factorAB >= 0.0f) && (factorAB <= 1.0f) && (factorCD >= 0.0f) && (factorCD <= 1.0f)) { // Segments intersect. return true; } // Intersection is outside of one or both segments. return false; } /** * Sets the left face value of the specified edge to the specified value * if the value is not already set. * <p> * Note that this means that once the value has been set to a value other * than UNDEFINED, this operation will not change the value. * <p> * * @param iEdge * The index of the edge to update. * @param iStartVert * The vertex that represents the start of the edge. * Used to determine which side of the edge is left. * @param faceValue * The new value to apply. * @param edges * The list of edges in the form * (vertAIndex, vertBIndex, valueA, valueB) * where valueA represents the left side of the edge A->B and valueB * represents the left side of the edge B->A. */ private static void updateLeftFace(final int iEdge, final int iStartVert, final int faceValue, final ArrayList<Integer> edges) { final int pEdge = iEdge * 4; if ((edges.get(pEdge) == iStartVert) && (edges.get(pEdge + 2) == UNDEFINED)) { edges.set(pEdge + 2, faceValue); } else if ((edges.get(pEdge + 1) == iStartVert) && (edges.get(pEdge + 3) == UNDEFINED)) { edges.set(pEdge + 3, faceValue); } } }