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; + } } } }