[Upstream] Cleanup filter code and improved documentation (https://github.com/recastnavigation/recastnavigation/pull/683)

- https://github.com/recastnavigation/recastnavigation/pull/683

This mostly just changes variable names and adds some comments to make the code more clear.

It also has a few small fixup changes to the unit tests.
This commit is contained in:
ikpil 2024-01-03 14:09:19 +09:00 committed by Ikpil
parent 652b8d751a
commit be73850965
2 changed files with 168 additions and 113 deletions

View File

@ -33,7 +33,12 @@ namespace DotRecast.Recast
private const int MAX_LAYERS = RC_NOT_CONNECTED - 1; private const int MAX_LAYERS = RC_NOT_CONNECTED - 1;
private const int MAX_HEIGHT = RcConstants.SPAN_MAX_HEIGHT; 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. /// 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. /// 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 the #rcConfig documentation for more information on the configuration parameters.
/// ///
/// @see rcAllocCompactHeightfield, rcHeightfield, rcCompactHeightfield, rcConfig /// @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(); RcCompactHeightfield chf = new RcCompactHeightfield();
int w = hf.width; int w = heightfield.width;
int h = hf.height; int h = heightfield.height;
int spanCount = GetHeightFieldSpanCount(hf); int spanCount = GetHeightFieldSpanCount(context, heightfield);
// Fill in header. // Fill in header.
chf.width = w; chf.width = w;
chf.height = h; chf.height = h;
chf.borderSize = hf.borderSize; chf.borderSize = heightfield.borderSize;
chf.spanCount = spanCount; chf.spanCount = spanCount;
chf.walkableHeight = walkableHeight; chf.walkableHeight = walkableHeight;
chf.walkableClimb = walkableClimb; chf.walkableClimb = walkableClimb;
chf.maxRegions = 0; chf.maxRegions = 0;
chf.bmin = hf.bmin; chf.bmin = heightfield.bmin;
chf.bmax = hf.bmax; chf.bmax = heightfield.bmax;
chf.bmax.Y += walkableHeight * hf.ch; chf.bmax.Y += walkableHeight * heightfield.ch;
chf.cs = hf.cs; chf.cs = heightfield.cs;
chf.ch = hf.ch; chf.ch = heightfield.ch;
chf.cells = new RcCompactCell[w * h]; chf.cells = new RcCompactCell[w * h];
//chf.spans = new RcCompactSpan[spanCount]; //chf.spans = new RcCompactSpan[spanCount];
chf.areas = new int[spanCount]; chf.areas = new int[spanCount];
@ -79,7 +94,7 @@ namespace DotRecast.Recast
{ {
for (int x = 0; x < w; ++x) 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 there are no spans at this cell, just leave the data to index=0, count=0.
if (s == null) if (s == null)
continue; continue;
@ -167,16 +182,21 @@ namespace DotRecast.Recast
return chf; 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 w = heightfield.width;
int h = hf.height; int h = heightfield.height;
int spanCount = 0; int spanCount = 0;
for (int y = 0; y < h; ++y) for (int y = 0; y < h; ++y)
{ {
for (int x = 0; x < w; ++x) 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) if (s.area != RC_NULL_AREA)
spanCount++; spanCount++;

View File

@ -28,150 +28,175 @@ namespace DotRecast.Recast
public static class RcFilters 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 /// 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.
/// objects such as curbs, and up structures such as stairways. /// 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.
/// Two neighboring spans are walkable if: <tt>RcAbs(currentSpan.smax - neighborSpan.smax) < walkableClimb</tt> ///
/// /// Obstacle spans are marked walkable if: <tt>obstacleSpan.smax - walkableSpan.smax < walkableClimb</tt>
/// @warning Will override the effect of #rcFilterLedgeSpans. So if both filters are used, call ///
/// #rcFilterLedgeSpans after calling this filter. /// @warning Will override the effect of #rcFilterLedgeSpans. If both filters are used, call #rcFilterLedgeSpans only after applying this filter.
/// ///
/// @see rcHeightfield, rcConfig /// @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 xSize = heightfield.width;
int h = solid.height; 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; RcSpan previousSpan = null;
bool previousWalkable = false; bool previousWasWalkable = false;
int previousArea = RC_NULL_AREA; 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; bool walkable = span.area != RC_NULL_AREA;
// If current span is not walkable, but there is walkable // If current span is not walkable, but there is walkable span just below it and the height difference
// span just below it, mark the span above it walkable too. // is small enough for the agent to walk over, mark the current span as walkable too.
if (!walkable && previousWalkable) if (!walkable && previousWasWalkable && span.smax - previousSpan.smax <= walkableClimb)
{ {
if (MathF.Abs(s.smax - ps.smax) <= walkableClimb) span.area = previousAreaID;
s.area = previousArea;
} }
// Copy walkable flag so that it cannot propagate // Copy the original walkable value regardless of whether we changed it.
// past multiple non-walkable objects. // This prevents multiple consecutive non-walkable spans from being erroneously marked as walkable.
previousWalkable = walkable; previousWasWalkable = walkable;
previousArea = s.area; 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 /// A ledge is a span with one or more neighbors whose maximum is further away than @p walkableClimb
/// from the current span's maximum. /// 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. /// so the resulting mesh will not have regions hanging in the air over ledges.
/// ///
/// A span is a ledge if: <tt>RcAbs(currentSpan.smax - neighborSpan.smax) > walkableClimb</tt> /// A span is a ledge if: <tt>rcAbs(currentSpan.smax - neighborSpan.smax) > walkableClimb</tt>
/// ///
/// @see rcHeightfield, rcConfig /// @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 xSize = heightfield.width;
int zSize = heightfield.height; 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 z = 0; z < zSize; ++z)
{ {
for (int x = 0; x < xSize; ++x) for (int x = 0; x < xSize; ++x)
{ {
for (RcSpan span = heightfield.spans[x + z * xSize]; span != null; span = span.next) 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) if (span.area == RC_NULL_AREA)
{ {
continue; continue;
} }
int bot = (span.smax); int floor = (span.smax);
int top = span.next != null ? span.next.smin : SPAN_MAX_HEIGHT; int ceiling = span.next != null ? span.next.smin : SPAN_MAX_HEIGHT;
// Find neighbours minimum height. // The difference between this walkable area and the lowest neighbor walkable area.
int minNeighborHeight = SPAN_MAX_HEIGHT; // 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. // Min and max height of accessible neighbours.
int accessibleNeighborMinHeight = span.smax; int lowestTraversableNeighborFloor = span.smax;
int accessibleNeighborMaxHeight = span.smax; int highestTraversableNeighborFloor = span.smax;
for (int direction = 0; direction < 4; ++direction) for (int direction = 0; direction < 4; ++direction)
{ {
int dx = x + GetDirOffsetX(direction); int neighborX = x + GetDirOffsetX(direction);
int dz = z + GetDirOffsetY(direction); int neighborZ = z + GetDirOffsetY(direction);
// Skip neighbours which are out of bounds. // 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; break;
} }
// From minus infinity to the first span. RcSpan neighborSpan = heightfield.spans[neighborX + neighborZ * xSize];
RcSpan neighborSpan = heightfield.spans[dx + dz * xSize];
int neighborTop = neighborSpan != null ? neighborSpan.smin : SPAN_MAX_HEIGHT; // 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. // 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; break;
} }
// Rest of the spans. // For each span in the neighboring column...
for (neighborSpan = heightfield.spans[dx + dz * xSize]; neighborSpan != null; neighborSpan = neighborSpan.next) for (; neighborSpan != null; neighborSpan = neighborSpan.next)
{ {
int neighborBot = neighborSpan.smax; int neighborFloor = neighborSpan.smax;
neighborTop = neighborSpan.next != null ? neighborSpan.next.smin : SPAN_MAX_HEIGHT; neighborCeiling = 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);
// Find min/max accessible neighbour height. // Only consider neighboring areas that have enough overlap to be potentially traversable.
if (MathF.Abs(accessibleNeighbourHeight) <= walkableClimb) if (Math.Min(ceiling, neighborCeiling) - Math.Max(floor, neighborFloor) < walkableHeight)
{ {
if (neighborBot < accessibleNeighborMinHeight) accessibleNeighborMinHeight = neighborBot; // No space to traverse between them.
if (neighborBot > accessibleNeighborMaxHeight) accessibleNeighborMaxHeight = neighborBot; continue;
} }
else if (accessibleNeighbourHeight < -walkableClimb)
{ int neighborFloorDifference = neighborFloor - floor;
break; 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 // The current span is close to a ledge if the magnitude of the drop to any neighbour span is greater than the walkableClimb distance.
// neighbour span is less than the walkableClimb. // 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 (minNeighborHeight < -walkableClimb) // (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; span.area = RC_NULL_AREA;
} }
// If the difference between all neighbor floors is too large, this is a steep slope, so mark the span as an unwalkable ledge.
// If the difference between all neighbours is too large, else if ((highestTraversableNeighborFloor - lowestTraversableNeighborFloor) > walkableClimb)
// we are at steep slope, mark the span as ledge.
if ((accessibleNeighborMaxHeight - accessibleNeighborMinHeight) > walkableClimb)
{ {
span.area = RC_NULL_AREA; span.area = RC_NULL_AREA;
} }
@ -180,31 +205,41 @@ namespace DotRecast.Recast
} }
} }
/// @par /// 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 /// 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.) /// 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 /// @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 xSize = heightfield.width;
int h = solid.height; int zSize = heightfield.height;
// Remove walkable flag from spans which do not have enough // Remove walkable flag from spans which do not have enough
// space above them for the agent to stand there. // 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 floor = (span.smax);
int top = s.next != null ? s.next.smin : SPAN_MAX_HEIGHT; int ceiling = span.next != null ? span.next.smin : SPAN_MAX_HEIGHT;
if ((top - bot) < walkableHeight) if ((ceiling - floor) < walkableHeight)
s.area = RC_NULL_AREA; {
span.area = RC_NULL_AREA;
}
} }
} }
} }