[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_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++;

View File

@ -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.
/// 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.
///
/// 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
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
/// 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
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;
int neighborFloor = neighborSpan.smax;
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)
// Only consider neighboring areas that have enough overlap to be potentially traversable.
if (Math.Min(ceiling, neighborCeiling) - Math.Max(floor, neighborFloor) < walkableHeight)
{
int accessibleNeighbourHeight = neighborBot - bot;
minNeighborHeight = Math.Min(minNeighborHeight, accessibleNeighbourHeight);
// No space to traverse between them.
continue;
}
// 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;
}
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
/// 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 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
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;
}
}
}
}