package org.critterai.nmgen;
import java.util.ArrayDeque;
import org.critterai.nmgen.OpenHeightfield.OpenHeightFieldIterator;
/**
* Implements three algorithms that clean up issues that can
* develop around null region boarders.
*
* <p>
* <strong>Detect and fix encompassed null regions:</strong>
* </p>
* <p>
* If a null region is found that is fully encompassed by a single region, then
* the region will be split into two regions at the null region border.
* </p>
*
* <p>
* <strong>Detect and fix "short wrapping" of null regions:</strong>
* </p>
* <p>
* Regions can sometimes wrap slightly around the corner of a null region in a
* manner that eventually results in the formation of self-intersecting
* polygons.
* </p>
* <p>
* Example: Before the algorithm is applied:
* </p>
* <p>
* <a href=
* "http://www.critterai.org/projects/nmgen/images/ohfg_08_cornerwrapbefore.png"
* target="_parent"> <img alt="" src=
* "http://www.critterai.org/projects/nmgen/images/ohfg_08_cornerwrapbefore.jpg"
* style="width: 620px; height: 353px; " /> </a>
* </p>
* <p>
* Example: After the algorithm is applied:
* </p>
* <p>
* <a href=
* "http://www.critterai.org/projects/nmgen/images/ohfg_09_cornerwrapafter.png"
* target="_parent"> <img alt="" src=
* "http://www.critterai.org/projects/nmgen/images/ohfg_09_cornerwrapafter.jpg"
* style="width: 620px; height: 353px; " /> </a>
* </p>
*
* <p>
* <strong>Detect and fix incomplete null region connections:</strong>
* </p>
* <p>
* If a region touches null region only diagonally, then contour detection
* algorithms may not properly detect the null region connection. This can
* adversely effect other algorithms in the pipeline.
* <p>
* <p>
* Example: Before algorithm is applied:
* </p>
* <p>
*
* <pre>
* b b a a a a
* b b a a a a
* a a x x x x
* a a x x x x
* </pre>
*
* </p>
* <p>
* Example: After algorithm is applied:
* </p>
* <p>
*
* <pre>
* b b a a a a
* b b b a a a <-- Span transferred to region B.
* a a x x x x
* a a x x x x
* </pre>
*
* </p>
*
* @see <a href="http://www.critterai.org/nmgen_regiongen"
* target="_parent">Region Generation</a>
*/
public class CleanNullRegionBorders implements IOpenHeightFieldAlgorithm {
/*
* Design Notes:
*
* Three algorithms have been aggregated into this single class
* for performance reasons. Otherwise we would be stuck
* performing three full contour searches rather than one.
*
* The optimization method used in the search can result in missed
* null region contours. Consider the following pattern:
*
* x x x x x x
* x a a a a x x - null region WITHOUT SPANS
* x a v v a x a - region A
* x a v v a x v - null region WITHOUT SPANS
* x a a a a x
* x x x x x x
*
* If an all span search is performed, the outer null region (x) will
* be detected, but during the process all a-region spans will be marked as
* viewed. This will leave no spans available to detect the inner null
* region (v).
*
* I've not fixing this until it proves to be a problem or I figure out
* a way of resolving the design issue without killing performance.
*/
private static final int NULL_REGION = OpenHeightSpan.NULL_REGION;
private final boolean mUseOnlyNullSpans;
// Working variables. Content is meaningless outside of
// method they are used.
private final ArrayDeque<OpenHeightSpan> mwOpenSpans = new ArrayDeque<OpenHeightSpan>(1024);
private final ArrayDeque<Integer> mwBorderDistance = new ArrayDeque<Integer>(1024);
private final int[] mwNeighborRegions = new int[8];
/**
* Constructor.
* <p>
* Choosing a contour detection type:
* </p>
* <p>
* This algorithm has to detect and walk null region contours. (Where null
* regions border non-null regions.) There are two options for detection:
* Search every single span looking for null region neighbors. Or search
* only null region spans looking for non-null region neighbors. Since null
* region spans are only a tiny fraction of total spans, the second option
* has better performance.
* </p>
* <p>
* If a heightfield is constructed such that all null regions have at least
* one null region span in each contour, then set useOnlyNullRegionSpans to
* TRUE.
* </p>
*
* @param useOnlyNullRegionSpans
* If TRUE, then only null region spans
* will be used to initially detect null region borders. This
* improves performance. If FALSE, all spans are searched to detect
* borders.
*/
public CleanNullRegionBorders(final boolean useOnlyNullRegionSpans) {
this.mUseOnlyNullSpans = useOnlyNullRegionSpans;
}
/**
* {@inheritDoc}
* <p>
* This operation utilizes {@link OpenHeightSpan#flags}. It expects the
* value to be zero on entry, and re-zero's the value on exit.
* </p>
* <p>
* Expects a heightfield with fully built regions.
* </p>
*/
@Override
public void apply(final OpenHeightfield field) {
int nextRegionID = field.regionCount();
final OpenHeightFieldIterator iter = field.dataIterator();
// Iterate over the spans, trying to find null region borders.
while (iter.hasNext()) {
final OpenHeightSpan span = iter.next();
if (span.flags != 0) {
// Span was processed in a previous iteration.
// Ignore it.
continue;
}
span.flags = 1;
OpenHeightSpan workingSpan = null;
int edgeDirection = -1;
if (span.regionID() == NULL_REGION) {
// This is a null region span. See if it
// connects to a span in a non-null region.
edgeDirection = getNonNullBorderDrection(span);
if (edgeDirection == -1) {
// This span is not a border span. Ignore it.
continue;
}
// This is a border span. Step into the non-null
// region and swing the direction around 180 degrees.
workingSpan = span.getNeighbor(edgeDirection);
edgeDirection = (edgeDirection + 2) & 0x3;
} else if (!this.mUseOnlyNullSpans) {
// This is a non-null region span and I'm allowed
// to look at it. See if it connects to a null region.
edgeDirection = getNullBorderDrection(span);
if (edgeDirection == -1) {
// This span is not a null region border span. Ignore it.
continue;
}
workingSpan = span;
} else {
// Not interested in this span.
continue;
}
// Process the null region contour. Detect and fix
// local issues. Determine if the region is
// fully encompassed by a single non-null region.
final boolean isEncompassedNullRegion = processNullRegion(workingSpan, edgeDirection);
if (isEncompassedNullRegion) {
// This span is part of a group of null region spans
// that is encompassed within a single non-null region.
// This is not permitted. Need to fix it.
partialFloodRegion(workingSpan, edgeDirection, nextRegionID);
nextRegionID++;
}
}
field.setRegionCount(nextRegionID);
// Clear all flags.
iter.reset();
while (iter.hasNext()) {
iter.next().flags = 0;
}
}
/**
* Partially flood a region away from the specified direction.
* <p>
* {@link OpenHeightSpan#distanceToRegionCore()} is set to zero for all
* flooded spans.
* </p>
*
* @param startSpan
* The span to start the flood from.
* @param borderDirection
* The hard border for flooding. No
* spans in this direction from the startSpan will be flooded.
* @param newRegionID
* The region id to assign the flooded
* spans to.
*/
private void partialFloodRegion(final OpenHeightSpan startSpan, final int borderDirection,
final int newRegionID) {
// Gather some information.
final int antiBorderDirection = (borderDirection + 2) & 0x3;
final int regionID = startSpan.regionID();
// Re-assign the start span and queue it for the neighbor search.
startSpan.setRegionID(newRegionID);
startSpan.setDistanceToRegionCore(0); // This information is lost.
this.mwOpenSpans.add(startSpan);
this.mwBorderDistance.add(0);
// Search for new spans that can be assigned the new region.
while (!this.mwOpenSpans.isEmpty()) {
// Get the next span off the stack.
final OpenHeightSpan span = this.mwOpenSpans.pollLast();
final int distance = this.mwBorderDistance.pollLast();
// Search in all directions for neighbors.
for (int i = 0; i < 4; i++) {
final OpenHeightSpan nSpan = span.getNeighbor(i);
if ((nSpan == null) || (nSpan.regionID() != regionID)) {
// No span in this direction, or the span
// is not in the region being processed.
// Note: It may have already been transferred.
continue;
}
int nDistance = distance;
if (i == borderDirection) {
// This neighbor is back toward the border.
if (distance == 0) {
// The span is at the border. Can't go
// further in this direction. Ignore
// this neighbor.
continue;
}
nDistance--;
} else if (i == antiBorderDirection) {
// This neighbor is further away from the border.
nDistance++;
}
// Transfer the neighbor to the new region.
nSpan.setRegionID(newRegionID);
nSpan.setDistanceToRegionCore(0); // This information is lost.
// Add the span to the stack to be processed.
this.mwOpenSpans.add(nSpan);
this.mwBorderDistance.add(nDistance);
}
}
}
/**
* Detects and fixes bad span configurations in the vicinity of a
* null region contour. (See class description for details.)
*
* @param startSpan
* A span in a non-null region that borders a null
* region.
* @param startDirection
* The direction of the null region border.
* @return TRUE if the start span's region completely encompasses
* the null region.
*/
private boolean processNullRegion(final OpenHeightSpan startSpan, final int startDirection) {
/*
* This algorithm traverses the contour. As it does so, it detects
* and fixes various known dangerous span configurations.
*
* Traversing the contour: A good 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
*
* As the traversal occurs, the number of acute (90 degree) and
* obtuse (270 degree) corners are monitored. If a complete contour is
* detected and (obtuse corners > acute corners), then the null
* region is inside the contour. Otherwise the null region is
* outside the contour, which we don't care about.
*/
final int borderRegionID = startSpan.regionID();
// Prepare for loop.
OpenHeightSpan span = startSpan;
OpenHeightSpan nSpan = null;
int dir = startDirection;
// Initialize monitoring variables.
int loopCount = 0;
int acuteCornerCount = 0;
int obtuseCornerCount = 0;
int stepsWithoutBorder = 0;
boolean borderSeenLastLoop = false;
boolean isBorder = true; // Initial value doesn't matter.
// Assume a single region is connected to the null region
// until proven otherwise.
boolean hasSingleConnection = true;
/*
* The loop limit exists for the sole reason of preventing
* an infinite loop in case of bad input data.
* It is set to a very high value because there is no way of
* definitively determining a safe smaller value. Setting
* the value too low can result in rescanning a contour
* multiple times, killing performance.
*/
while (++loopCount < Integer.MAX_VALUE) {
// Get the span across the border.
nSpan = span.getNeighbor(dir);
// Detect which type of edge this direction points across.
if (nSpan == null) {
// It points across a null region border edge.
isBorder = true;
} else {
// We never need to perform contour detection
// on this span again. So mark it as processed.
nSpan.flags = 1;
if (nSpan.regionID() == NULL_REGION) {
// It points across a null region border edge.
isBorder = true;
} else {
// This isn't a null region border.
isBorder = false;
if (nSpan.regionID() != borderRegionID) {
// It points across a border to a non-null region.
// This means the current contour can't
// represent a fully encompassed null region.
hasSingleConnection = false;
}
}
}
// Process the border.
if (isBorder) {
// It is a border edge.
if (borderSeenLastLoop) {
/*
* A border was detected during the last loop as well.
* Two detections in a row indicates we passed an acute
* (inner) corner.
*
* a x
* x x
*/
acuteCornerCount++;
} else if (stepsWithoutBorder > 1) {
/*
* We have moved at least two spans before detecting
* a border. This indicates we passed an obtuse
* (outer) corner.
*
* a a
* a x
*/
obtuseCornerCount++;
stepsWithoutBorder = 0;
// Detect and fix span configuraiton issue around this
// corner.
if (processOuterCorner(span, dir)) {
// A change was made and it resulted in the
// corner area having multiple region connections.
hasSingleConnection = false;
}
}
dir = (dir + 1) & 0x3; // Rotate in clockwise direction.
borderSeenLastLoop = true;
stepsWithoutBorder = 0;
} else {
/*
* Not a null region border.
* 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 = nSpan;
dir = (dir + 3) & 0x3; // Rotate counterclockwise direction.
borderSeenLastLoop = false;
stepsWithoutBorder++;
}
if ((startSpan == span) && (startDirection == dir)) {
// Have returned to the original span and direction.
// The search is complete.
// Is the null region inside the contour?
return (hasSingleConnection && (obtuseCornerCount > acuteCornerCount));
}
}
// If got here then the null region boarder is too large to be fully
// explored. So it can't be encompassed.
return false;
}
/**
* Detects and fixes span configuration issues in the vicinity
* of obtuse (outer) null region corners.
*
* @param referenceSpan
* The span in a non-null region that is
* just past the outer corner.
* @param borderDirection
* The direciton of the null region border.
* @return TRUE if more than one region connects to the null region
* in the vicinity of the corner. (This may or may not be due to
* a change made by this operation.)
*/
private boolean processOuterCorner(final OpenHeightSpan referenceSpan, final int borderDirection) {
boolean hasMultiRegions = false;
// Get the previous two spans along the border.
final OpenHeightSpan backOne = referenceSpan.getNeighbor((borderDirection + 3) & 0x3);
final OpenHeightSpan backTwo = backOne.getNeighbor(borderDirection);
OpenHeightSpan testSpan;
if ((backOne.regionID() != referenceSpan.regionID()) &&
(backTwo.regionID() == referenceSpan.regionID())) {
/*
* Dangerous corner configuration.
*
* a x
* b a
*
* Need to change to one of the following configurations:
*
* b x a x
* b a b b
*
* Reason: During contour detection this type of configuration can
* result in the region connection being detected as a
* region-region portal, when it is not. The region connection
* is actually interrupted by the null region.
*
* This configuration has been demonstrated to result in
* two regions being improperly merged to encompass an
* internal null region.
*
* Example:
*
* a a x x x a
* a a x x a a
* b b a a a a
* b b a a a a
*
* During contour and connection detection for region b, at no
* point will the null region be detected. It will appear
* as if a clean a-b portal exists.
*
* An investigation into fixing this issue via updates to the
* watershed or contour detection algorithms did not turn
* up a better way of resolving this issue.
*/
hasMultiRegions = true;
// Determine how many connections backTwo has to backOne's region.
testSpan = backOne.getNeighbor((borderDirection + 3) & 0x3);
int backTwoConnections = 0;
if ((testSpan != null) && (testSpan.regionID() == backOne.regionID())) {
backTwoConnections++;
testSpan = testSpan.getNeighbor(borderDirection);
if ((testSpan != null) && (testSpan.regionID() == backOne.regionID())) {
backTwoConnections++;
}
}
// Determine how many connections the reference span has
// to backOne's region.
int referenceConnections = 0;
testSpan = backOne.getNeighbor((borderDirection + 2) & 0x3);
if ((testSpan != null) && (testSpan.regionID() == backOne.regionID())) {
referenceConnections++;
testSpan = testSpan.getNeighbor((borderDirection + 2) & 0x3);
if ((testSpan != null) && (testSpan.regionID() == backOne.regionID())) {
backTwoConnections++;
}
}
// Change the region of the span that has the most connections
// to the target region.
if (referenceConnections > backTwoConnections) {
referenceSpan.setRegionID(backOne.regionID());
} else {
backTwo.setRegionID(backOne.regionID());
}
} else if ((backOne.regionID() == referenceSpan.regionID()) &&
(backTwo.regionID() == referenceSpan.regionID())) {
/*
* Potential dangerous short wrap.
*
* a x
* a a
*
* Example of actual problem configuration:
*
* b b x x
* b a x x <- Short wrap.
* b a a a
*
* In the above case, the short wrap around the corner of the
* null region has been demonstrated to cause self-intersecting
* polygons during polygon formation.
*
* This algorithm detects whether or not one (and only one)
* of the axis neighbors of the corner should be re-assigned to
* a more appropriate region.
*
* In the above example, the following configuration is more
* appropriate:
*
* b b x x
* b b x x <- Change to this row.
* b a a a
*/
// Check to see if backTwo should be in a different region.
int selectedRegion =
selectedRegionID(backTwo, (borderDirection + 1) & 0x3,
(borderDirection + 2) & 0x3);
if (selectedRegion == backTwo.regionID()) {
// backTwo should not be re-assigned. How about
// the reference span?
selectedRegion =
selectedRegionID(referenceSpan, borderDirection,
(borderDirection + 3) & 0x3);
if (selectedRegion != referenceSpan.regionID()) {
// The reference span should be reassigned
// to a new region.
referenceSpan.setRegionID(selectedRegion);
hasMultiRegions = true;
}
} else {
// backTwo should be re-assigned to a new region.
backTwo.setRegionID(selectedRegion);
hasMultiRegions = true;
}
} else {
/*
* No dangerous configurations detected. But definitely
* has a change in regions at the corner. (We know this
* because one of the previous checks looked for a single
* region for all wrap spans.)
*/
hasMultiRegions = true;
}
return hasMultiRegions;
}
/**
* Checks the span to see if it should be reassigned to a new region.
*
* @param referenceSpan
* A span on one side of an null region contour's
* outer corner. It is expected that the all spans that wrap the
* corner are in the same region.
* @param borderDirection
* The direction of the null region border.
* @param cornerDirection
* The direction of the outer corner from the
* reference span.
* @return The region the span should be a member of. May be the
* region the span is currently a member of.
*/
private int selectedRegionID(final OpenHeightSpan referenceSpan, final int borderDirection,
final int cornerDirection) {
// Get the regions of all neighbors.
referenceSpan.getDetailedRegionMap(this.mwNeighborRegions, 0);
/*
* Initial example state:
*
* a - Known region.
* x - Null region.
* u - Unknown, not checked yet.
*
* u u u
* u a x
* u a a
*/
// The only possible alternate region id is from
// the span that is opposite the border. So check it first.
int regionID = this.mwNeighborRegions[(borderDirection + 2) & 0x3];
if ((regionID == referenceSpan.regionID()) || (regionID == NULL_REGION)) {
/*
* The region away from the border is either a null region
* or the same region. So we keep the current region.
*
* u u u u u u
* a a x or x a x <-- Potentially bad, but stuck with it.
* u a a u a a
*/
return referenceSpan.regionID();
}
// Candidate region for re-assignment.
final int potentialRegion = regionID;
// Next we check the region opposite from the corner direction.
// If it is the current region, then we definitely can't
// change the region id without risk of splitting the region.
regionID = this.mwNeighborRegions[(cornerDirection + 2) & 0x3];
if ((regionID == referenceSpan.regionID()) || (regionID == NULL_REGION)) {
/*
* The region opposite from the corner direction is
* either a null region or the same region. So we
* keep the current region.
*
* u a u u x u
* b a x or b a x
* u a a u a a
*/
return referenceSpan.regionID();
}
/*
* We have checked the early exit special cases. Now a generalized
* brute count is performed.
*
* Priority is given to the potential region. Here is why:
* (Highly unlikely worst case scenario.)
*
* c c c c c c
* b a x -> b b x Select b even though b count == a count.
* b a a b a a
*/
// Neighbors in potential region.
// We know this will have a minimum value of 1.
int potentialCount = 0;
// Neighbors in the span's current region.
// We know this will have a minimum value of 2.
int currentCount = 0;
/*
* Maximum edge case:
*
* b b b
* b a x
* b a a
*
* The maximum edge case for region A can't exist. It
* is filtered out during one of the earlier special cases
* handlers.
*
* Other cases may exist if more regions are involved.
* Such cases will tend to favor the current region.
*/
for (int i = 0; i < 8; i++) {
if (this.mwNeighborRegions[i] == referenceSpan.regionID()) {
currentCount++;
} else if (this.mwNeighborRegions[i] == potentialRegion) {
potentialCount++;
}
}
return (potentialCount < currentCount ? referenceSpan.regionID() : potentialRegion);
}
/**
* Returns the direction of the first neighbor in a non-null region.
*
* @param span
* The span to check.
* @return The direction of the first neighbor in a non-null region, or
* -1 if all neighbors are in the null region.
*/
private static int getNonNullBorderDrection(final OpenHeightSpan span) {
// Search axis-neighbors.
for (int dir = 0; dir < 4; ++dir) {
final OpenHeightSpan nSpan = span.getNeighbor(dir);
if ((nSpan != null) && (nSpan.regionID() != NULL_REGION)) {
// The neighbor is a non-null region.
return dir;
}
}
// All neighbors are in the null region.
return -1;
}
/**
* Returns the direction of the first neighbor in the null region.
*
* @param span
* The span to check.
* @return The direction of the first neighbor that is in the null
* region, or -1 if there are no null region neighbors.
*/
private static int getNullBorderDrection(final OpenHeightSpan span) {
// Search axis-neighbors.
for (int dir = 0; dir < 4; ++dir) {
final OpenHeightSpan nSpan = span.getNeighbor(dir);
if ((nSpan == null) || (nSpan.regionID() == NULL_REGION)) {
// The neighbor is a null region.
return dir;
}
}
// All neighbors are in a non-null region.
return -1;
}
}