diff --git a/Recast/Include/Recast.h b/Recast/Include/Recast.h index 2104aa5eb..5b34ea52b 100644 --- a/Recast/Include/Recast.h +++ b/Recast/Include/Recast.h @@ -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: obstacleSpan.smax - walkableSpan.smax < walkableClimb /// diff --git a/Recast/Source/RecastFilter.cpp b/Recast/Source/RecastFilter.cpp index 1ecf85883..7875040b9 100644 --- a/Recast/Source/RecastFilter.cpp +++ b/Recast/Source/RecastFilter.cpp @@ -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; } } } @@ -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; } @@ -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; } diff --git a/Tests/Recast/Tests_RecastFilter.cpp b/Tests/Recast/Tests_RecastFilter.cpp index 1882adba2..d6008356b 100644 --- a/Tests/Recast/Tests_RecastFilter.cpp +++ b/Tests/Recast/Tests_RecastFilter.cpp @@ -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; @@ -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; @@ -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); @@ -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]")