diff --git a/src/DotRecast.Recast/RcCompacts.cs b/src/DotRecast.Recast/RcCompacts.cs
index 82560b4..fdb8eff 100644
--- a/src/DotRecast.Recast/RcCompacts.cs
+++ b/src/DotRecast.Recast/RcCompacts.cs
@@ -33,7 +33,12 @@ namespace DotRecast.Recast
private const int MAX_LAYERS = RC_NOT_CONNECTED - 1;
private const int MAX_HEIGHT = RcConstants.SPAN_MAX_HEIGHT;
- /// @par
+ /// @}
+ /// @name Compact Heightfield Functions
+ /// @see rcCompactHeightfield
+ /// @{
+
+ /// Builds a compact heightfield representing open space, from a heightfield representing solid space.
///
/// This is just the beginning of the process of fully building a compact heightfield.
/// Various filters may be applied, then the distance field and regions built.
@@ -42,28 +47,38 @@ namespace DotRecast.Recast
/// See the #rcConfig documentation for more information on the configuration parameters.
///
/// @see rcAllocCompactHeightfield, rcHeightfield, rcCompactHeightfield, rcConfig
- public static RcCompactHeightfield BuildCompactHeightfield(RcTelemetry ctx, int walkableHeight, int walkableClimb, RcHeightfield hf)
+ /// @ingroup recast
+ ///
+ /// @param[in,out] context The build context to use during the operation.
+ /// @param[in] walkableHeight Minimum floor to 'ceiling' height that will still allow the floor area
+ /// to be considered walkable. [Limit: >= 3] [Units: vx]
+ /// @param[in] walkableClimb Maximum ledge height that is considered to still be traversable.
+ /// [Limit: >=0] [Units: vx]
+ /// @param[in] heightfield The heightfield to be compacted.
+ /// @param[out] compactHeightfield The resulting compact heightfield. (Must be pre-allocated.)
+ /// @returns True if the operation completed successfully.
+ public static RcCompactHeightfield BuildCompactHeightfield(RcTelemetry context, int walkableHeight, int walkableClimb, RcHeightfield heightfield)
{
- using var timer = ctx.ScopedTimer(RcTimerLabel.RC_TIMER_BUILD_COMPACTHEIGHTFIELD);
+ using var timer = context.ScopedTimer(RcTimerLabel.RC_TIMER_BUILD_COMPACTHEIGHTFIELD);
RcCompactHeightfield chf = new RcCompactHeightfield();
- int w = hf.width;
- int h = hf.height;
- int spanCount = GetHeightFieldSpanCount(hf);
+ int w = heightfield.width;
+ int h = heightfield.height;
+ int spanCount = GetHeightFieldSpanCount(context, heightfield);
// Fill in header.
chf.width = w;
chf.height = h;
- chf.borderSize = hf.borderSize;
+ chf.borderSize = heightfield.borderSize;
chf.spanCount = spanCount;
chf.walkableHeight = walkableHeight;
chf.walkableClimb = walkableClimb;
chf.maxRegions = 0;
- chf.bmin = hf.bmin;
- chf.bmax = hf.bmax;
- chf.bmax.Y += walkableHeight * hf.ch;
- chf.cs = hf.cs;
- chf.ch = hf.ch;
+ chf.bmin = heightfield.bmin;
+ chf.bmax = heightfield.bmax;
+ chf.bmax.Y += walkableHeight * heightfield.ch;
+ chf.cs = heightfield.cs;
+ chf.ch = heightfield.ch;
chf.cells = new RcCompactCell[w * h];
//chf.spans = new RcCompactSpan[spanCount];
chf.areas = new int[spanCount];
@@ -79,7 +94,7 @@ namespace DotRecast.Recast
{
for (int x = 0; x < w; ++x)
{
- RcSpan s = hf.spans[x + y * w];
+ RcSpan s = heightfield.spans[x + y * w];
// If there are no spans at this cell, just leave the data to index=0, count=0.
if (s == null)
continue;
@@ -167,16 +182,21 @@ namespace DotRecast.Recast
return chf;
}
- private static int GetHeightFieldSpanCount(RcHeightfield hf)
+ /// Returns the number of spans contained in the specified heightfield.
+ /// @ingroup recast
+ /// @param[in,out] context The build context to use during the operation.
+ /// @param[in] heightfield An initialized heightfield.
+ /// @returns The number of spans in the heightfield.
+ private static int GetHeightFieldSpanCount(RcTelemetry context, RcHeightfield heightfield)
{
- int w = hf.width;
- int h = hf.height;
+ int w = heightfield.width;
+ int h = heightfield.height;
int spanCount = 0;
for (int y = 0; y < h; ++y)
{
for (int x = 0; x < w; ++x)
{
- for (RcSpan s = hf.spans[x + y * w]; s != null; s = s.next)
+ for (RcSpan s = heightfield.spans[x + y * w]; s != null; s = s.next)
{
if (s.area != RC_NULL_AREA)
spanCount++;
diff --git a/src/DotRecast.Recast/RcFilters.cs b/src/DotRecast.Recast/RcFilters.cs
index fb0ce1f..7df99fd 100644
--- a/src/DotRecast.Recast/RcFilters.cs
+++ b/src/DotRecast.Recast/RcFilters.cs
@@ -28,150 +28,175 @@ namespace DotRecast.Recast
public static class RcFilters
{
- /// @par
+ /// Marks non-walkable spans as walkable if their maximum is within @p walkableClimb of the span below them.
///
- /// Allows the formation of walkable regions that will flow over low lying
- /// objects such as curbs, and up structures such as stairways.
- ///
- /// Two neighboring spans are walkable if: RcAbs(currentSpan.smax - neighborSpan.smax) < walkableClimb
- ///
- /// @warning Will override the effect of #rcFilterLedgeSpans. So if both filters are used, call
- /// #rcFilterLedgeSpans after calling this filter.
+ /// This removes small obstacles that the agent would be able to walk over such as curbs, and also allows agents to move up structures such as stairs.
+ /// This removes small obstacles and rasterization artifacts that the agent would be able to walk over
+ /// such as curbs. It also allows agents to move up terraced structures like stairs.
+ ///
+ /// Obstacle spans are marked walkable if: obstacleSpan.smax - walkableSpan.smax < walkableClimb
+ ///
+ /// @warning Will override the effect of #rcFilterLedgeSpans. If both filters are used, call #rcFilterLedgeSpans only after applying this filter.
///
/// @see rcHeightfield, rcConfig
- public static void FilterLowHangingWalkableObstacles(RcTelemetry ctx, int walkableClimb, RcHeightfield solid)
+ ///
+ /// @ingroup recast
+ /// @param[in,out] context The build context to use during the operation.
+ /// @param[in] walkableClimb Maximum ledge height that is considered to still be traversable.
+ /// [Limit: >=0] [Units: vx]
+ /// @param[in,out] heightfield A fully built heightfield. (All spans have been added.)
+ public static void FilterLowHangingWalkableObstacles(RcTelemetry context, int walkableClimb, RcHeightfield heightfield)
{
- using var timer = ctx.ScopedTimer(RcTimerLabel.RC_TIMER_FILTER_LOW_OBSTACLES);
+ using var timer = context.ScopedTimer(RcTimerLabel.RC_TIMER_FILTER_LOW_OBSTACLES);
- int w = solid.width;
- int h = solid.height;
+ int xSize = heightfield.width;
+ int zSize = heightfield.height;
- for (int y = 0; y < h; ++y)
+ for (int z = 0; z < zSize; ++z)
{
- for (int x = 0; x < w; ++x)
+ for (int x = 0; x < xSize; ++x)
{
- RcSpan ps = null;
- bool previousWalkable = false;
- int previousArea = RC_NULL_AREA;
+ RcSpan previousSpan = null;
+ bool previousWasWalkable = false;
+ int previousAreaID = RC_NULL_AREA;
- for (RcSpan s = solid.spans[x + y * w]; s != null; ps = s, s = s.next)
+ // For each span in the column...
+ for (RcSpan span = heightfield.spans[x + z * xSize]; span != null; previousSpan = span, span = span.next)
{
- bool walkable = s.area != RC_NULL_AREA;
- // If current span is not walkable, but there is walkable
- // span just below it, mark the span above it walkable too.
- if (!walkable && previousWalkable)
+ bool walkable = span.area != RC_NULL_AREA;
+ // If current span is not walkable, but there is walkable span just below it and the height difference
+ // is small enough for the agent to walk over, mark the current span as walkable too.
+ if (!walkable && previousWasWalkable && span.smax - previousSpan.smax <= walkableClimb)
{
- if (MathF.Abs(s.smax - ps.smax) <= walkableClimb)
- s.area = previousArea;
+ span.area = previousAreaID;
}
- // Copy walkable flag so that it cannot propagate
- // past multiple non-walkable objects.
- previousWalkable = walkable;
- previousArea = s.area;
+ // Copy the original walkable value regardless of whether we changed it.
+ // This prevents multiple consecutive non-walkable spans from being erroneously marked as walkable.
+ previousWasWalkable = walkable;
+ previousAreaID = span.area;
}
}
}
}
- /// @par
+ /// Marks spans that are ledges as not-walkable.
///
/// A ledge is a span with one or more neighbors whose maximum is further away than @p walkableClimb
/// from the current span's maximum.
- /// This method removes the impact of the overestimation of conservative voxelization
+ /// This method removes the impact of the overestimation of conservative voxelization
/// so the resulting mesh will not have regions hanging in the air over ledges.
- ///
- /// A span is a ledge if: RcAbs(currentSpan.smax - neighborSpan.smax) > walkableClimb
- ///
+ ///
+ /// A span is a ledge if: rcAbs(currentSpan.smax - neighborSpan.smax) > walkableClimb
+ ///
/// @see rcHeightfield, rcConfig
- public static void FilterLedgeSpans(RcTelemetry ctx, int walkableHeight, int walkableClimb, RcHeightfield heightfield)
+ ///
+ /// @ingroup recast
+ /// @param[in,out] context The build context to use during the operation.
+ /// @param[in] walkableHeight Minimum floor to 'ceiling' height that will still allow the floor area to
+ /// be considered walkable. [Limit: >= 3] [Units: vx]
+ /// @param[in] walkableClimb Maximum ledge height that is considered to still be traversable.
+ /// [Limit: >=0] [Units: vx]
+ /// @param[in,out] heightfield A fully built heightfield. (All spans have been added.)
+ public static void FilterLedgeSpans(RcTelemetry context, int walkableHeight, int walkableClimb, RcHeightfield heightfield)
{
- using var timer = ctx.ScopedTimer(RcTimerLabel.RC_TIMER_FILTER_BORDER);
+ using var timer = context.ScopedTimer(RcTimerLabel.RC_TIMER_FILTER_BORDER);
int xSize = heightfield.width;
int zSize = heightfield.height;
- // Mark border spans.
+ // Mark spans that are adjacent to a ledge as unwalkable..
for (int z = 0; z < zSize; ++z)
{
for (int x = 0; x < xSize; ++x)
{
for (RcSpan span = heightfield.spans[x + z * xSize]; span != null; span = span.next)
{
- // Skip non walkable spans.
+ // Skip non-walkable spans.
if (span.area == RC_NULL_AREA)
{
continue;
}
- int bot = (span.smax);
- int top = span.next != null ? span.next.smin : SPAN_MAX_HEIGHT;
+ int floor = (span.smax);
+ int ceiling = span.next != null ? span.next.smin : SPAN_MAX_HEIGHT;
- // Find neighbours minimum height.
- int minNeighborHeight = SPAN_MAX_HEIGHT;
+ // The difference between this walkable area and the lowest neighbor walkable area.
+ // This is the difference between the current span and all neighbor spans that have
+ // enough space for an agent to move between, but not accounting at all for surface slope.
+ int lowestNeighborFloorDifference = SPAN_MAX_HEIGHT;
// Min and max height of accessible neighbours.
- int accessibleNeighborMinHeight = span.smax;
- int accessibleNeighborMaxHeight = span.smax;
+ int lowestTraversableNeighborFloor = span.smax;
+ int highestTraversableNeighborFloor = span.smax;
for (int direction = 0; direction < 4; ++direction)
{
- int dx = x + GetDirOffsetX(direction);
- int dz = z + GetDirOffsetY(direction);
+ int neighborX = x + GetDirOffsetX(direction);
+ int neighborZ = z + GetDirOffsetY(direction);
+
// Skip neighbours which are out of bounds.
- if (dx < 0 || dz < 0 || dx >= xSize || dz >= zSize)
+ if (neighborX < 0 || neighborZ < 0 || neighborX >= xSize || neighborZ >= zSize)
{
- minNeighborHeight = (-walkableClimb - 1);
+ lowestNeighborFloorDifference = (-walkableClimb - 1);
break;
}
- // From minus infinity to the first span.
- RcSpan neighborSpan = heightfield.spans[dx + dz * xSize];
- int neighborTop = neighborSpan != null ? neighborSpan.smin : SPAN_MAX_HEIGHT;
-
+ RcSpan neighborSpan = heightfield.spans[neighborX + neighborZ * xSize];
+
+ // The most we can step down to the neighbor is the walkableClimb distance.
+ // Start with the area under the neighbor span
+ int neighborCeiling = neighborSpan != null ? neighborSpan.smin : SPAN_MAX_HEIGHT;
+
// Skip neightbour if the gap between the spans is too small.
- if (Math.Min(top, neighborTop) - bot >= walkableHeight)
+ if (Math.Min(ceiling, neighborCeiling) - floor >= walkableHeight)
{
- minNeighborHeight = (-walkableClimb - 1);
+ lowestNeighborFloorDifference = (-walkableClimb - 1);
break;
}
- // Rest of the spans.
- for (neighborSpan = heightfield.spans[dx + dz * xSize]; neighborSpan != null; neighborSpan = neighborSpan.next)
+ // For each span in the neighboring column...
+ for (; neighborSpan != null; neighborSpan = neighborSpan.next)
{
- int neighborBot = neighborSpan.smax;
- neighborTop = neighborSpan.next != null ? neighborSpan.next.smin : SPAN_MAX_HEIGHT;
-
- // Skip neightbour if the gap between the spans is too small.
- if (Math.Min(top, neighborTop) - Math.Max(bot, neighborBot) >= walkableHeight)
- {
- int accessibleNeighbourHeight = neighborBot - bot;
- minNeighborHeight = Math.Min(minNeighborHeight, accessibleNeighbourHeight);
+ int neighborFloor = neighborSpan.smax;
+ neighborCeiling = neighborSpan.next != null ? neighborSpan.next.smin : SPAN_MAX_HEIGHT;
- // Find min/max accessible neighbour height.
- if (MathF.Abs(accessibleNeighbourHeight) <= walkableClimb)
- {
- if (neighborBot < accessibleNeighborMinHeight) accessibleNeighborMinHeight = neighborBot;
- if (neighborBot > accessibleNeighborMaxHeight) accessibleNeighborMaxHeight = neighborBot;
- }
- else if (accessibleNeighbourHeight < -walkableClimb)
- {
- break;
- }
+ // Only consider neighboring areas that have enough overlap to be potentially traversable.
+ if (Math.Min(ceiling, neighborCeiling) - Math.Max(floor, neighborFloor) < walkableHeight)
+ {
+ // No space to traverse between them.
+ continue;
+ }
+
+ int neighborFloorDifference = neighborFloor - floor;
+ lowestNeighborFloorDifference = Math.Min(lowestNeighborFloorDifference, neighborFloorDifference);
+
+ // Find min/max accessible neighbor height.
+ // Only consider neighbors that are at most walkableClimb away.
+ if (MathF.Abs(neighborFloorDifference) <= walkableClimb)
+ {
+ // There is space to move to the neighbor cell and the slope isn't too much.
+ lowestTraversableNeighborFloor = Math.Min(lowestTraversableNeighborFloor, neighborFloor);
+ highestTraversableNeighborFloor = Math.Max(highestTraversableNeighborFloor, neighborFloor);
+ }
+ else if (neighborFloorDifference < -walkableClimb)
+ {
+ // We already know this will be considered a ledge span so we can early-out
+ break;
}
}
}
- // The current span is close to a ledge if the drop to any
- // neighbour span is less than the walkableClimb.
- if (minNeighborHeight < -walkableClimb)
+ // The current span is close to a ledge if the magnitude of the drop to any neighbour span is greater than the walkableClimb distance.
+ // That is, there is a gap that is large enough to let an agent move between them, but the drop (surface slope) is too large to allow it.
+ // (If this is the case, then biggestNeighborStepDown will be negative, so compare against the negative walkableClimb as a means of checking
+ // the magnitude of the delta)
+ if (lowestNeighborFloorDifference < -walkableClimb)
{
span.area = RC_NULL_AREA;
}
-
- // If the difference between all neighbours is too large,
- // we are at steep slope, mark the span as ledge.
- if ((accessibleNeighborMaxHeight - accessibleNeighborMinHeight) > walkableClimb)
+ // If the difference between all neighbor floors is too large, this is a steep slope, so mark the span as an unwalkable ledge.
+ else if ((highestTraversableNeighborFloor - lowestTraversableNeighborFloor) > walkableClimb)
{
span.area = RC_NULL_AREA;
}
@@ -180,31 +205,41 @@ namespace DotRecast.Recast
}
}
- /// @par
- ///
- /// For this filter, the clearance above the span is the distance from the span's
- /// maximum to the next higher span's minimum. (Same grid column.)
- ///
+ /// Marks walkable spans as not walkable if the clearance above the span is less than the specified walkableHeight.
+ ///
+ /// For this filter, the clearance above the span is the distance from the span's
+ /// maximum to the minimum of the next higher span in the same column.
+ /// If there is no higher span in the column, the clearance is computed as the
+ /// distance from the top of the span to the maximum heightfield height.
+ ///
/// @see rcHeightfield, rcConfig
- public static void FilterWalkableLowHeightSpans(RcTelemetry ctx, int walkableHeight, RcHeightfield solid)
+ /// @ingroup recast
+ ///
+ /// @param[in,out] context The build context to use during the operation.
+ /// @param[in] walkableHeight Minimum floor to 'ceiling' height that will still allow the floor area to
+ /// be considered walkable. [Limit: >= 3] [Units: vx]
+ /// @param[in,out] heightfield A fully built heightfield. (All spans have been added.)
+ public static void FilterWalkableLowHeightSpans(RcTelemetry context, int walkableHeight, RcHeightfield heightfield)
{
- using var timer = ctx.ScopedTimer(RcTimerLabel.RC_TIMER_FILTER_WALKABLE);
+ using var timer = context.ScopedTimer(RcTimerLabel.RC_TIMER_FILTER_WALKABLE);
- int w = solid.width;
- int h = solid.height;
+ int xSize = heightfield.width;
+ int zSize = heightfield.height;
// Remove walkable flag from spans which do not have enough
// space above them for the agent to stand there.
- for (int y = 0; y < h; ++y)
+ for (int z = 0; z < zSize; ++z)
{
- for (int x = 0; x < w; ++x)
+ for (int x = 0; x < xSize; ++x)
{
- for (RcSpan s = solid.spans[x + y * w]; s != null; s = s.next)
+ for (RcSpan span = heightfield.spans[x + z * xSize]; span != null; span = span.next)
{
- int bot = (s.smax);
- int top = s.next != null ? s.next.smin : SPAN_MAX_HEIGHT;
- if ((top - bot) < walkableHeight)
- s.area = RC_NULL_AREA;
+ int floor = (span.smax);
+ int ceiling = span.next != null ? span.next.smin : SPAN_MAX_HEIGHT;
+ if ((ceiling - floor) < walkableHeight)
+ {
+ span.area = RC_NULL_AREA;
+ }
}
}
}