/*
* 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 java.util.Hashtable;
import org.critterai.nmgen.OpenHeightfield.OpenHeightFieldIterator;
/**
* Builds an open heightfield from the solid data contained by an
* {@link SolidHeightfield}. It does this by locating and creating spans
* representing the area above spans within the source field that have a
* specified flag.
* <p>
* Options are provided to generate neighbor, distance field, and region
* information for the open span data
* </p>
* <p>
* Example of a fully formed open heightfield. (I.e. With region information.)
* Only the floor of the spans is shown.
* </p>
* <p>
* <a href="http://www.critterai.org/projects/nmgen/images/stage_regions.png"
* target="_parent"> <img alt=""
* src="http://www.critterai.org/projects/nmgen/images/stage_regions.gif"
* style="width: 620px; height: 465px;" /></a>
* </p>
*
* @see <a href="http://www.critterai.org/?q=nmgen_hfintro"
* target="_parent">Introduction to Height Fields</a>
* @see <a href="http://www.critterai.org/nmgen_regiongen"
* target="_parent">Region Generation</a>
* @see OpenHeightfield
* @see OpenHeightSpan
*/
public final class OpenHeightfieldBuilder {
/*
* Design notes:
*
* Recast references:
* rcBuildCompactHeightfield in Recast.cpp
* rcBuildDistanceField in RecastRegion.cpp
* rcBuildRegions in RecastRegion.cpp
*
* Configuration getters won't be added until they are needed.
* Never add setters. Configuration should remain immutable to keep
* the class thread friendly.
*/
private static final int NULL_REGION = OpenHeightSpan.NULL_REGION;
private final int mMinTraversableHeight;
private final int mMaxTraversableStep;
private final int mSmoothingThreshold;
private final int mTraversableAreaBorderSize;
private final int mFilterFlags;
private final boolean mUseConservativeExpansion;
private final ArrayList<IOpenHeightFieldAlgorithm> mRegionAlgorithms =
new ArrayList<IOpenHeightFieldAlgorithm>();
/**
* Constructor
*
* @param minTraversableHeight
* Represents the minimum floor to ceiling
* height that will still allow the floor area to be considered
* walkable.
* <p>
* Permits detection of overhangs in the geometry which make the
* geometry below become unwalkable.
* </p>
* <p>
* Constraints: > 0
* </p>
*
* @param maxTraversableStep
* Represents the maximum ledge height that
* is considered to still be walkable.
* <p>
* Prevents minor deviations in height from improperly showing as
* obstructions. Permits detection of stair-like structures, curbs,
* etc.
* </p>
* <p>
* Constraints: >= 0
* </p>
*
* @param traversableAreaBorderSize
* Represents the closest any part
* of the navmesh can get to an obstruction in the source mesh.
* <p>
* Usually set to the maximum bounding radius of entities utilizing
* the navmesh for navigation decisions.
* </p>
* <p>
* Constraints: >= 0
* </p>
*
* @param smoothingThreshold
* The amount of smoothing to be performed
* when generating the distance field.
* <p>
* This value impacts region formation and border detection. A higher
* value results in generally larger regions and larger border sizes.
* A value of zero will disable smoothing.
* </p>
* <p>
* Constraints: 0 <= value <= 4
* </p>
*
* @param filterFlags
* The flags used to determine which spans from the
* source {@link SolidHeightfield} should be used to build the
* {@link OpenHeightfield}. Only those spans which whose flags
* exactly match the filter flag will be considered for inclusion in
* the generated open field.
* <p>
* Note: Spans from the source field which do not match the filter
* flags are still taken into account as height obstructions.
* <p>
*
* @param useConservativeExpansion
* Applies extra algorithms to regions
* to help prevent poorly formed regions from forming.
* <p>
* If the navigation mesh is missing sections that should be present,
* then enabling this feature will likely fix the problem
* </p>
* <p>
* Enabling this feature significantly increased processing cost.
* </p>
*
* @param regionAlgorithms
* A list of the algorithms to run after
* initial region generation is complete. The algorithms will be run
* in the order of the list.
*/
public OpenHeightfieldBuilder(final int minTraversableHeight, final int maxTraversableStep,
final int traversableAreaBorderSize, final int smoothingThreshold,
final int filterFlags, final boolean useConservativeExpansion,
final ArrayList<IOpenHeightFieldAlgorithm> regionAlgorithms) {
this.mMaxTraversableStep = Math.max(0, maxTraversableStep);
this.mMinTraversableHeight = Math.max(1, minTraversableHeight);
this.mTraversableAreaBorderSize = Math.max(0, traversableAreaBorderSize);
this.mFilterFlags = filterFlags;
this.mSmoothingThreshold = Math.min(4, Math.max(0, smoothingThreshold));
this.mUseConservativeExpansion = useConservativeExpansion;
if (regionAlgorithms != null) {
this.mRegionAlgorithms.addAll(regionAlgorithms);
}
}
/**
* Performs a smoothing pass on the distance field data.
* <p>
* This operation depends on distance field information. So the
* {@link #generateDistanceField(OpenHeightfield)} operation must be run
* before this operation.
* </p>
* <p>
* This operation does not need to be run if the
* {@link #build(SolidHeightfield, boolean) build} operation was run with
* performFullGeneration set to TRUE.
* </p>
*
* @param field
* A populated open height field with distance field data
* already generated.
*/
public void blurDistanceField(final OpenHeightfield field) {
// TODO: DOC: Need to find source documentation.
// The basic process is to combine a span's original distance with
// that of its neighbors.
if (field == null) {
return;
}
// Reference: Neighbor searches and nomenclature.
// http://www.critterai.org/?q=nmgen_hfintro#nsearch
if (this.mSmoothingThreshold <= 0) {
// Not configured to perform smoothing. Exit early.
return;
}
// TODO: EVAL: Try to optimize out this hash table.
/*
* Holds information on the final blurred distance for each span.
* Key = span
* Value = new blurred distance.
*/
final Hashtable<OpenHeightSpan, Integer> blurResults =
new Hashtable<OpenHeightSpan, Integer>(field.spanCount());
// Loop through all spans.
final OpenHeightFieldIterator iter = field.dataIterator();
while (iter.hasNext()) {
final OpenHeightSpan span = iter.next();
final int origDist = span.distanceToBorder();
if (origDist <= this.mSmoothingThreshold) {
// This span is at the minimum threshold.
// Add it to the results and continue to next span.
blurResults.put(span, this.mSmoothingThreshold);
continue;
}
int workingDist = origDist;
// Loop through neighbors.
for (int dir = 0; dir < 4; dir++) {
OpenHeightSpan nSpan = span.getNeighbor(dir); // axis-neighbor.
if (nSpan == null) {
// No neighbor on this side. Self buff using own
// original distance.
workingDist += origDist * 2;
} else {
// Neighbor on this side. Add its distance to the
// current span.
workingDist += nSpan.distanceToBorder();
// Get diagonal neighbor.
nSpan = nSpan.getNeighbor((dir + 1) & 0x3);
if (nSpan == null) {
// No diagonal neighbor. Self buff using own
// original distance.
workingDist += origDist;
} else {
// Has diagonal neighbor. Add its distance to
// the current span.
workingDist += nSpan.distanceToBorder();
}
}
}
// Adjust and store the result.
// Don't know the why behind this specific formula.
blurResults.put(span, ((workingDist + 5) / 9));
}
// Replace the original distance information with the new
// distance information.
for (final OpenHeightSpan span : blurResults.keySet()) {
span.setDistanceToBorder(blurResults.get(span));
}
// Reset the known min/max border distance. This will force a
// recalculation if the values are needed later.
field.clearBorderDistanceBounds();
}
/**
* Builds an {@link OpenHeightfield} from the provided
* {@link SolidHeightfield} based on the configuration settings.
*
* @param sourceField
* The solid field to derive the open field from.
* @param performFullGeneration
* If TRUE, neighbor link, distance field
* (including blurring),
* and region information will be generated. If FALSE, only the spans
* will be generated.
*/
public OpenHeightfield build(final SolidHeightfield sourceField,
final boolean performFullGeneration) {
if (sourceField == null) {
return null;
}
// Construct the open field object.
final OpenHeightfield result =
new OpenHeightfield(sourceField.boundsMin(), sourceField.boundsMax(),
sourceField.cellSize(), sourceField.cellHeight());
// Loop through all solid field grid locations.
for (int depthIndex = 0; depthIndex < sourceField.depth(); depthIndex++) {
for (int widthIndex = 0; widthIndex < sourceField.width(); widthIndex++) {
// The lowest span in this column.
OpenHeightSpan baseSpan = null;
// The last span processed in this column.
OpenHeightSpan previousSpan = null;
// Climb up the list of spans at this grid location.
// A loop will only occur if the grid location has at least
// one span.
for (HeightSpan span = sourceField.getData(widthIndex, depthIndex); span != null; span =
span.next()) {
// Ignore spans that do not match the filter flags.
if (span.flags() != this.mFilterFlags) {
continue;
}
/*
* Determine the open space between this span and the next
* higher span. Note that the flag of the ceiling span
* does not matter. All spans matter when it comes to
* this step.
*/
final int floor = span.max();
final int ceiling =
(span.next() != null ? span.next().max() : Integer.MAX_VALUE);
// Add the span.
// Note that the original span flags are being discarded.
final OpenHeightSpan oSpan = new OpenHeightSpan(floor, (ceiling - floor));
if (baseSpan == null) {
// This is the first span created at this grid location.
baseSpan = oSpan;
}
if (previousSpan != null) {
// There is a span at a lower location in this grid
// location. Link the lower (previous) span to this
// span.
previousSpan.setNext(oSpan);
}
previousSpan = oSpan;
result.incrementSpanCount();
}
if (baseSpan != null) {
// There is one or more spans at this grid location.
// Add the first span in the chain to the spans hash.
result.addData(widthIndex, depthIndex, baseSpan);
}
}
}
if (performFullGeneration) {
// Need to perform a full generation.
generateNeighborLinks(result);
generateDistanceField(result);
blurDistanceField(result);
generateRegions(result);
}
return result;
}
/**
* Generates distance field information.
* The {@link OpenHeightSpan#distanceToBorder()} information is generated
* for all spans in the field.
* <p>
* A boundary is a span with a missing neighbor. It will always have a
* distance value of zero. A span is not a boundary if is has 4 neighbors.
* Its boundary distance value will be higher the further it is from a
* boundary span.
* </p>
* <p>
* All distance values are relative and do not represent explicit distance
* values (such as grid unit distance). The algorithm which is used results
* in an approximation only. It is not exhaustive.
* </p>
* <p>
* This operation depends on neighbor information. So the
* {@link #generateNeighborLinks(OpenHeightfield)} operation must be run
* before this operation.
* </p>
* <p>
* This operation does not need to be run if the
* {@link #build(SolidHeightfield, boolean) build} operation was run with
* performFullGeneration set to TRUE.
* </p>
* <p>
* The data generated by this operation is required by
* {@link #blurDistanceField(OpenHeightfield)} and
* {@link #generateRegions(OpenHeightfield)}
* </p>
*
* @param field
* A field with spans and neighbor information already
* generated.
*/
public void generateDistanceField(final OpenHeightfield field) {
// TODO: DOC: Need to find source documentation for this algorithm.
if (field == null) {
return;
}
// Reference: Neighbor searches and nomenclature.
// http://www.critterai.org/?q=nmgen_hfintro#nsearch
// Enumerated values for border distance:
// Represents a border span. The value for a border span will never be
// changed during any stage.
final int BORDER = 0;
// Represents an uninitialized non-border span.
// All non-border spans will have this value going into pass 1.
// No span will have this value by the end of pass 1.
final int NEEDS_INIT = Integer.MAX_VALUE;
/*
* Initialization pass.
* Loop through each span.
* Set distance to BORDER for boundary spans. (Spans with less than
* 4 known neighbors.)
* Set distance to NEEDS_INIT for non-boundary spans.
*/
final OpenHeightFieldIterator iter = field.dataIterator();
while (iter.hasNext()) {
final OpenHeightSpan span = iter.next();
// Perform 8-neighbor search. If any neighbor is missing, this
// is a border.
boolean isBorder = false;
for (int dir = 0; dir < 4; dir++) {
final OpenHeightSpan nSpan = span.getNeighbor(dir);
if ((nSpan == null) || (nSpan.getNeighbor(dir == 3 ? 0 : dir + 1) == null)) {
// Either this axis-neighbor or the diagonal-neighbor
// associated with it is missing. This is a border span.
isBorder = true;
break;
}
}
if (isBorder) {
// Mark as a border span.
span.setDistanceToBorder(BORDER);
} else {
// Marks as a non-border span that needs initialization.
span.setDistanceToBorder(NEEDS_INIT);
}
}
/*
* The next two phases basically check the neighbors of a span and
* set the span's distance field to be slightly greater than the
* neighbor with the lowest border distance. Distance is increased
* slightly more for diagonal-neighbors than for axis-neighbors.
*
* Example:
* Span.dist = 5 // Current estimated distance from border.
* Neighor1.dist = 3
* Neighor2.dist = 1 <- Neighbor with lowest border distance.
* Neighor3.dist = 2
* Neighor4.dist = 4
* Then set Span.dist = Neighbor2.dist + 2
*
* See the first pass (below) for comments that detail the algorithm.
*/
/*
* Pass 1
* During this pass, the following neighbors are checked:
* (-1, 0) (-1, -1) (0, -1) (1, -1)
*
* There is a special case during this pass since non-border spans may
* have a value of NEEDS_INIT: If a neighbor's border distance is not
* known, then it is treated at if it is a border span.
*
* By the end of this pass, no spans will have the value NEEDS_INIT
* since all non-border spans will have a neighbor that forces
* its value to change.
*/
// Loop through all spans.
iter.reset();
while (iter.hasNext()) {
final OpenHeightSpan span = iter.next();
int dist = span.distanceToBorder();
if (dist == BORDER) {
// This is a border cell. Skip it.
continue;
}
// This span is guaranteed to have 4 axis neighbors.
// (-1, 0) Guaranteed to exist.
OpenHeightSpan nSpan = span.getNeighbor(0);
int ndist = nSpan.distanceToBorder();
/*
* At this point, dist is guaranteed to equal NEEDS_INIT.
* (This is the first time this span has been selected for
* change.) So the value of dist will always have its value set
* to slightly higher than this first neighbor.
*/
if (ndist == NEEDS_INIT) {
// Don't know how far from border this neighbor is.
// Default to slightly away from the border.
dist = 1;
} else {
// Set to slightly further from the border than this neighbor.
dist = ndist + 2;
}
// (-1, -1) Diagonal. Not guaranteed to exist.
nSpan = nSpan.getNeighbor(3);
if (nSpan != null) {
// There is a diagonal neighbor.
ndist = nSpan.distanceToBorder();
if (ndist == NEEDS_INIT) {
// Don't know how far from border this neighbor is.
// Default to slightly away from the border.
ndist = 2;
} else {
// Set to slightly further from the border than
// this neighbor.
ndist += 3;
}
if (ndist < dist) {
/*
* This neighbor is closer to the border than
* previously detected neighbors. Use this neighbor's
* increment. (I.e. Slightly further from border than
* this neighbor.)
*/
dist = ndist;
}
}
nSpan = span.getNeighbor(3); // (0, -1) Guaranteed to exist.
ndist = nSpan.distanceToBorder();
if (ndist == NEEDS_INIT) {
// Don't know how far from border this neighbor is.
// Default to slightly away from the border.
ndist = 1;
} else {
// Set to slightly further from the border than this neighbor.
ndist += 2;
}
if (ndist < dist) {
// This neighbor is closer to the border than previously
// detected neighbors. Use this neighbor's increment.
// (I.e. Slightly further from border than this neighbor.)
dist = ndist;
}
// (1, -1) Diagonal. Not guaranteed to exist.
nSpan = nSpan.getNeighbor(2);
// More of the same. So no new comments.
if (nSpan != null) {
ndist = nSpan.distanceToBorder();
if (ndist == NEEDS_INIT) {
ndist = 2;
} else {
ndist += 3;
}
if (ndist < dist) {
dist = ndist;
}
}
// At this point, dist will contain this shortest estimated
// distance. Set the span to this value.
span.setDistanceToBorder(dist);
}
/*
* Pass 2
* During this pass, the following neighbors are checked:
* (1, 0) (1, 1) (0, 1) (-1, 1)
*
* Besides checking different neighbors, this pass performs its
* grid search in reverse order.
*
* This pass does the same thing as the first pass, but is much
* simpler because it doesn't need to handle the NEEDS_INIT special
* case.
*
* Minimal commenting is provided since nothing new is happening
* here that isn't already described in the the previous pass.
*/
// Loop through all spans in reverse order. The looping method is
// slightly more complex since the standard iterator cannot be used.
for (int depthIndex = field.depth() - 1; depthIndex >= 0; depthIndex--) {
for (int widthIndex = field.width() - 1; widthIndex >= 0; widthIndex--) {
// Loop through all spans in the current grid location.
for (OpenHeightSpan span = field.getData(widthIndex, depthIndex); span != null; span =
span.next()) {
int dist = span.distanceToBorder();
if (dist == BORDER) {
continue; // Border cells never change.
}
OpenHeightSpan nSpan = span.getNeighbor(2); // (1, 0)
int ndist = nSpan.distanceToBorder();
ndist = nSpan.distanceToBorder() + 2;
if (ndist < dist) {
dist = ndist;
}
nSpan = nSpan.getNeighbor(1); // (1, 1)
if (nSpan != null) {
ndist = nSpan.distanceToBorder() + 3;
if (ndist < dist) {
dist = ndist;
}
}
nSpan = span.getNeighbor(1); // (0, 1)
ndist = nSpan.distanceToBorder();
ndist = nSpan.distanceToBorder() + 2;
if (ndist < dist) {
dist = ndist;
}
nSpan = nSpan.getNeighbor(0); // (-1, 1)
if (nSpan != null) {
ndist = nSpan.distanceToBorder() + 3;
if (ndist < dist) {
dist = ndist;
}
}
span.setDistanceToBorder(dist);
}
}
}
// Reset the known min/max border distance. This will force a
// recalculation if the values are needed later.
field.clearBorderDistanceBounds();
}
/**
* Generates axis-neighbor link information for all spans in the field.
* This information is required for algorithms which perform neighbor
* searches.
* <p>
* After this operation is run, the {@link OpenHeightSpan#getNeighbor(int)}
* operation can be used for neighbor searches.
* </p>
* <p>
* This operation does not need to be run if the
* {@link #build(SolidHeightfield, boolean) build} operation was run with
* performFullGeneration set to TRUE.
* </p>
* <p>
* The data generated by this operation is required by
* {@link #generateDistanceField(OpenHeightfield)}
* </p>
*
* @param field
* A field already loaded with span information.
* @see <a href="http://www.critterai.org/?q=nmgen_hfintro#nsearch"
* target="_parent">Neighbor Searches</a>
*/
public void generateNeighborLinks(final OpenHeightfield field) {
if (field == null) {
return;
}
// Loop through all spans and generate neighbor information.
final OpenHeightFieldIterator iter = field.dataIterator();
while (iter.hasNext()) {
final OpenHeightSpan span = iter.next();
// Loop through all neighbor grid locations and check to see if
// any of their spans are accessible from this span.
for (int dir = 0; dir < 4; dir++) {
// Get the neighbor offset for this direction.
final int nWidthIndex = (iter.widthIndex() + BoundedField.getDirOffsetWidth(dir));
final int nDepthIndex = (iter.depthIndex() + BoundedField.getDirOffsetDepth(dir));
/*
* Loop through all spans in the neighbor grid location.
* Note: Because of the way solid heightfields are built, only
* one span in each neighbor column can ever meet the
* conditions to be a neighbor of the current span.
*/
for (OpenHeightSpan nSpan = field.getData(nWidthIndex, nDepthIndex); nSpan != null; nSpan =
nSpan.next()) {
// Select the floor, current or neighbor span, that is
// higher.
final int maxFloor = Math.max(span.floor(), nSpan.floor());
// Select the ceiling, current or neighbor span, this is
// lower.
final int minCeiling = Math.min(span.ceiling(), nSpan.ceiling());
/*
* The above values are used to determine if the the gap
* formed by the two spans is large enough for agents to
* walk through? E.g. Without bumping its head on anything.
*/
if (((minCeiling - maxFloor) >= this.mMinTraversableHeight) &&
(Math.abs(nSpan.floor() - span.floor()) <= this.mMaxTraversableStep)) {
// There is space to walk between current and neighbor
// span, and the step up/down between this and
// neighbor span is acceptable.
span.setNeighbor(dir, nSpan);
break;
}
}
}
}
}
/**
* Groups spans into contiguous regions using an watershed based algorithm.
* <p>
* This operation depends on neighbor and distance field information. So the
* {@link #generateNeighborLinks(OpenHeightfield)} and
* {@link #generateDistanceField(OpenHeightfield)} operations must be run
* before this operation.
* </p>
* <p>
* This operation does not need to be run if the
* {@link #build(SolidHeightfield, boolean) build} operation was run with
* performFullGeneration set to TRUE.
* </p>
*
* @param field
* A field with span, neighbor, and distance information
* fully generated.
*/
public void generateRegions(final OpenHeightfield field) {
if (field == null) {
return;
/*
* Watershed Algorithm
*
* Reference: http://en.wikipedia.org/wiki/Watershed_%28algorithm%29
* A good visualization:
* http://artis.imag.fr/Publications/2003/HDS03/ (PDF)
*
* Summary:
*
* This algorithm utilizes the span.distanceToBorder() value, which
* is generated by the generateDistanceField() operation.
*
* Using the watershed analogy, the spans which are furthest from
* a border (highest distance to border) represent the lowest points
* in the watershed. A border span represents the highest possible
* water level.
*
* The main loop iterates, starting at the lowest point in the
* watershed, then incrementing with each loop until the highest
* allowed water level is reached. This slowly "floods" the spans
* starting at the lowest points.
*
* (Remember: From this algorithm's point of view "lower" refers
* to distance from a border, not height within the heightfield.)
*
* During each iteration of the loop, spans that are below the
* current water level are located and an attempt is made to either
* add them to exiting regions or create new regions from them.
*
* During the region expansion phase, if a newly flooded span
* borders on an existing region, it is usually added to the region.
*
* Any newly flooded span that survives the region expansion phase
* is used as a seed for a new region.
*
* At the end of the main loop, a final region expansion is
* performed which should catch any stray spans that escaped region
* assignment during the main loop.
*/
}
/*
* Represents the minimum distance to an obstacle that is considered
* traversable. I.e. Can't traverse spans closer than this distance
* to a border. This provides a way of artificially capping the
* height to which watershed flooding can occur.
* I.e. Don't let the algorithm flood all the way to the actual border.
*
* We add the minimum border distance to take into account the
* blurring algorithm which can result in a border span having a
* border distance > 0.
*/
final int minDist = this.mTraversableAreaBorderSize + field.minBorderDistance();
// TODO: EVAL: Figure out why this iteration limit is needed.
final int expandIterations = 4 + (this.mTraversableAreaBorderSize * 2);
/*
* This value represents the current distance from the border which
* is to be searched. The search starts at the maximum distance then
* moves toward zero. (Toward borders.)
*
* This number will always be divisible by 2.
*/
int dist = (field.maxBorderDistance() - 1) & ~1;
/*
* Contains a list of spans that are considered to be flooded and
* therefore are ready to be processed. This list may contain nulls
* at certain points in the process. Nulls indicate spans that were
* initially in the list but have been successfully added to a region.
* The initial size is arbitrary.
*/
final ArrayList<OpenHeightSpan> floodedSpans = new ArrayList<OpenHeightSpan>(1024);
/*
* A predefined stack for use in the flood operation. Its content
* has no meaning outside the new region flooding operation.
* (Saves on object creation time.)
*/
final ArrayDeque<OpenHeightSpan> workingStack = new ArrayDeque<OpenHeightSpan>(1024);
final OpenHeightFieldIterator iter = field.dataIterator();
// Zero is reserved for the null-region. So initializing to 1.
int nextRegionID = 1;
/*
* Search until the current distance reaches the minimum allowed
* distance.
*
* Note: This loop will not necessarily complete all region
* assignments. This is OK since a final region assignment step
* occurs after the loop iteration is complete.
*/
while (dist > minDist) {
// Find all spans that are at or below the current "water level"
// and are not already assigned to a region. Add these spans to
// the flooded span list for processing.
iter.reset();
floodedSpans.clear();
while (iter.hasNext()) {
final OpenHeightSpan span = iter.next();
if ((span.regionID() == NULL_REGION) && (span.distanceToBorder() >= dist)) {
// The span is not already assigned a region and is
// below the current "water level". So the span can be
// considered for region assignment.
floodedSpans.add(span);
}
}
if (nextRegionID > 1) {
// At least one region has already been created, so first
// try to put the newly flooded spans into existing regions.
if (dist > 0) {
expandRegions(floodedSpans, expandIterations);
} else {
expandRegions(floodedSpans, -1);
}
}
// Create new regions for all spans that could not be added to
// existing regions.
for (final OpenHeightSpan span : floodedSpans) {
if ((span == null) || (span.regionID() != 0)) {
// This span was assigned to a newly created region
// during an earlier iteration of this loop.
// So it can be skipped.
continue;
}
// Fill to slightly more than the current "water level".
// This improves efficiency of the algorithm.
final int fillTo = Math.max(dist - 2, minDist);
if (floodNewRegion(span, fillTo, nextRegionID, workingStack)) {
// A new region was successfully generated.
nextRegionID++;
}
}
// Increment the "water level" by 2, clamping at 0.
dist = Math.max(dist - 2, 0);
}
// Find all spans that haven't been assigned regions by the main loop.
// (Up to the minimum distance.)
iter.reset();
floodedSpans.clear();
while (iter.hasNext()) {
final OpenHeightSpan span = iter.next();
if ((span.distanceToBorder() >= minDist) && (span.regionID() == NULL_REGION)) {
// Not a border or null region span. Should be in a region.
floodedSpans.add(span);
}
}
// Perform a final expansion of existing regions.
// Allow more iterations than normal for this last expansion.
if (minDist > 0) {
expandRegions(floodedSpans, expandIterations * 8);
} else {
expandRegions(floodedSpans, -1);
}
field.setRegionCount(nextRegionID);
// Run the post processing algorithms.
for (final IOpenHeightFieldAlgorithm algorithm : this.mRegionAlgorithms) {
algorithm.apply(field);
}
}
/**
* Attempts to find the most appropriate regions to attach spans to.
* <p>
* Any spans successfully attached to a region will have their list entry
* set to null. So any non-null entries in the list will be spans for which
* a region could not be determined.
* </p>
*
* @param inoutSpans
* As input, the list of spans available for formation
* of new regions. As output, the spans that could not be assigned
* to new regions.
* @param maxIterations
* If set to -1, will iterate through completion.
*/
private void expandRegions(final ArrayList<OpenHeightSpan> inoutSpans, final int maxIterations) {
if (inoutSpans.size() == 0) {
return; // No spans available to process.
}
int iterCount = 0;
while (true) {
/*
* The number of spans in the working list that have been
* successfully processed or could not be processed successfully
* for some reason.
* This value controls when iteration ends.
*/
int skipped = 0;
// Loop through all spans in the working list.
for (int iSpan = 0; iSpan < inoutSpans.size(); iSpan++) {
final OpenHeightSpan span = inoutSpans.get(iSpan);
if (span == null) {
// The span originally at this index location has
// already been successfully assigned a region. Nothing
// else to do with it.
skipped++;
continue;
}
// Default to unassigned.
int spanRegion = NULL_REGION;
// Default to highest possible distance.
int regionCenterDist = Integer.MAX_VALUE;
/*
* Search through this span's axis-neighbors.
* Reference: Neighbor searches
* http://www.critterai.org/?q=nmgen_hfintro#nsearch
*/
for (int dir = 0; dir < 4; dir++) {
final OpenHeightSpan nSpan = span.getNeighbor(dir);
if (nSpan == null) {
// No neighbor at this location.
continue;
}
// There is a neighbor at this location.
if (nSpan.regionID() > NULL_REGION) {
/*
* This neighbor span belongs to a region.
*/
if ((nSpan.distanceToRegionCore() + 2) < regionCenterDist) {
/*
* This neighbor is closer to its region core
* than previously detected neighbors.
*/
int sameRegionCount = 0;
if (this.mUseConservativeExpansion) {
/*
* Check to ensure that this neighbor has
* at least two other neighbors in its region.
* This makes sure that adding this span to
* this neighbor's region will not result
* in a single width line of voxels.
*/
for (int ndir = 0; ndir < 4; ndir++) {
final OpenHeightSpan nnSpan = nSpan.getNeighbor(ndir);
if (nnSpan == null) {
// No diagonal-neighbor.
continue;
}
// There is a diagonal-neighbor
if (nnSpan.regionID() == nSpan.regionID()) {
// This neighbor has a neighbor in
// the same region.
sameRegionCount++;
}
}
}
if (!this.mUseConservativeExpansion || (sameRegionCount > 1)) {
/*
* Either conservative expansion is turned off,
* or it is on and this neighbor's region is
* acceptable for the current span.
* Choose this neighbor's region.
* Set the current distance to center as
* slightly further than this neighbor.
*/
spanRegion = nSpan.regionID();
regionCenterDist = nSpan.distanceToRegionCore() + 2;
}
}
}
}
if (spanRegion != NULL_REGION) {
// Found a suitable region for this span to belong to.
// Mark this index as having been processed.
inoutSpans.set(iSpan, null);
span.setRegionID(spanRegion);
span.setDistanceToRegionCore(regionCenterDist);
} else {
// Could not find an existing region for this span.
skipped++;
}
}
if (skipped == inoutSpans.size()) {
// All spans have either been processed or could not be
// processed during the last cycle.
break;
}
if (maxIterations != -1) {
iterCount++;
if (iterCount > maxIterations) {
// Reached the iteration limit.
break;
}
}
}
}
/**
* Creates a new region surrounding a span, adding neighbor spans to the
* new region as appropriate.
* <p>
* The new region creation will fail if the root span is on the border of an
* existing region.
* </p>
* <p>
* All spans added to the new region as part of this process become "core"
* spans with a distance to region core of zero.
* </p>
*
* @param rootSpan
* The span used to seed the new region.
* @param fillToDist
* The watershed distance to flood to.
* @param regionID
* The region ID to use for the new region.
* (If creation is successful.)
* @param workingStack
* A stack used internally. The content is
* cleared before use. Its content has no meaning outside of
* this operation.
* @return TRUE if a new region was created. Otherwise FALSE.
*/
private static boolean floodNewRegion(final OpenHeightSpan rootSpan, final int fillToDist,
final int regionID, final ArrayDeque<OpenHeightSpan> workingStack) {
workingStack.clear();
// TODO: EVAL: Change this into a working argument?
// Don't want unneeded object creation.
final ArrayList<OpenHeightSpan> workingList = new ArrayList<OpenHeightSpan>();
// See stack and list.
workingStack.push(rootSpan);
workingList.add(rootSpan);
rootSpan.setRegionID(regionID); // Seed with region id.
rootSpan.setDistanceToRegionCore(0); // Set as center of region.
int regionSize = 0;
while (workingStack.size() > 0) {
final OpenHeightSpan span = workingStack.pop();
/*
* Check regions of neighbor spans.
*
* If any neighbor is found to have a region assigned, then
* the current span can't be in the new region.
* (Want standard flooding algorithm to handle deciding which
* region this span should go in.)
*
* Up to 8 neighbors are checked.
*
* Reference: Neighbor searches.
* http://www.critterai.org/?q=nmgen_hfintro#nsearch
*/
boolean isOnRegionBorder = false;
for (int dir = 0; dir < 4; dir++) {
OpenHeightSpan nSpan = span.getNeighbor(dir);
if (nSpan == null) {
// No neighbor in this direction.
continue;
}
// Check this axis-neighbor.
if ((nSpan.regionID() != NULL_REGION) && (nSpan.regionID() != regionID)) {
// Current span borders the null region or another region.
// No need to check rest of neighbors.
isOnRegionBorder = true;
break;
}
// Check the diagonal-neighbor.
nSpan = nSpan.getNeighbor((dir + 1) & 0x3);
if ((nSpan != null) &&
(nSpan.regionID() != NULL_REGION) &&
(nSpan.regionID() != regionID)) {
// Current span borders the null region or another region.
// No need to check rest of neighbors.
isOnRegionBorder = true;
break;
}
}
if (isOnRegionBorder) {
// Current span borders the null region or another region.
// Can't be part of the new region.
span.setRegionID(NULL_REGION);
continue;
}
regionSize++;
// If got this far, we know the current span is part of the new
// region. Now check its neighbors to see if they should be
// assigned to this new region.
for (int dir = 0; dir < 4; dir++) {
final OpenHeightSpan nSpan = span.getNeighbor(dir);
if ((nSpan != null) &&
(nSpan.distanceToBorder() >= fillToDist) &&
(nSpan.regionID() == 0)) {
// This neighbor does not have a region assignment and
// it is within the allowed fill range. Set it as a
// candidate for this new region.
nSpan.setRegionID(regionID);
nSpan.setDistanceToRegionCore(0);
workingStack.push(nSpan);
workingList.add(nSpan);
}
}
}
return (regionSize > 0);
}
}