Skip to content

Commit

Permalink
Cleanup filter code and improved documentation (recastnavigation#683)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
grahamboree authored Jan 1, 2024
1 parent fc18ed8 commit c36a406
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 110 deletions.
3 changes: 2 additions & 1 deletion Recast/Include/Recast.h
Original file line number Diff line number Diff line change
Expand Up @@ -1002,7 +1002,8 @@ bool rcRasterizeTriangles(rcContext* context,

/// Marks non-walkable spans as walkable if their maximum is within @p walkableClimb of the span below them.
///
/// 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: <tt>obstacleSpan.smax - walkableSpan.smax < walkableClimb</tt>
///
Expand Down
126 changes: 70 additions & 56 deletions Recast/Source/RecastFilter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,24 @@ void rcFilterLowHangingWalkableObstacles(rcContext* context, const int walkableC
{
rcSpan* previousSpan = NULL;
bool previousWasWalkable = false;
unsigned char previousArea = RC_NULL_AREA;
unsigned char previousAreaID = RC_NULL_AREA;

// For each span in the column...
for (rcSpan* span = heightfield.spans[x + z * xSize]; span != NULL; previousSpan = span, span = span->next)
{
const bool walkable = span->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 && previousWasWalkable)

// 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 && (int)span->smax - (int)previousSpan->smax <= walkableClimb)
{
if (rcAbs((int)span->smax - (int)previousSpan->smax) <= walkableClimb)
{
span->area = previousArea;
}
span->area = previousAreaID;
}
// Copy walkable flag so that it cannot propagate
// past multiple non-walkable objects.

// 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;
previousArea = span->area;
previousAreaID = span->area;
}
}
}
Expand All @@ -73,84 +73,98 @@ void rcFilterLedgeSpans(rcContext* context, const int walkableHeight, const int
const int xSize = heightfield.width;
const 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; span = span->next)
{
// Skip non walkable spans.
// Skip non-walkable spans.
if (span->area == RC_NULL_AREA)
{
continue;
}

const int bot = (int)(span->smax);
const int top = span->next ? (int)(span->next->smin) : MAX_HEIGHTFIELD_HEIGHT;
const int floor = (int)(span->smax);
const int ceiling = span->next ? (int)(span->next->smin) : MAX_HEIGHTFIELD_HEIGHT;

// Find neighbours minimum height.
int minNeighborHeight = MAX_HEIGHTFIELD_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 = MAX_HEIGHTFIELD_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 + rcGetDirOffsetX(direction);
int dz = z + rcGetDirOffsetY(direction);
const int neighborX = x + rcGetDirOffsetX(direction);
const int neighborZ = z + rcGetDirOffsetY(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.
const rcSpan* neighborSpan = heightfield.spans[dx + dz * xSize];
int neighborTop = neighborSpan ? (int)neighborSpan->smin : MAX_HEIGHTFIELD_HEIGHT;

const 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 ? (int)neighborSpan->smin : MAX_HEIGHTFIELD_HEIGHT;

// Skip neighbour if the gap between the spans is too small.
if (rcMin(top, neighborTop) - bot >= walkableHeight)
if (rcMin(ceiling, neighborCeiling) - floor >= walkableHeight)
{
minNeighborHeight = (-walkableClimb - 1);
lowestNeighborFloorDifference = (-walkableClimb - 1);
break;
}

// Rest of the spans.
for (neighborSpan = heightfield.spans[dx + dz * xSize]; neighborSpan; neighborSpan = neighborSpan->next)
// For each span in the neighboring column...
for (; neighborSpan != NULL; neighborSpan = neighborSpan->next)
{
int neighborBot = (int)neighborSpan->smax;
neighborTop = neighborSpan->next ? (int)neighborSpan->next->smin : MAX_HEIGHTFIELD_HEIGHT;

// Skip neighbour if the gap between the spans is too small.
if (rcMin(top, neighborTop) - rcMax(bot, neighborBot) >= walkableHeight)
const int neighborFloor = (int)neighborSpan->smax;
neighborCeiling = neighborSpan->next ? (int)neighborSpan->next->smin : MAX_HEIGHTFIELD_HEIGHT;

// Only consider neighboring areas that have enough overlap to be potentially traversable.
if (rcMin(ceiling, neighborCeiling) - rcMax(floor, neighborFloor) < walkableHeight)
{
// No space to traverse between them.
continue;
}

const int neighborFloorDifference = neighborFloor - floor;
lowestNeighborFloorDifference = rcMin(lowestNeighborFloorDifference, neighborFloorDifference);

// Find min/max accessible neighbor height.
// Only consider neighbors that are at most walkableClimb away.
if (rcAbs(neighborFloorDifference) <= walkableClimb)
{
// There is space to move to the neighbor cell and the slope isn't too much.
lowestTraversableNeighborFloor = rcMin(lowestTraversableNeighborFloor, neighborFloor);
highestTraversableNeighborFloor = rcMax(highestTraversableNeighborFloor, neighborFloor);
}
else if (neighborFloorDifference < -walkableClimb)
{
int accessibleNeighbourHeight = neighborBot - bot;
minNeighborHeight = rcMin(minNeighborHeight, accessibleNeighbourHeight);

// Find min/max accessible neighbour height.
if (rcAbs(accessibleNeighbourHeight) <= walkableClimb)
{
if (neighborBot < accessibleNeighborMinHeight) accessibleNeighborMinHeight = neighborBot;
if (neighborBot > accessibleNeighborMaxHeight) accessibleNeighborMaxHeight = neighborBot;
}
else if (accessibleNeighbourHeight < -walkableClimb)
{
break;
}
// 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.
else 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;
}
Expand All @@ -175,9 +189,9 @@ void rcFilterWalkableLowHeightSpans(rcContext* context, const int walkableHeight
{
for (rcSpan* span = heightfield.spans[x + z*xSize]; span; span = span->next)
{
const int bot = (int)(span->smax);
const int top = span->next ? (int)(span->next->smin) : MAX_HEIGHTFIELD_HEIGHT;
if ((top - bot) < walkableHeight)
const int floor = (int)(span->smax);
const int ceiling = span->next ? (int)(span->next->smin) : MAX_HEIGHTFIELD_HEIGHT;
if (ceiling - floor < walkableHeight)
{
span->area = RC_NULL_AREA;
}
Expand Down
56 changes: 3 additions & 53 deletions Tests/Recast/Tests_RecastFilter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ TEST_CASE("rcFilterLowHangingWalkableObstacles", "[recast, filtering]")
{
// Put the second span just above the first one.
rcSpan* secondSpan = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM);
secondSpan->area = 1;
secondSpan->area = RC_NULL_AREA;
secondSpan->next = NULL;
secondSpan->smin = 1 + walkableHeight;
secondSpan->smax = secondSpan->smin + 1;
Expand All @@ -64,7 +64,7 @@ TEST_CASE("rcFilterLowHangingWalkableObstacles", "[recast, filtering]")

// Check that nothing has changed.
REQUIRE(heightfield.spans[0]->area == 1);
REQUIRE(heightfield.spans[0]->next->area == 1);
REQUIRE(heightfield.spans[0]->next->area == RC_NULL_AREA);

// Check again but with a more clearance
secondSpan->smin += 10;
Expand All @@ -74,7 +74,7 @@ TEST_CASE("rcFilterLowHangingWalkableObstacles", "[recast, filtering]")

// Check that nothing has changed.
REQUIRE(heightfield.spans[0]->area == 1);
REQUIRE(heightfield.spans[0]->next->area == 1);
REQUIRE(heightfield.spans[0]->next->area == RC_NULL_AREA);

rcFree(span);
rcFree(secondSpan);
Expand Down Expand Up @@ -251,56 +251,6 @@ TEST_CASE("rcFilterLedgeSpans", "[recast, filtering]")
}
}
}

SECTION("Edge spans are marked unwalkable")
{
// Create a flat plane.
for (int x = 0; x < heightfield.width; ++x)
{
for (int z = 0; z < heightfield.height; ++z)
{
rcSpan* span = (rcSpan*)rcAlloc(sizeof(rcSpan), RC_ALLOC_PERM);
span->area = 1;
span->next = NULL;
span->smin = 0;
span->smax = 1;
heightfield.spans[x + z * heightfield.width] = span;
}
}

rcFilterLedgeSpans(&context, walkableHeight, walkableClimb, heightfield);

for (int x = 0; x < heightfield.width; ++x)
{
for (int z = 0; z < heightfield.height; ++z)
{
rcSpan* span = heightfield.spans[x + z * heightfield.width];
REQUIRE(span != NULL);

if (x == 0 || z == 0 || x == 9 || z == 9)
{
REQUIRE(span->area == RC_NULL_AREA);
}
else
{
REQUIRE(span->area == 1);
}

REQUIRE(span->next == NULL);
REQUIRE(span->smin == 0);
REQUIRE(span->smax == 1);
}
}

// Free all the heightfield spans
for (int x = 0; x < heightfield.width; ++x)
{
for (int z = 0; z < heightfield.height; ++z)
{
rcFree(heightfield.spans[x + z * heightfield.width]);
}
}
}
}

TEST_CASE("rcFilterWalkableLowHeightSpans", "[recast, filtering]")
Expand Down

0 comments on commit c36a406

Please sign in to comment.