/*
* 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 org.critterai.nmgen.OpenHeightfield.OpenHeightFieldIterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Builds a set of contours from the region information contained by an
* {@link OpenHeightfield}. It does this by locating and "walking" the edges
* <p>
* <a href=
* "http://www.critterai.org/projects/nmgen/images/cont_11_simplified_full.png"
* target="_parent"> <img class="insert" height="465" width="620" src=
* "http://www.critterai.org/projects/nmgen/images/cont_11_simplified_full.jpg"
* /> </a>
* </p>
*
* @see <a href="http://www.critterai.org/nmgen_contourgen"
* target="_parent">Contour Generation</a>
* @see Contour
* @see ContourSet
*/
public final class ContourSetBuilder {
/*
* Design notes:
*
* Not adding configuration getters until they are needed.
* Never add setters. Configuration should remain immutable to keep
* the class thread friendly.
*
* Recast Reference: rcBuildContours() in RecastContour.cpp
*/
private static final Logger logger = LoggerFactory.getLogger(ContourSetBuilder.class);
private static final int NULL_REGION = OpenHeightSpan.NULL_REGION;
/**
* The post-processing algorithms to apply to the contours.
*/
private final ArrayList<IContourAlgorithm> mAlgorithms = new ArrayList<IContourAlgorithm>();
/**
* Contructor
*
* @param algorithms
* The post-processing algorithms to apply to
* the contours.
*/
public ContourSetBuilder(final ArrayList<IContourAlgorithm> algorithms) {
if (algorithms == null) {
return;
}
this.mAlgorithms.addAll(algorithms);
}
/**
* Generates a contour set from the provided {@link OpenHeightfield}
* <p>
* The provided field is expected to contain region information. Behavior is
* undefined if the provided field is malformed or incomplete.
* </p>
* <p>
* This operation overwrites the flag fields for all spans in the provided
* field. So the flags must be saved and restored if they are important.
* </p>
*
* @param sourceField
* A fully generated field.
* @return The contours generated from the field.
*/
public ContourSet build(final OpenHeightfield sourceField) {
if ((sourceField == null) || (sourceField.regionCount() == 0)) {
return null;
}
// Initialize the contour set.
final ContourSet result =
new ContourSet(sourceField.boundsMin(), sourceField.boundsMax(),
sourceField.cellSize(), sourceField.cellHeight(), sourceField.regionCount());
int discardedContours = 0;
/*
* Set the flags on all spans in non-null regions to indicate which
* edges are connected to external regions.
*
* Reference: Neighbor search and nomenclature.
* http://www.critterai.org/?q=nmgen_hfintro#nsearch
*
* If a span has no connections to external regions or is
* completely surrounded by other regions (a single span island),
* its flag will be zero.
*
* If a span is connected to one or more external regions then the
* flag will be a 4 bit value where connections are recorded as
* follows:
* bit1 = neighbor0
* bit2 = neighbor1
* bit3 = neighbor2
* bit4 = neighbor3
* With the meaning of the bits as follows:
* 0 = neighbor in same region.
* 1 = neighbor not in same region. (Neighbor may be the null
* region or a real region.)
*/
final OpenHeightFieldIterator iter = sourceField.dataIterator();
while (iter.hasNext()) {
// Note: This algorithm first sets the flag bits such that
// 1 = "neighbor is in the same region". At the end it inverts
// the bits so flags are as expected.
final OpenHeightSpan span = iter.next();
// Default to "not connected to any external region".
span.flags = 0;
if (span.regionID() == NULL_REGION) {
// Don't care about spans in the null region.
continue;
}
// Loop through all directions.
for (int dir = 0; dir < 4; dir++) {
// Default to show neighbor is in null region.
int nRegionID = NULL_REGION;
final OpenHeightSpan nSpan = span.getNeighbor(dir);
if (nSpan != null) {
// There is a neighbor in the current direction.
// Get its region ID.
nRegionID = nSpan.regionID();
}
if (span.regionID() == nRegionID) {
// Neighbor is in same region as this span. Set the bit
// for this neighbor to 1. (Will be inverted later.)
span.flags |= (1 << dir);
}
}
// Invert the bits so a bit value of 1 indicates neighbor NOT in
// same region.
span.flags ^= 0xf;
if (span.flags == 0xf) {
// This is an island span. (All neighbors are other regions.)
// Get rid of flags.
span.flags = 0;
discardedContours++;
logger.warn(
"Discarded contour: Island span. Can't form " + "a contour. Region: {}",
span.regionID());
}
}
/*
* These are working lists whose content changes with each iteration
* of the up coming loop. They represent the detailed and simple
* contour vertices.
* Initial sizing is arbitrary.
*/
final ArrayList<Integer> workingRawVerts = new ArrayList<Integer>(256);
final ArrayList<Integer> workingSimplifiedVerts = new ArrayList<Integer>(64);
/*
* Loop through all spans looking for spans on the edge of a region.
*
* At this point, only spans with flags != 0 are edge spans that
* are part of a region contour.
*
* The process of building a contour will clear the flags on all spans
* that make up the contour. This ensures that the spans that make
* up a contour are only processed once.
*/
iter.reset();
while (iter.hasNext()) {
final OpenHeightSpan span = iter.next();
if ((span.regionID() == NULL_REGION) || (span.flags == 0)) {
// Span is either: Part of the null region, does not
// represent an edge span, or was already processed during
// an earlier iteration.
continue;
}
workingRawVerts.clear();
workingSimplifiedVerts.clear();
// The span is part of an unprocessed region's contour.
// Locate a direction of the span's edge which points toward
// another region. (We know there is at least one.)
int startDir = 0;
while ((span.flags & (1 << startDir)) == 0) {
// This is not an edge direction. Try the next one.
startDir++;
}
// We now have a span that is part of a contour and a direction
// that points to a different region (null or real).
// Build the contour.
buildRawContours(span, iter.widthIndex(), iter.depthIndex(), startDir, workingRawVerts);
// Perform post processing on the contour in order to
// create the final, simplified contour.
generateSimplifiedContour(span.regionID(), workingRawVerts, workingSimplifiedVerts);
/*
* This next test is needed because some extreme cases contours
* just can't be successfully generated.
* We can't just copy the raw contour to the simplified contour
* because the reason the build failed may be because it
* can't be triangulated. (E.g. Has too many vertical segments.)
*/
if (workingSimplifiedVerts.size() < 12) {
logger.warn("Discarded contour: Can't form enough valid"
+ "edges from the vertices."
+ " Region: {}", span.regionID());
discardedContours++;
} else {
result.add(new Contour(span.regionID(), workingRawVerts, workingSimplifiedVerts));
}
}
if (discardedContours > 0) {
logger.warn("Contours not generated for " + discardedContours + " regions.");
}
if ((result.size() + discardedContours) != (sourceField.regionCount() - 1)) {
/*
* The only valid state is one contour per region.
*
* The only time this should occur is if an invalid contour
* was formed or if a region resulted in multiple
* contours (bad region data).
*
* IMPORTANT: While a mismatch may not be a fatal error,
* it should be addressed since it can result in odd,
* hard to spot anomalies later in the pipeline.
*
* A known cause is if a region fully encompasses another
* region. In such a case, two contours will be formed.
* The normal outer contour and an inner contour.
* The CleanNullRegionBorders algorithm protects
* against internal encompassed null regions.
*/
// Detect and report anomalies.
// Not reporting missing contours since those are sometimes
// expected and already reported.
for (int regionID = 1; regionID < sourceField.regionCount(); regionID++) {
int regionMatches = 0;
for (int iContour = 0; iContour < result.size(); iContour++) {
if (result.get(iContour).regionID == regionID) {
regionMatches++;
}
}
if (regionMatches > 1) {
logger.error("More than one contour generated for a" +
"region: Region: " +
regionID +
", Contours:" +
regionMatches);
}
}
for (int iContour = 0; iContour < result.size(); iContour++) {
final Contour contour = result.get(iContour);
if (contour.regionID <= 0) {
// Indicates a problem with this class.
logger.error("A contour was generated for the null" + "region.");
} else if (contour.regionID >= sourceField.regionCount()) {
// Indicates a problem with region generation.
logger.error("A contour was generated for a region" +
" not in the source field's range: " +
contour.regionID);
}
}
/*
* logger.error("Contour generation failed: Detected contours does"
* + " not match the number of regions. Regions: "
* + (sourceField.regionCount() - 1)
* + ", Detected contours: "
* + (result.size() + discardedContours)
* + " (Actual: " + result.size()
* + ", Discarded: " + discardedContours + ")");
* return null;
*/
}
return result;
}
/**
* Walk around the edge of this span's region gathering vertices that
* represent the corners of each span on the sides that are external facing.
* <p>
* There will be two or three vertices for each edge span: Two for spans
* that don't represent a change in edge direction. Three for spans that
* represent a change in edge direction.
* <p>
* <p>
* The output array will contain vertices ordered as follows: (x, y, z,
* regionID) where regionID is the region (null or real) that this vertex is
* considered to be connected to.
* </p>
* <p>
* WARNING: Only run this operation on spans that are already known to be on
* a region edge. The direction must also be pointing to a valid edge.
* Otherwise behavior will be undefined.
* </p>
*
* @param startSpan
* A span that is known to be on the edge of a region.
* (Part of a region contour.)
* @param startWidthIndex
* The width index of the starting span.
* @param startDepthIndex
* The depth index of the starting span.
* @param startDirection
* The direction of the edge of the span that is
* known to point
* across the region edge.
* @param outContourVerts
* The list of vertices that represent the edge
* of the region. (Plus region information.)
*/
private void buildRawContours(final OpenHeightSpan startSpan, final int startWidthIndex,
final int startDepthIndex, final int startDirection,
final ArrayList<Integer> outContourVerts) {
/*
* Flaw in Algorithm:
*
* This method of contour generation can result in an inappropriate
* impassable seam between two adjacent regions in the following case:
*
* 1. One region connects to another region on two sides in an
* uninterrupted manner. (Visualize one region wrapping in an L
* shape around the corner of another.)
* 2. At the corner shared by the two regions, a change in height
* occurs.
*
* In this case, the two regions should share a corner vertex.
* (An obtuse corner vertex for one region and an acute corner
* vertex for the other region.)
*
* In reality, though this algorithm will select the same (x, z)
* coordinates for each region's corner vertex, the vertex heights
* may differ, eventually resulting in an impassable seam.
*/
/*
* It is a bit hard to describe the stepping portion of this algorithm.
* One way to visualize it is to think of a robot sitting on the
* floor facing a known wall. It then does the following to skirt
* the wall:
* 1. If there is a wall in front of it, turn clockwise in 90 degrees
* increments until it finds the wall is gone.
* 2. Move forward one step.
* 3. Turn counter-clockwise by 90 degrees.
* 4. Repeat from step 1 until it finds itself at its original
* location facing its original direction.
*
* See also: http://www.critterai.org/nmgen_contourgen#robotwalk
*/
// Initialize to current span pointing to the edge.
OpenHeightSpan span = startSpan;
int dir = startDirection;
int spanX = startWidthIndex;
int spanZ = startDepthIndex;
int loopCount = 0;
/*
* The loop limit is arbitrary. It exists only to guarantee that
* bad input data doesn't result in an infinite loop.
* The only down side of this loop limit is that it limits the
* number of detectable edge vertices. (The longer the region edge
* and the higher the number of "turns" in a region's edge, the less
* edge vertices can be detected for that region.)
*/
while (++loopCount < 65535) {
// Note: The design of this loop is such that the span variable
// will always reference an edge span from the same region as
// the start span.
if ((span.flags & (1 << dir)) != 0) {
// The current direction is pointing toward an edge.
// Get this edge's vertex.
int px = spanX;
final int py = getCornerHeight(span, dir);
int pz = spanZ;
/*
* Update the px and pz values based on current direction.
* The update is such that the corner being represented is
* clockwise from the edge the direction is currently pointing
* toward.
*/
switch (dir) {
case 0:
pz++;
break;
case 1:
px++;
pz++;
break;
case 2:
px++;
break;
}
// Default in case no neighbor.
int regionThisDirection = NULL_REGION;
final OpenHeightSpan nSpan = span.getNeighbor(dir);
if (nSpan != null) {
// There is a neighbor in this direction.
// Get its region ID.
regionThisDirection = nSpan.regionID();
}
// Add the vertex to the contour.
outContourVerts.add(px);
outContourVerts.add(py);
outContourVerts.add(pz);
outContourVerts.add(regionThisDirection);
// Remove the flag for this edge. We never need to consider
// it again since we have a vertex for this edge.
span.flags &= ~(1 << dir);
dir = (dir + 1) & 0x3; // Rotate in clockwise direction.
} else {
/*
* The current direction does not point to an edge. So it
* must point to a neighbor span in the same region as the
* current span. Move to the neighbor and swing the search
* direction back one increment (counterclockwise).
* By moving the direction back one increment we guarantee we
* don't miss any edges.
*/
span = span.getNeighbor(dir);
// Update the span index based on the direction traveled to
// get to this neighbor.
switch (dir) {
case 0:
spanX--;
break;
case 1:
spanZ++;
break;
case 2:
spanX++;
break;
case 3:
spanZ--;
break;
}
dir = (dir + 3) & 0x3; // Rotate counterclockwise.
}
if ((span == startSpan) && (dir == startDirection)) {
// We have returned to the starting point. Time to stop the
// walk. The contour is complete.
break;
}
}
}
/**
* Takes a group of vertices that represent a region contour and changes
* it in the following manner:
* <ul>
* <li>For any edges that connect to non-null regions, remove all vertices
* except the start and end vertices for that edge. (This smoothes the edges
* between non-null regions into a straight line.)</li>
* <li>Runs all algorithm's in {@link #mAlgorithms} against the contour.
* <li>
* </ul>
*
* @param regionID
* The region the contour was derived from.
* @param sourceVerts
* The source vertices that represent the complex
* contour in the form (x, y, z, regionID)
* @param outVerts
* The simplified contour vertices in the form:
* (x, y, z, regionID)
*/
private void generateSimplifiedContour(final int regionID,
final ArrayList<Integer> sourceVerts, final ArrayList<Integer> outVerts) {
/*
* NOTE: In the output list, the forth field in each vertex contains
* the index of its corresponding source vertex. Only at the very end
* is the region information copied from the source to the output list.
*/
boolean noConnections = true;
// Determine if this contour has any connections to non-null regions.
for (int pVert = 0; pVert < sourceVerts.size(); pVert += 4) {
if (sourceVerts.get(pVert + 3) != NULL_REGION) {
// Found a non-null region connection.
noConnections = false;
break;
}
}
// Seed the simplified contour with the mandatory edges.
// (At least one edge.)
if (noConnections) {
/*
* This contour represents an island region surrounded only by the
* null region. Seed the simplified contour with the source's
* lower left (ll) and upper right (ur) vertices.
*/
int llx = sourceVerts.get(0);
int lly = sourceVerts.get(1);
int llz = sourceVerts.get(2);
int lli = 0; // Will be index of source vertex, not region.
int urx = sourceVerts.get(0);
int ury = sourceVerts.get(1);
int urz = sourceVerts.get(2);
int uri = 0; // Will be index of source vertex, not region.
// Loop through the source contour vertices and find the ur and
// ll vertices.
for (int pVert = 0; pVert < sourceVerts.size(); pVert += 4) {
final int x = sourceVerts.get(pVert);
final int y = sourceVerts.get(pVert + 1);
final int z = sourceVerts.get(pVert + 2);
if ((x < llx) || ((x == llx) && (z < llz))) {
// This the new lower left vertex.
llx = x;
lly = y;
llz = z;
lli = pVert / 4;
}
if ((x >= urx) || ((x == urx) && (z > urz))) {
// This is the new upper right vertex.
urx = x;
ury = y;
urz = z;
uri = pVert / 4;
}
}
// Seed the simplified contour with this edge.
outVerts.add(llx);
outVerts.add(lly);
outVerts.add(llz);
outVerts.add(lli);
outVerts.add(urx);
outVerts.add(ury);
outVerts.add(urz);
outVerts.add(uri);
} else {
/*
* The contour shares edges with other non-null regions.
* Seed the simplified contour with a new vertex for every
* location where the region connection changes. These are
* vertices that are important because they represent portals
* to other regions.
*/
for (int iVert = 0, vCount = sourceVerts.size() / 4; iVert < vCount; iVert++) {
if (!sourceVerts.get((iVert * 4) + 3).equals(
sourceVerts.get((((iVert + 1) % vCount) * 4) + 3))) {
// The current vertex has a different region than the
// next vertex. So there is a change in vertex region.
outVerts.add(sourceVerts.get(iVert * 4));
outVerts.add(sourceVerts.get((iVert * 4) + 1));
outVerts.add(sourceVerts.get((iVert * 4) + 2));
outVerts.add(iVert);
}
}
}
/*
* There are two situations where the out vert list may contain
* only two vertices. (An invalid polygon.)
* 1. The region is fully encompassed by the null region.
* 2. The region is encompassed by exactly two regions.
* This must be kept in mind since at least one vertex must be added
* back at some point.
*
* Though a region encompassed by two regions is technically a
* candidate for merging into one of the other regions, this is
* not done. It is the responsibility of region building to
* decide on such things.
*/
// Run all post processing algorithms. These will build the final
// simplified contour from the seeded edges.
for (final IContourAlgorithm algorithm : this.mAlgorithms) {
algorithm.apply(sourceVerts, outVerts);
}
if (outVerts.size() < 12) {
/*
* Less than 3 vertices.
*
* This can occur in only one known case: The contour started
* with only two seed vertices and none of the algorithms added
* a vertex.
*
* This case is not completely unexpected. At this time,
* the contour algorithms only add vertices back if a null region
* edge is involved. So if a region is only surrounded by two
* non-null regions, it can end up in this situation.
*
* Find the vertex farthest from the current line segment
* and add it back to the contour.
*
* Design notes:
*
* This shouldn't happen very often. So I'm not optimizing it.
*/
final int sourceVertCount = sourceVerts.size() / 4;
int iSelected = -1;
float maxDistance = 0;
final int ax = outVerts.get(0);
final int az = outVerts.get(2);
final int bx = outVerts.get(4);
final int bz = outVerts.get(6);
for (int iVert = 0; iVert < sourceVertCount; iVert++) {
final float dist =
Geometry.getPointSegmentDistanceSq(sourceVerts.get((iVert * 4) + 0),
sourceVerts.get((iVert * 4) + 2), ax, az, bx, bz);
if (dist > maxDistance) {
maxDistance = dist;
iSelected = iVert;
}
}
// As selected vertex such that the contour stays
// wrapped clockwise.
if (iSelected < outVerts.get(3)) {
// Insert selected vertex before other vertices.
outVerts.add(bx);
outVerts.add(outVerts.get(5));
outVerts.add(bz);
outVerts.add(outVerts.get(7));
outVerts.set(4, ax);
outVerts.set(5, outVerts.get(1));
outVerts.set(6, az);
outVerts.set(7, outVerts.get(3));
outVerts.set(0, sourceVerts.get((iSelected * 4) + 0));
outVerts.set(1, sourceVerts.get((iSelected * 4) + 1));
outVerts.set(2, sourceVerts.get((iSelected * 4) + 2));
outVerts.set(3, sourceVerts.get(iSelected));
} else if (iSelected < outVerts.get(7)) {
// Insert selected vertex between other vertices.
outVerts.add(bx);
outVerts.add(outVerts.get(5));
outVerts.add(bz);
outVerts.add(outVerts.get(7));
outVerts.set(4, sourceVerts.get((iSelected * 4) + 0));
outVerts.set(5, sourceVerts.get((iSelected * 4) + 1));
outVerts.set(6, sourceVerts.get((iSelected * 4) + 2));
outVerts.set(7, sourceVerts.get(iSelected));
} else {
// Insert selected vertex at end.
outVerts.add(sourceVerts.get((iSelected * 4) + 0));
outVerts.add(sourceVerts.get((iSelected * 4) + 1));
outVerts.add(sourceVerts.get((iSelected * 4) + 2));
outVerts.add(sourceVerts.get(iSelected));
}
}
// Replace the index pointers in the output list with region IDs.
final int sourceVertCount = sourceVerts.size() / 4;
final int simplifiedVertCount = outVerts.size() / 4;
for (int iVert = 0; iVert < simplifiedVertCount; ++iVert) {
// The connected region id is taken from the next source point.
final int sourceVertIndex = (outVerts.get((iVert * 4) + 3) + 1) % sourceVertCount;
outVerts.set((iVert * 4) + 3, sourceVerts.get((sourceVertIndex * 4) + 3));
}
/*
* Remove segments that will cause problems for the triangulation.
*
* There is a possibility that this will drop the vertices back below
* three. If this happens, nothing we can do about it. The contour
* will be lost.
*/
removeVerticalSegments(regionID, outVerts);
removeIntersectingSegments(regionID, outVerts);
}
/**
* Removes segments that intersect with region portal segments.
* This can occur along the height axis in certain region configurations
* when detail is added back to a simplified contour.
* <p>
* This is required to prevent triangluation failures later in the pipeline.
* </p>
*
* @param regionID
* The region the contour was derived from.
* @param verts
* Contour vertices in the following form: (x, y, z, regionID)
*/
static void removeIntersectingSegments(final int regionID, final ArrayList<Integer> verts) {
/*
* Dev Notes:
*
* This is meant to be a temporary fix since it might remove
* important details. A more appropriate fix will require redesigning
* contour building into multiple stages so that detail can be
* added to portals such that both sides of the portal get the same
* detail.
*
* I'm a little worried about side effects from unanticipated
* contour configurations.
*/
final int startSize = verts.size();
int vCount = startSize / 4;
for (int iVert = 0; iVert < vCount; iVert++) {
final int iVertNext = (iVert + 1) % vCount;
if (verts.get((iVertNext * 4) + 3) != NULL_REGION) {
// Segment iVert->iVertNext is a non-null region edge.
// Check for intersections.
iVert += removeIntersectingSegments(iVert, iVertNext, verts); // Offset
// will
// always
// be
// negative.
vCount = verts.size() / 4;
}
}
if (startSize != verts.size()) {
logger.warn("Contour detail lost: Found and removed null" +
" region segments which were intersecting a portal." +
" Region: " +
regionID +
", Segments removed: " +
((startSize - verts.size()) / 4));
}
}
/**
* Merges segments such that no vertical segments exist.
* A vertical segment is a segment comprised of end points with
* duplicate (x, z) coordinates.
* <p>
* This is required to prevent triangluation failures later in the pipeline.
* </p>
*
* @param regionID
* The region the contour was derived from.
* @param verts
* Contour vertices in the following form: (x, y, z, regionID)
*/
static void removeVerticalSegments(final int regionID, final ArrayList<Integer> verts) {
/*
* Design Notes:
*
* Access level is set to internal to permit direct testing.
*/
/*
* Remove vertical segments.
*
* Design notes:
*
* Protecting triangulation is more important than keeping region
* portals intact. So this algorithm will remove
* seed vertices if necessary.
*
* A potential enhancement is to have the algorithm try to detect
* the best vertex to remove from the pair.
*
* Another potential consideration: If otherwise there is no priority,
* Remove the vertex with the lowest y-value. This fits the general
* rule that the navigation mesh should be above the source geometry.
*/
// Loop through all vertices starting with the last vertex.
for (int pVert = 0; pVert < verts.size();) {
final int pNextVert = (pVert + 4) % verts.size();
if (verts.get(pVert).equals(verts.get(pNextVert)) &&
verts.get(pVert + 2).equals(verts.get(pNextVert + 2))) {
// This segment represents a vertical line.
verts.remove(pNextVert);
verts.remove(pNextVert); // +1
verts.remove(pNextVert); // +2
verts.remove(pNextVert); // +3
logger.warn("Contour detail lost: Removed a vertical" +
" segment from contour. Region: " +
regionID);
} else {
pVert += 4;
}
}
}
/**
* Finds the correct height to use for a particular span vertex.
* (The vertex to the the right (clockwise) of the specified direction.)
*
* @param span
* A span on a region edge.
* @param direction
* A direction that points to a neighbor in a different
* region. (I.e. Crosses the border to a new region.)
* @return The height (y-value) to use for the vertex for this edge.
*/
static private int getCornerHeight(final OpenHeightSpan span, final int direction) {
/*
* This algorithm, while it uses similar processes as Recast,
* has been significantly adjusted.
*
* Examples:
* - Only 3 vertices are surveyed instead of the 4
* the recast uses.
* - This algorithm searches more thoroughly for the diagonal neighbor.
*
* Reference: Neighbor search and nomenclature.
* http://www.critterai.org/?q=nmgen_hfintro#nsearch
*
* See also: http://www.critterai.org/nmgen_contourgen#yselection
*/
// Default height to the current floor.
int maxFloor = span.floor();
// The diagonal neighbor span to this corner.
OpenHeightSpan dSpan = null;
// Rotate clockwise from original direction.
final int directionOffset = (direction + 1) & 0x3;
// Check axis neighbor in current direction.
OpenHeightSpan nSpan = span.getNeighbor(direction);
if (nSpan != null) {
// Select for maximum floor using this neighbor.
maxFloor = Math.max(maxFloor, nSpan.floor());
// Get diagonal neighbor. (By looking clockwise from this
// neighbor.)
dSpan = nSpan.getNeighbor(directionOffset);
}
// Check original span's axis-neighbor in clockwise direction.
nSpan = span.getNeighbor(directionOffset);
if (nSpan != null) {
// Select for maximum floor using this neighbor.
maxFloor = Math.max(maxFloor, nSpan.floor());
if (dSpan == null) {
// Haven't found the diagonal neighbor yet.
// Try to get it by looking counter-clockwise
// from this neighbor.
dSpan = nSpan.getNeighbor(direction);
}
}
if (dSpan != null) {
// The diagonal neighbor was found.
// Select for maximum floor using this neighbor.
maxFloor = Math.max(maxFloor, dSpan.floor());
}
return maxFloor;
}
/**
* Removes any null region segments that intersect with the
* specified edge.
* <p>
* Concerning the return value:
* </p>
* <p>
* The start and end vertices are expected to be in the correct contour
* order. So the only time the start index will be greater than the end
* index in value is if the end index is zero.
* </p>
* <p>
* So the return value can be used to offset the the end index as well as
* the start index unless the end index is zero.
* </p>
*
* @param startVertIndex
* The index of the edge's start index.
* @param endVertIndex
* The index of the edge's end index.
* @param verts
* Contour vertices in the following format:
* (x, y, z, regionID)
* @return If vertices were removed in such a way as to
* change the location of the start vertex, then this
* is the offset to apply to the startIndex. The value will always
* be <= 0.
*/
private static int removeIntersectingSegments(int startVertIndex, int endVertIndex,
final ArrayList<Integer> verts) {
if (verts.size() < 16) {
// Must have at least four vertices to have an intersection.
// This check is mandatory. An infinite loop will occur if
// there are less than four.
return 0;
}
int offset = 0;
final int startX = verts.get((startVertIndex * 4) + 0);
final int startZ = verts.get((startVertIndex * 4) + 2);
final int endX = verts.get((endVertIndex * 4) + 0);
final int endZ = verts.get((endVertIndex * 4) + 2);
int vCount = verts.size() / 4;
// Start at the line segment after the segment being
// checked and loop to the beginning of the segment being
// checked.
for (int iVert = (endVertIndex + 2) % vCount, iVertMinus = (endVertIndex + 1) % vCount; iVert != startVertIndex;) {
/*
* Only remove a vertex if it meets both of the following:
* - Both edges it belongs to connect to the null region.
* (null region segment - vertex - null region segment)
* - Belongs to a segment that intersects the segment being
* tested against.
*/
if ((verts.get((iVert * 4) + 3) == NULL_REGION) &&
(verts.get((((iVert + 1) % vCount) * 4) + 3) == NULL_REGION) &&
Geometry.segmentsIntersect(startX, startZ, endX, endZ,
verts.get((iVertMinus * 4) + 0), verts.get((iVertMinus * 4) + 2),
verts.get((iVert * 4) + 0), verts.get((iVert * 4) + 2))) {
// Remove the null region segment.
verts.remove(iVert * 4);
verts.remove(iVert * 4); // +1
verts.remove(iVert * 4); // +2
verts.remove(iVert * 4); // +3
if ((iVert < startVertIndex) || (iVert < endVertIndex)) {
// The removed vertex was stored before the line
// segment being tested. So the removal resulted
// in the segment indices shifting.
startVertIndex--;
endVertIndex--;
offset--;
}
// Segment has changed. Need to check the new segment.
// Adjust the indices as needed.
if (iVert < iVertMinus) {
iVertMinus--;
}
vCount = verts.size() / 4;
iVert = iVert % vCount;
} else {
// Move to the next segment.
iVertMinus = iVert;
iVert = (iVert + 1) % vCount;
}
}
return offset;
}
}