diff --git a/src/DotRecast.Core/RcVec3f.cs b/src/DotRecast.Core/RcVec3f.cs index 6f63870..9092892 100644 --- a/src/DotRecast.Core/RcVec3f.cs +++ b/src/DotRecast.Core/RcVec3f.cs @@ -230,6 +230,24 @@ namespace DotRecast.Core } } + public const float EPSILON = 1e-6f; + + /// Normalizes the vector if the length is greater than zero. + /// If the magnitude is zero, the vector is unchanged. + /// @param[in,out] v The vector to normalize. [(x, y, z)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SafeNormalize() + { + float sqMag = RcMath.Sqr(x) + RcMath.Sqr(y) + RcMath.Sqr(z); + if (sqMag > EPSILON) + { + float inverseMag = 1.0f / (float)Math.Sqrt(sqMag); + x *= inverseMag; + y *= inverseMag; + z *= inverseMag; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Min(float[] @in, int i) { @@ -262,7 +280,7 @@ namespace DotRecast.Core y = Math.Max(y, @in[i + 1]); z = Math.Max(z, @in[i + 2]); } - + public override string ToString() { return $"{x}, {y}, {z}"; diff --git a/src/DotRecast.Recast/PolyUtils.cs b/src/DotRecast.Recast/PolyUtils.cs index 4bda6cf..791c0d0 100644 --- a/src/DotRecast.Recast/PolyUtils.cs +++ b/src/DotRecast.Recast/PolyUtils.cs @@ -25,106 +25,163 @@ namespace DotRecast.Recast { public static class PolyUtils { - public static bool PointInPoly(float[] verts, RcVec3f p) + // public static bool PointInPoly(float[] verts, RcVec3f p) + // { + // bool c = false; + // int i, j; + // for (i = 0, j = verts.Length - 3; i < verts.Length; j = i, i += 3) + // { + // int vi = i; + // int vj = j; + // if (((verts[vi + 2] > p.z) != (verts[vj + 2] > p.z)) + // && (p.x < (verts[vj] - verts[vi]) * (p.z - verts[vi + 2]) / (verts[vj + 2] - verts[vi + 2]) + // + verts[vi])) + // c = !c; + // } + // + // return c; + // } + + // TODO (graham): This is duplicated in the ConvexVolumeTool in RecastDemo + /// Checks if a point is contained within a polygon + /// + /// @param[in] numVerts Number of vertices in the polygon + /// @param[in] verts The polygon vertices + /// @param[in] point The point to check + /// @returns true if the point lies within the polygon, false otherwise. + public static bool PointInPoly(float[] verts, RcVec3f point) { - int i, j; - bool c = false; - for (i = 0, j = verts.Length / 3 - 1; i < verts.Length / 3; j = i++) + bool inPoly = false; + for (int i = 0, j = verts.Length / 3 - 1; i < verts.Length / 3; j = i++) { RcVec3f vi = RcVec3f.Of(verts[i * 3], verts[i * 3 + 1], verts[i * 3 + 2]); RcVec3f vj = RcVec3f.Of(verts[j * 3], verts[j * 3 + 1], verts[j * 3 + 2]); - if (((vi.z > p.z) != (vj.z > p.z)) - && (p.x < (vj.x - vi.x) * (p.z - vi.z) / (vj.z - vi.z) + vi.x)) + if (vi.z > point.z == vj.z > point.z) { - c = !c; + continue; } + + if (point.x >= (vj.x - vi.x) * (point.z - vi.z) / (vj.z - vi.z) + vi.x) + { + continue; + } + + inPoly = !inPoly; } - return c; + return inPoly; } - public static int OffsetPoly(float[] verts, int nverts, float offset, float[] outVerts, int maxOutVerts) + /// Expands a convex polygon along its vertex normals by the given offset amount. + /// Inserts extra vertices to bevel sharp corners. + /// + /// Helper function to offset convex polygons for rcMarkConvexPolyArea. + /// + /// @ingroup recast + /// + /// @param[in] verts The vertices of the polygon [Form: (x, y, z) * @p numVerts] + /// @param[in] numVerts The number of vertices in the polygon. + /// @param[in] offset How much to offset the polygon by. [Units: wu] + /// @param[out] outVerts The offset vertices (should hold up to 2 * @p numVerts) [Form: (x, y, z) * return value] + /// @param[in] maxOutVerts The max number of vertices that can be stored to @p outVerts. + /// @returns Number of vertices in the offset polygon or 0 if too few vertices in @p outVerts. + public static int OffsetPoly(float[] verts, int numVerts, float offset, float[] outVerts, int maxOutVerts) { + // Defines the limit at which a miter becomes a bevel. + // Similar in behavior to https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-miterlimit const float MITER_LIMIT = 1.20f; - int n = 0; + int numOutVerts = 0; - for (int i = 0; i < nverts; i++) + for (int vertIndex = 0; vertIndex < numVerts; vertIndex++) { - int a = (i + nverts - 1) % nverts; - int b = i; - int c = (i + 1) % nverts; - int va = a * 3; - int vb = b * 3; - int vc = c * 3; - float dx0 = verts[vb] - verts[va]; - float dy0 = verts[vb + 2] - verts[va + 2]; - float d0 = dx0 * dx0 + dy0 * dy0; - if (d0 > 1e-6f) - { - d0 = (float)(1.0f / Math.Sqrt(d0)); - dx0 *= d0; - dy0 *= d0; - } + int vertIndexA = (vertIndex + numVerts - 1) % numVerts; + int vertIndexB = vertIndex; + int vertIndexC = (vertIndex + 1) % numVerts; + + RcVec3f vertA = RcVec3f.Of(verts, vertIndexA * 3); + RcVec3f vertB = RcVec3f.Of(verts, vertIndexB * 3); + RcVec3f vertC = RcVec3f.Of(verts, vertIndexC * 3); + + // From A to B on the x/z plane + RcVec3f prevSegmentDir = vertB.Subtract(vertA); + prevSegmentDir.y = 0; // Squash onto x/z plane + prevSegmentDir.SafeNormalize(); + + // From B to C on the x/z plane + RcVec3f currSegmentDir = vertC.Subtract(vertB); + currSegmentDir.y = 0; // Squash onto x/z plane + currSegmentDir.SafeNormalize(); + + // The y component of the cross product of the two normalized segment directions. + // The X and Z components of the cross product are both zero because the two + // segment direction vectors fall within the x/z plane. + float cross = currSegmentDir.x * prevSegmentDir.z - prevSegmentDir.x * currSegmentDir.z; - float dx1 = verts[vc] - verts[vb]; - float dy1 = verts[vc + 2] - verts[vb + 2]; - float d1 = dx1 * dx1 + dy1 * dy1; - if (d1 > 1e-6f) - { - d1 = (float)(1.0f / Math.Sqrt(d1)); - dx1 *= d1; - dy1 *= d1; - } + // CCW perpendicular vector to AB. The segment normal. + float prevSegmentNormX = -prevSegmentDir.z; + float prevSegmentNormZ = prevSegmentDir.x; - float dlx0 = -dy0; - float dly0 = dx0; - float dlx1 = -dy1; - float dly1 = dx1; - float cross = dx1 * dy0 - dx0 * dy1; - float dmx = (dlx0 + dlx1) * 0.5f; - float dmy = (dly0 + dly1) * 0.5f; - float dmr2 = dmx * dmx + dmy * dmy; - bool bevel = dmr2 * MITER_LIMIT * MITER_LIMIT < 1.0f; - if (dmr2 > 1e-6f) - { - float scale = 1.0f / dmr2; - dmx *= scale; - dmy *= scale; - } + // CCW perpendicular vector to BC. The segment normal. + float currSegmentNormX = -currSegmentDir.z; + float currSegmentNormZ = currSegmentDir.x; - if (bevel && cross < 0.0f) + // Average the two segment normals to get the proportional miter offset for B. + // This isn't normalized because it's defining the distance and direction the corner will need to be + // adjusted proportionally to the edge offsets to properly miter the adjoining edges. + float cornerMiterX = (prevSegmentNormX + currSegmentNormX) * 0.5f; + float cornerMiterZ = (prevSegmentNormZ + currSegmentNormZ) * 0.5f; + float cornerMiterSqMag = RcMath.Sqr(cornerMiterX) + RcMath.Sqr(cornerMiterZ); + + // If the magnitude of the segment normal average is less than about .69444, + // the corner is an acute enough angle that the result should be beveled. + bool bevel = cornerMiterSqMag * MITER_LIMIT * MITER_LIMIT < 1.0f; + + // Scale the corner miter so it's proportional to how much the corner should be offset compared to the edges. + if (cornerMiterSqMag > RcVec3f.EPSILON) { - if (n + 2 > maxOutVerts) + float scale = 1.0f / cornerMiterSqMag; + cornerMiterX *= scale; + cornerMiterZ *= scale; + } + + if (bevel && cross < 0.0f) // If the corner is convex and an acute enough angle, generate a bevel. + { + if (numOutVerts + 2 > maxOutVerts) { return 0; } - float d = (1.0f - (dx0 * dx1 + dy0 * dy1)) * 0.5f; - outVerts[n * 3 + 0] = verts[vb] + (-dlx0 + dx0 * d) * offset; - outVerts[n * 3 + 1] = verts[vb + 1]; - outVerts[n * 3 + 2] = verts[vb + 2] + (-dly0 + dy0 * d) * offset; - n++; - outVerts[n * 3 + 0] = verts[vb] + (-dlx1 - dx1 * d) * offset; - outVerts[n * 3 + 1] = verts[vb + 1]; - outVerts[n * 3 + 2] = verts[vb + 2] + (-dly1 - dy1 * d) * offset; - n++; + // Generate two bevel vertices at a distances from B proportional to the angle between the two segments. + // Move each bevel vertex out proportional to the given offset. + float d = (1.0f - (prevSegmentDir.x * currSegmentDir.x + prevSegmentDir.z * currSegmentDir.z)) * 0.5f; + + outVerts[numOutVerts * 3 + 0] = vertB.x + (-prevSegmentNormX + prevSegmentDir.x * d) * offset; + outVerts[numOutVerts * 3 + 1] = vertB.y; + outVerts[numOutVerts * 3 + 2] = vertB.z + (-prevSegmentNormZ + prevSegmentDir.z * d) * offset; + numOutVerts++; + + outVerts[numOutVerts * 3 + 0] = vertB.x + (-currSegmentNormX - currSegmentDir.x * d) * offset; + outVerts[numOutVerts * 3 + 1] = vertB.y; + outVerts[numOutVerts * 3 + 2] = vertB.z + (-currSegmentNormZ - currSegmentDir.z * d) * offset; + numOutVerts++; } else { - if (n + 1 > maxOutVerts) + if (numOutVerts + 1 > maxOutVerts) { return 0; } - outVerts[n * 3 + 0] = verts[vb] - dmx * offset; - outVerts[n * 3 + 1] = verts[vb + 1]; - outVerts[n * 3 + 2] = verts[vb + 2] - dmy * offset; - n++; + // Move B along the miter direction by the specified offset. + outVerts[numOutVerts * 3 + 0] = vertB.x - cornerMiterX * offset; + outVerts[numOutVerts * 3 + 1] = vertB.y; + outVerts[numOutVerts * 3 + 2] = vertB.z - cornerMiterZ * offset; + numOutVerts++; } } - return n; + return numOutVerts; } } } \ No newline at end of file diff --git a/src/DotRecast.Recast/RecastArea.cs b/src/DotRecast.Recast/RecastArea.cs index 5da81c0..a9d1daf 100644 --- a/src/DotRecast.Recast/RecastArea.cs +++ b/src/DotRecast.Recast/RecastArea.cs @@ -27,115 +27,142 @@ namespace DotRecast.Recast public static class RecastArea { - /// @par - /// - /// Basically, any spans that are closer to a boundary or obstruction than the specified radius - /// are marked as unwalkable. + /// Erodes the walkable area within the heightfield by the specified radius. + /// + /// Basically, any spans that are closer to a boundary or obstruction than the specified radius + /// are marked as un-walkable. /// /// This method is usually called immediately after the heightfield has been built. - /// + /// /// @see rcCompactHeightfield, rcBuildCompactHeightfield, rcConfig::walkableRadius - public static void ErodeWalkableArea(RcTelemetry ctx, int radius, RcCompactHeightfield chf) + /// @ingroup recast + /// + /// @param[in,out] context The build context to use during the operation. + /// @param[in] erosionRadius The radius of erosion. [Limits: 0 < value < 255] [Units: vx] + /// @param[in,out] compactHeightfield The populated compact heightfield to erode. + /// @returns True if the operation completed successfully. + public static void ErodeWalkableArea(RcTelemetry context, int erosionRadius, RcCompactHeightfield compactHeightfield) { - int w = chf.width; - int h = chf.height; - using var timer = ctx.ScopedTimer(RcTimerLabel.RC_TIMER_ERODE_AREA); + int xSize = compactHeightfield.width; + int zSize = compactHeightfield.height; + int zStride = xSize; // For readability + + using var timer = context.ScopedTimer(RcTimerLabel.RC_TIMER_ERODE_AREA); + + int[] distanceToBoundary = new int[compactHeightfield.spanCount]; + Array.Fill(distanceToBoundary, 255); - int[] dist = new int[chf.spanCount]; - Array.Fill(dist, 255); // Mark boundary cells. - 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) { - RcCompactCell c = chf.cells[x + y * w]; - for (int i = c.index, ni = c.index + c.count; i < ni; ++i) + RcCompactCell cell = compactHeightfield.cells[x + z * zStride]; + for (int spanIndex = cell.index, maxSpanIndex = cell.index + cell.count; spanIndex < maxSpanIndex; ++spanIndex) { - if (chf.areas[i] == RC_NULL_AREA) + if (compactHeightfield.areas[spanIndex] == RC_NULL_AREA) { - dist[i] = 0; + distanceToBoundary[spanIndex] = 0; } else { - RcCompactSpan s = chf.spans[i]; - int nc = 0; - for (int dir = 0; dir < 4; ++dir) + RcCompactSpan span = compactHeightfield.spans[spanIndex]; + + // Check that there is a non-null adjacent span in each of the 4 cardinal directions. + int neighborCount = 0; + for (int direction = 0; direction < 4; ++direction) { - if (RecastCommon.GetCon(s, dir) != RC_NOT_CONNECTED) + int neighborConnection = RecastCommon.GetCon(span, direction); + if (neighborConnection == RC_NOT_CONNECTED) { - int nx = x + RecastCommon.GetDirOffsetX(dir); - int ny = y + RecastCommon.GetDirOffsetY(dir); - int nidx = chf.cells[nx + ny * w].index + RecastCommon.GetCon(s, dir); - if (chf.areas[nidx] != RC_NULL_AREA) - { - nc++; - } + break; } + + int neighborX = x + RecastCommon.GetDirOffsetX(direction); + int neighborZ = z + RecastCommon.GetDirOffsetY(direction); + int neighborSpanIndex = compactHeightfield.cells[neighborX + neighborZ * zStride].index + RecastCommon.GetCon(span, direction); + if (compactHeightfield.areas[neighborSpanIndex] == RC_NULL_AREA) + { + break; + } + + neighborCount++; } - // At least one missing neighbour. - if (nc != 4) - dist[i] = 0; + // At least one missing neighbour, so this is a boundary cell. + if (neighborCount != 4) + { + distanceToBoundary[spanIndex] = 0; + } } } } } - int nd; + int newDistance; // Pass 1 - 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) { - RcCompactCell c = chf.cells[x + y * w]; - for (int i = c.index, ni = c.index + c.count; i < ni; ++i) + RcCompactCell cell = compactHeightfield.cells[x + z * zStride]; + int maxSpanIndex = cell.index + cell.count; + for (int spanIndex = cell.index; spanIndex < maxSpanIndex; ++spanIndex) { - RcCompactSpan s = chf.spans[i]; + RcCompactSpan span = compactHeightfield.spans[spanIndex]; - if (RecastCommon.GetCon(s, 0) != RC_NOT_CONNECTED) + if (RecastCommon.GetCon(span, 0) != RC_NOT_CONNECTED) { // (-1,0) - int ax = x + RecastCommon.GetDirOffsetX(0); - int ay = y + RecastCommon.GetDirOffsetY(0); - int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, 0); - RcCompactSpan @as = chf.spans[ai]; - nd = Math.Min(dist[ai] + 2, 255); - if (nd < dist[i]) - dist[i] = nd; + int aX = x + RecastCommon.GetDirOffsetX(0); + int aY = z + RecastCommon.GetDirOffsetY(0); + int aIndex = compactHeightfield.cells[aX + aY * xSize].index + RecastCommon.GetCon(span, 0); + RcCompactSpan aSpan = compactHeightfield.spans[aIndex]; + newDistance = Math.Min(distanceToBoundary[aIndex] + 2, 255); + if (newDistance < distanceToBoundary[spanIndex]) + { + distanceToBoundary[spanIndex] = newDistance; + } // (-1,-1) - if (RecastCommon.GetCon(@as, 3) != RC_NOT_CONNECTED) + if (RecastCommon.GetCon(aSpan, 3) != RC_NOT_CONNECTED) { - int aax = ax + RecastCommon.GetDirOffsetX(3); - int aay = ay + RecastCommon.GetDirOffsetY(3); - int aai = chf.cells[aax + aay * w].index + RecastCommon.GetCon(@as, 3); - nd = Math.Min(dist[aai] + 3, 255); - if (nd < dist[i]) - dist[i] = nd; + int bX = aX + RecastCommon.GetDirOffsetX(3); + int bY = aY + RecastCommon.GetDirOffsetY(3); + int bIndex = compactHeightfield.cells[bX + bY * xSize].index + RecastCommon.GetCon(aSpan, 3); + newDistance = Math.Min(distanceToBoundary[bIndex] + 3, 255); + if (newDistance < distanceToBoundary[spanIndex]) + { + distanceToBoundary[spanIndex] = newDistance; + } } } - if (RecastCommon.GetCon(s, 3) != RC_NOT_CONNECTED) + if (RecastCommon.GetCon(span, 3) != RC_NOT_CONNECTED) { // (0,-1) - int ax = x + RecastCommon.GetDirOffsetX(3); - int ay = y + RecastCommon.GetDirOffsetY(3); - int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, 3); - RcCompactSpan @as = chf.spans[ai]; - nd = Math.Min(dist[ai] + 2, 255); - if (nd < dist[i]) - dist[i] = nd; + int aX = x + RecastCommon.GetDirOffsetX(3); + int aY = z + RecastCommon.GetDirOffsetY(3); + int aIndex = compactHeightfield.cells[aX + aY * xSize].index + RecastCommon.GetCon(span, 3); + RcCompactSpan aSpan = compactHeightfield.spans[aIndex]; + newDistance = Math.Min(distanceToBoundary[aIndex] + 2, 255); + if (newDistance < distanceToBoundary[spanIndex]) + { + distanceToBoundary[spanIndex] = newDistance; + } // (1,-1) - if (RecastCommon.GetCon(@as, 2) != RC_NOT_CONNECTED) + if (RecastCommon.GetCon(aSpan, 2) != RC_NOT_CONNECTED) { - int aax = ax + RecastCommon.GetDirOffsetX(2); - int aay = ay + RecastCommon.GetDirOffsetY(2); - int aai = chf.cells[aax + aay * w].index + RecastCommon.GetCon(@as, 2); - nd = Math.Min(dist[aai] + 3, 255); - if (nd < dist[i]) - dist[i] = nd; + int bX = aX + RecastCommon.GetDirOffsetX(2); + int bY = aY + RecastCommon.GetDirOffsetY(2); + int bIndex = compactHeightfield.cells[bX + bY * xSize].index + RecastCommon.GetCon(aSpan, 2); + newDistance = Math.Min(distanceToBoundary[bIndex] + 3, 255); + if (newDistance < distanceToBoundary[spanIndex]) + { + distanceToBoundary[spanIndex] = newDistance; + } } } } @@ -143,219 +170,289 @@ namespace DotRecast.Recast } // Pass 2 - for (int y = h - 1; y >= 0; --y) + for (int z = zSize - 1; z >= 0; --z) { - for (int x = w - 1; x >= 0; --x) + for (int x = xSize - 1; x >= 0; --x) { - RcCompactCell c = chf.cells[x + y * w]; - for (int i = c.index, ni = c.index + c.count; i < ni; ++i) + RcCompactCell cell = compactHeightfield.cells[x + z * zStride]; + int maxSpanIndex = cell.index + cell.count; + for (int i = cell.index; i < maxSpanIndex; ++i) { - RcCompactSpan s = chf.spans[i]; + RcCompactSpan span = compactHeightfield.spans[i]; - if (RecastCommon.GetCon(s, 2) != RC_NOT_CONNECTED) + if (RecastCommon.GetCon(span, 2) != RC_NOT_CONNECTED) { // (1,0) - int ax = x + RecastCommon.GetDirOffsetX(2); - int ay = y + RecastCommon.GetDirOffsetY(2); - int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, 2); - RcCompactSpan @as = chf.spans[ai]; - nd = Math.Min(dist[ai] + 2, 255); - if (nd < dist[i]) - dist[i] = nd; + int aX = x + RecastCommon.GetDirOffsetX(2); + int aY = z + RecastCommon.GetDirOffsetY(2); + int aIndex = compactHeightfield.cells[aX + aY * xSize].index + RecastCommon.GetCon(span, 2); + RcCompactSpan aSpan = compactHeightfield.spans[aIndex]; + newDistance = Math.Min(distanceToBoundary[aIndex] + 2, 255); + if (newDistance < distanceToBoundary[i]) + { + distanceToBoundary[i] = newDistance; + } // (1,1) - if (RecastCommon.GetCon(@as, 1) != RC_NOT_CONNECTED) + if (RecastCommon.GetCon(aSpan, 1) != RC_NOT_CONNECTED) { - int aax = ax + RecastCommon.GetDirOffsetX(1); - int aay = ay + RecastCommon.GetDirOffsetY(1); - int aai = chf.cells[aax + aay * w].index + RecastCommon.GetCon(@as, 1); - nd = Math.Min(dist[aai] + 3, 255); - if (nd < dist[i]) - dist[i] = nd; - } - } - - if (RecastCommon.GetCon(s, 1) != RC_NOT_CONNECTED) - { - // (0,1) - int ax = x + RecastCommon.GetDirOffsetX(1); - int ay = y + RecastCommon.GetDirOffsetY(1); - int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, 1); - RcCompactSpan @as = chf.spans[ai]; - nd = Math.Min(dist[ai] + 2, 255); - if (nd < dist[i]) - dist[i] = nd; - - // (-1,1) - if (RecastCommon.GetCon(@as, 0) != RC_NOT_CONNECTED) - { - int aax = ax + RecastCommon.GetDirOffsetX(0); - int aay = ay + RecastCommon.GetDirOffsetY(0); - int aai = chf.cells[aax + aay * w].index + RecastCommon.GetCon(@as, 0); - nd = Math.Min(dist[aai] + 3, 255); - if (nd < dist[i]) - dist[i] = nd; - } - } - } - } - } - - int thr = radius * 2; - for (int i = 0; i < chf.spanCount; ++i) - if (dist[i] < thr) - chf.areas[i] = RC_NULL_AREA; - } - - /// @par - /// - /// This filter is usually applied after applying area id's using functions - /// such as #rcMarkBoxArea, #rcMarkConvexPolyArea, and #rcMarkCylinderArea. - /// - /// @see rcCompactHeightfield - public static bool MedianFilterWalkableArea(RcTelemetry ctx, RcCompactHeightfield chf) - { - int w = chf.width; - int h = chf.height; - - using var timer = ctx.ScopedTimer(RcTimerLabel.RC_TIMER_MEDIAN_AREA); - - int[] areas = new int[chf.spanCount]; - - for (int y = 0; y < h; ++y) - { - for (int x = 0; x < w; ++x) - { - RcCompactCell c = chf.cells[x + y * w]; - for (int i = c.index, ni = c.index + c.count; i < ni; ++i) - { - RcCompactSpan s = chf.spans[i]; - if (chf.areas[i] == RC_NULL_AREA) - { - areas[i] = chf.areas[i]; - continue; - } - - int[] nei = new int[9]; - for (int j = 0; j < 9; ++j) - nei[j] = chf.areas[i]; - - for (int dir = 0; dir < 4; ++dir) - { - if (RecastCommon.GetCon(s, dir) != RC_NOT_CONNECTED) - { - int ax = x + RecastCommon.GetDirOffsetX(dir); - int ay = y + RecastCommon.GetDirOffsetY(dir); - int ai = chf.cells[ax + ay * w].index + RecastCommon.GetCon(s, dir); - if (chf.areas[ai] != RC_NULL_AREA) - nei[dir * 2 + 0] = chf.areas[ai]; - - RcCompactSpan @as = chf.spans[ai]; - int dir2 = (dir + 1) & 0x3; - if (RecastCommon.GetCon(@as, dir2) != RC_NOT_CONNECTED) + int bX = aX + RecastCommon.GetDirOffsetX(1); + int bY = aY + RecastCommon.GetDirOffsetY(1); + int bIndex = compactHeightfield.cells[bX + bY * xSize].index + RecastCommon.GetCon(aSpan, 1); + newDistance = Math.Min(distanceToBoundary[bIndex] + 3, 255); + if (newDistance < distanceToBoundary[i]) { - int ax2 = ax + RecastCommon.GetDirOffsetX(dir2); - int ay2 = ay + RecastCommon.GetDirOffsetY(dir2); - int ai2 = chf.cells[ax2 + ay2 * w].index + RecastCommon.GetCon(@as, dir2); - if (chf.areas[ai2] != RC_NULL_AREA) - nei[dir * 2 + 1] = chf.areas[ai2]; + distanceToBoundary[i] = newDistance; } } } - Array.Sort(nei); - areas[i] = nei[4]; - } - } - } - - chf.areas = areas; - - return true; - } - - /// @par - /// - /// The value of spacial parameters are in world units. - /// - /// @see rcCompactHeightfield, rcMedianFilterWalkableArea - public static void MarkBoxArea(RcTelemetry ctx, float[] bmin, float[] bmax, RcAreaModification areaMod, RcCompactHeightfield chf) - { - using var timer = ctx.ScopedTimer(RcTimerLabel.RC_TIMER_MARK_BOX_AREA); - - int minx = (int)((bmin[0] - chf.bmin.x) / chf.cs); - int miny = (int)((bmin[1] - chf.bmin.y) / chf.ch); - int minz = (int)((bmin[2] - chf.bmin.z) / chf.cs); - int maxx = (int)((bmax[0] - chf.bmin.x) / chf.cs); - int maxy = (int)((bmax[1] - chf.bmin.y) / chf.ch); - int maxz = (int)((bmax[2] - chf.bmin.z) / chf.cs); - - if (maxx < 0) - return; - if (minx >= chf.width) - return; - if (maxz < 0) - return; - if (minz >= chf.height) - return; - - if (minx < 0) - minx = 0; - if (maxx >= chf.width) - maxx = chf.width - 1; - if (minz < 0) - minz = 0; - if (maxz >= chf.height) - maxz = chf.height - 1; - - for (int z = minz; z <= maxz; ++z) - { - for (int x = minx; x <= maxx; ++x) - { - RcCompactCell c = chf.cells[x + z * chf.width]; - for (int i = c.index, ni = c.index + c.count; i < ni; ++i) - { - RcCompactSpan s = chf.spans[i]; - if (s.y >= miny && s.y <= maxy) + if (RecastCommon.GetCon(span, 1) != RC_NOT_CONNECTED) { - if (chf.areas[i] != RC_NULL_AREA) - chf.areas[i] = areaMod.Apply(chf.areas[i]); + // (0,1) + int aX = x + RecastCommon.GetDirOffsetX(1); + int aY = z + RecastCommon.GetDirOffsetY(1); + int aIndex = compactHeightfield.cells[aX + aY * xSize].index + RecastCommon.GetCon(span, 1); + RcCompactSpan aSpan = compactHeightfield.spans[aIndex]; + newDistance = Math.Min(distanceToBoundary[aIndex] + 2, 255); + if (newDistance < distanceToBoundary[i]) + { + distanceToBoundary[i] = newDistance; + } + + // (-1,1) + if (RecastCommon.GetCon(aSpan, 0) != RC_NOT_CONNECTED) + { + int bX = aX + RecastCommon.GetDirOffsetX(0); + int bY = aY + RecastCommon.GetDirOffsetY(0); + int bIndex = compactHeightfield.cells[bX + bY * xSize].index + RecastCommon.GetCon(aSpan, 0); + newDistance = Math.Min(distanceToBoundary[bIndex] + 3, 255); + if (newDistance < distanceToBoundary[i]) + { + distanceToBoundary[i] = newDistance; + } + } } } } } + + int minBoundaryDistance = erosionRadius * 2; + for (int spanIndex = 0; spanIndex < compactHeightfield.spanCount; ++spanIndex) + { + if (distanceToBoundary[spanIndex] < minBoundaryDistance) + { + compactHeightfield.areas[spanIndex] = RC_NULL_AREA; + } + } } - static bool PointInPoly(float[] verts, RcVec3f p) + /// Applies a median filter to walkable area types (based on area id), removing noise. + /// + /// This filter is usually applied after applying area id's using functions + /// such as #rcMarkBoxArea, #rcMarkConvexPolyArea, and #rcMarkCylinderArea. + /// + /// @see rcCompactHeightfield + /// @ingroup recast + /// + /// @param[in,out] context The build context to use during the operation. + /// @param[in,out] compactHeightfield A populated compact heightfield. + /// @returns True if the operation completed successfully. + public static bool MedianFilterWalkableArea(RcTelemetry context, RcCompactHeightfield compactHeightfield) { - bool c = false; - int i, j; - for (i = 0, j = verts.Length - 3; i < verts.Length; j = i, i += 3) + int xSize = compactHeightfield.width; + int zSize = compactHeightfield.height; + int zStride = xSize; // For readability + + using var timer = context.ScopedTimer(RcTimerLabel.RC_TIMER_MEDIAN_AREA); + + int[] areas = new int[compactHeightfield.spanCount]; + + for (int z = 0; z < zSize; ++z) { - int vi = i; - int vj = j; - if (((verts[vi + 2] > p.z) != (verts[vj + 2] > p.z)) - && (p.x < (verts[vj] - verts[vi]) * (p.z - verts[vi + 2]) / (verts[vj + 2] - verts[vi + 2]) - + verts[vi])) - c = !c; + for (int x = 0; x < xSize; ++x) + { + RcCompactCell cell = compactHeightfield.cells[x + z * zStride]; + int maxSpanIndex = cell.index + cell.count; + for (int spanIndex = cell.index; spanIndex < maxSpanIndex; ++spanIndex) + { + RcCompactSpan span = compactHeightfield.spans[spanIndex]; + if (compactHeightfield.areas[spanIndex] == RC_NULL_AREA) + { + areas[spanIndex] = compactHeightfield.areas[spanIndex]; + continue; + } + + int[] neighborAreas = new int[9]; + for (int neighborIndex = 0; neighborIndex < 9; ++neighborIndex) + { + neighborAreas[neighborIndex] = compactHeightfield.areas[spanIndex]; + } + + for (int dir = 0; dir < 4; ++dir) + { + if (RecastCommon.GetCon(span, dir) == RC_NOT_CONNECTED) + { + continue; + } + + int aX = x + RecastCommon.GetDirOffsetX(dir); + int aZ = z + RecastCommon.GetDirOffsetY(dir); + int aIndex = compactHeightfield.cells[aX + aZ * zStride].index + RecastCommon.GetCon(span, dir); + if (compactHeightfield.areas[aIndex] != RC_NULL_AREA) + { + neighborAreas[dir * 2 + 0] = compactHeightfield.areas[aIndex]; + } + + RcCompactSpan aSpan = compactHeightfield.spans[aIndex]; + int dir2 = (dir + 1) & 0x3; + int neighborConnection2 = RecastCommon.GetCon(aSpan, dir2); + if (neighborConnection2 != RC_NOT_CONNECTED) + { + int bX = aX + RecastCommon.GetDirOffsetX(dir2); + int bZ = aZ + RecastCommon.GetDirOffsetY(dir2); + int bIndex = compactHeightfield.cells[bX + bZ * zStride].index + RecastCommon.GetCon(aSpan, dir2); + if (compactHeightfield.areas[bIndex] != RC_NULL_AREA) + { + neighborAreas[dir * 2 + 1] = compactHeightfield.areas[bIndex]; + } + } + } + + Array.Sort(neighborAreas); + areas[spanIndex] = neighborAreas[4]; + } + } } - return c; + compactHeightfield.areas = areas; + + return true; } - /// @par + /// Applies an area id to all spans within the specified bounding box. (AABB) + /// + /// @see rcCompactHeightfield, rcMedianFilterWalkableArea + /// @ingroup recast + /// + /// @param[in,out] context The build context to use during the operation. + /// @param[in] boxMinBounds The minimum extents of the bounding box. [(x, y, z)] [Units: wu] + /// @param[in] boxMaxBounds The maximum extents of the bounding box. [(x, y, z)] [Units: wu] + /// @param[in] areaId The area id to apply. [Limit: <= #RC_WALKABLE_AREA] + /// @param[in,out] compactHeightfield A populated compact heightfield. + public static void MarkBoxArea(RcTelemetry context, float[] boxMinBounds, float[] boxMaxBounds, RcAreaModification areaId, RcCompactHeightfield compactHeightfield) + { + using var timer = context.ScopedTimer(RcTimerLabel.RC_TIMER_MARK_BOX_AREA); + + int xSize = compactHeightfield.width; + int zSize = compactHeightfield.height; + int zStride = xSize; // For readability + + // Find the footprint of the box area in grid cell coordinates. + int minX = (int)((boxMinBounds[0] - compactHeightfield.bmin.x) / compactHeightfield.cs); + int minY = (int)((boxMinBounds[1] - compactHeightfield.bmin.y) / compactHeightfield.ch); + int minZ = (int)((boxMinBounds[2] - compactHeightfield.bmin.z) / compactHeightfield.cs); + int maxX = (int)((boxMaxBounds[0] - compactHeightfield.bmin.x) / compactHeightfield.cs); + int maxY = (int)((boxMaxBounds[1] - compactHeightfield.bmin.y) / compactHeightfield.ch); + int maxZ = (int)((boxMaxBounds[2] - compactHeightfield.bmin.z) / compactHeightfield.cs); + + if (maxX < 0) + { + return; + } + + if (minX >= xSize) + { + return; + } + + if (maxZ < 0) + { + return; + } + + if (minZ >= zSize) + { + return; + } + + if (minX < 0) + { + minX = 0; + } + + if (maxX >= xSize) + { + maxX = xSize - 1; + } + + if (minZ < 0) + { + minZ = 0; + } + + if (maxZ >= zSize) + { + maxZ = zSize - 1; + } + + for (int z = minZ; z <= maxZ; ++z) + { + for (int x = minX; x <= maxX; ++x) + { + RcCompactCell cell = compactHeightfield.cells[x + z * zStride]; + int maxSpanIndex = cell.index + cell.count; + for (int spanIndex = cell.index; spanIndex < maxSpanIndex; ++spanIndex) + { + RcCompactSpan span = compactHeightfield.spans[spanIndex]; + + // Skip if the span is outside the box extents. + if (span.y < minY || span.y > maxY) + { + continue; + } + + // Skip if the span has been removed. + if (compactHeightfield.areas[spanIndex] == RC_NULL_AREA) + { + continue; + } + + // Mark the span. + compactHeightfield.areas[spanIndex] = areaId.Apply(compactHeightfield.areas[spanIndex]); + } + } + } + } + + /// Applies the area id to the all spans within the specified convex polygon. /// /// The value of spacial parameters are in world units. - /// - /// The y-values of the polygon vertices are ignored. So the polygon is effectively - /// projected onto the xz-plane at @p hmin, then extruded to @p hmax. - /// + /// + /// The y-values of the polygon vertices are ignored. So the polygon is effectively + /// projected onto the xz-plane, translated to @p minY, and extruded to @p maxY. + /// /// @see rcCompactHeightfield, rcMedianFilterWalkableArea - public static void MarkConvexPolyArea(RcTelemetry ctx, float[] verts, float hmin, float hmax, RcAreaModification areaMod, - RcCompactHeightfield chf) + /// @ingroup recast + /// + /// @param[in,out] context The build context to use during the operation. + /// @param[in] verts The vertices of the polygon [For: (x, y, z) * @p numVerts] + /// @param[in] numVerts The number of vertices in the polygon. + /// @param[in] minY The height of the base of the polygon. [Units: wu] + /// @param[in] maxY The height of the top of the polygon. [Units: wu] + /// @param[in] areaId The area id to apply. [Limit: <= #RC_WALKABLE_AREA] + /// @param[in,out] compactHeightfield A populated compact heightfield. + public static void MarkConvexPolyArea(RcTelemetry context, float[] verts, + float minY, float maxY, RcAreaModification areaId, + RcCompactHeightfield compactHeightfield) { - using var timer = ctx.ScopedTimer(RcTimerLabel.RC_TIMER_MARK_CONVEXPOLY_AREA); + using var timer = context.ScopedTimer(RcTimerLabel.RC_TIMER_MARK_CONVEXPOLY_AREA); + int xSize = compactHeightfield.width; + int zSize = compactHeightfield.height; + int zStride = xSize; // For readability + + // Compute the bounding box of the polygon RcVec3f bmin = new RcVec3f(); RcVec3f bmax = new RcVec3f(); RcVec3f.Copy(ref bmin, verts, 0); @@ -366,129 +463,214 @@ namespace DotRecast.Recast bmax.Max(verts, i); } - bmin.y = hmin; - bmax.y = hmax; + bmin.y = minY; + bmax.y = maxY; - int minx = (int)((bmin.x - chf.bmin.x) / chf.cs); - int miny = (int)((bmin.y - chf.bmin.y) / chf.ch); - int minz = (int)((bmin.z - chf.bmin.z) / chf.cs); - int maxx = (int)((bmax.x - chf.bmin.x) / chf.cs); - int maxy = (int)((bmax.y - chf.bmin.y) / chf.ch); - int maxz = (int)((bmax.z - chf.bmin.z) / chf.cs); + // Compute the grid footprint of the polygon + int minx = (int)((bmin.x - compactHeightfield.bmin.x) / compactHeightfield.cs); + int miny = (int)((bmin.y - compactHeightfield.bmin.y) / compactHeightfield.ch); + int minz = (int)((bmin.z - compactHeightfield.bmin.z) / compactHeightfield.cs); + int maxx = (int)((bmax.x - compactHeightfield.bmin.x) / compactHeightfield.cs); + int maxy = (int)((bmax.y - compactHeightfield.bmin.y) / compactHeightfield.ch); + int maxz = (int)((bmax.z - compactHeightfield.bmin.z) / compactHeightfield.cs); + // Early-out if the polygon lies entirely outside the grid. if (maxx < 0) + { return; - if (minx >= chf.width) - return; - if (maxz < 0) - return; - if (minz >= chf.height) - return; + } + if (minx >= xSize) + { + return; + } + + if (maxz < 0) + { + return; + } + + if (minz >= zSize) + { + return; + } + + // Clamp the polygon footprint to the grid if (minx < 0) + { minx = 0; - if (maxx >= chf.width) - maxx = chf.width - 1; + } + + if (maxx >= xSize) + { + maxx = xSize - 1; + } + if (minz < 0) + { minz = 0; - if (maxz >= chf.height) - maxz = chf.height - 1; + } + + if (maxz >= zSize) + { + maxz = zSize - 1; + } // TODO: Optimize. for (int z = minz; z <= maxz; ++z) { for (int x = minx; x <= maxx; ++x) { - RcCompactCell c = chf.cells[x + z * chf.width]; - for (int i = c.index, ni = c.index + c.count; i < ni; ++i) + RcCompactCell cell = compactHeightfield.cells[x + z * zStride]; + int maxSpanIndex = cell.index + cell.count; + for (int spanIndex = cell.index; spanIndex < maxSpanIndex; ++spanIndex) { - RcCompactSpan s = chf.spans[i]; - if (chf.areas[i] == RC_NULL_AREA) - continue; - if (s.y >= miny && s.y <= maxy) - { - RcVec3f p = new RcVec3f(); - p.x = chf.bmin.x + (x + 0.5f) * chf.cs; - p.y = 0; - p.z = chf.bmin.z + (z + 0.5f) * chf.cs; + RcCompactSpan span = compactHeightfield.spans[spanIndex]; - if (PointInPoly(verts, p)) - { - chf.areas[i] = areaMod.Apply(chf.areas[i]); - } + // Skip if span is removed. + if (compactHeightfield.areas[spanIndex] == RC_NULL_AREA) + continue; + + // Skip if y extents don't overlap. + if (span.y < miny || span.y > maxy) + { + continue; + } + + RcVec3f point = new RcVec3f( + compactHeightfield.bmin.x + (x + 0.5f) * compactHeightfield.cs, + 0, + compactHeightfield.bmin.z + (z + 0.5f) * compactHeightfield.cs + ); + + if (PolyUtils.PointInPoly(verts, point)) + { + compactHeightfield.areas[spanIndex] = areaId.Apply(compactHeightfield.areas[spanIndex]); } } } } } - /// @par - /// - /// The value of spacial parameters are in world units. - /// + + /// Applies the area id to all spans within the specified y-axis-aligned cylinder. + /// /// @see rcCompactHeightfield, rcMedianFilterWalkableArea - public static void MarkCylinderArea(RcTelemetry ctx, float[] pos, float r, float h, RcAreaModification areaMod, RcCompactHeightfield chf) + /// + /// @ingroup recast + /// + /// @param[in,out] context The build context to use during the operation. + /// @param[in] position The center of the base of the cylinder. [Form: (x, y, z)] [Units: wu] + /// @param[in] radius The radius of the cylinder. [Units: wu] [Limit: > 0] + /// @param[in] height The height of the cylinder. [Units: wu] [Limit: > 0] + /// @param[in] areaId The area id to apply. [Limit: <= #RC_WALKABLE_AREA] + /// @param[in,out] compactHeightfield A populated compact heightfield. + public static void MarkCylinderArea(RcTelemetry context, float[] position, float radius, float height, + RcAreaModification areaId, RcCompactHeightfield compactHeightfield) { - using var timer = ctx.ScopedTimer(RcTimerLabel.RC_TIMER_MARK_CYLINDER_AREA); + using var timer = context.ScopedTimer(RcTimerLabel.RC_TIMER_MARK_CYLINDER_AREA); - RcVec3f bmin = new RcVec3f(); - RcVec3f bmax = new RcVec3f(); - bmin.x = pos[0] - r; - bmin.y = pos[1]; - bmin.z = pos[2] - r; - bmax.x = pos[0] + r; - bmax.y = pos[1] + h; - bmax.z = pos[2] + r; - float r2 = r * r; + int xSize = compactHeightfield.width; + int zSize = compactHeightfield.height; + int zStride = xSize; // For readability - int minx = (int)((bmin.x - chf.bmin.x) / chf.cs); - int miny = (int)((bmin.y - chf.bmin.y) / chf.ch); - int minz = (int)((bmin.z - chf.bmin.z) / chf.cs); - int maxx = (int)((bmax.x - chf.bmin.x) / chf.cs); - int maxy = (int)((bmax.y - chf.bmin.y) / chf.ch); - int maxz = (int)((bmax.z - chf.bmin.z) / chf.cs); + // Compute the bounding box of the cylinder + RcVec3f cylinderBBMin = new RcVec3f( + position[0] - radius, + position[1], + position[2] - radius + ); + RcVec3f cylinderBBMax = new RcVec3f( + position[0] + radius, + position[1] + height, + position[2] + radius + ); + + // Compute the grid footprint of the cylinder + int minx = (int)((cylinderBBMin.x - compactHeightfield.bmin.x) / compactHeightfield.cs); + int miny = (int)((cylinderBBMin.y - compactHeightfield.bmin.y) / compactHeightfield.ch); + int minz = (int)((cylinderBBMin.z - compactHeightfield.bmin.z) / compactHeightfield.cs); + int maxx = (int)((cylinderBBMax.x - compactHeightfield.bmin.x) / compactHeightfield.cs); + int maxy = (int)((cylinderBBMax.y - compactHeightfield.bmin.y) / compactHeightfield.ch); + int maxz = (int)((cylinderBBMax.z - compactHeightfield.bmin.z) / compactHeightfield.cs); + + // Early-out if the cylinder is completely outside the grid bounds. if (maxx < 0) + { return; - if (minx >= chf.width) + } + + if (minx >= xSize) + { return; + } + if (maxz < 0) + { return; - if (minz >= chf.height) - return; + } + if (minz >= zSize) + { + return; + } + + // Clamp the cylinder bounds to the grid. if (minx < 0) + { minx = 0; - if (maxx >= chf.width) - maxx = chf.width - 1; - if (minz < 0) - minz = 0; - if (maxz >= chf.height) - maxz = chf.height - 1; + } + if (maxx >= xSize) + { + maxx = xSize - 1; + } + + if (minz < 0) + { + minz = 0; + } + + if (maxz >= zSize) + { + maxz = zSize - 1; + } + + float radiusSq = radius * radius; for (int z = minz; z <= maxz; ++z) { for (int x = minx; x <= maxx; ++x) { - RcCompactCell c = chf.cells[x + z * chf.width]; - for (int i = c.index, ni = c.index + c.count; i < ni; ++i) + RcCompactCell cell = compactHeightfield.cells[x + z * zStride]; + int maxSpanIndex = cell.index + cell.count; + + float cellX = compactHeightfield.bmin[0] + ((float)x + 0.5f) * compactHeightfield.cs; + float cellZ = compactHeightfield.bmin[2] + ((float)z + 0.5f) * compactHeightfield.cs; + float deltaX = cellX - position[0]; + float deltaZ = cellZ - position[2]; + + // Skip this column if it's too far from the center point of the cylinder. + if (RcMath.Sqr(deltaX) + RcMath.Sqr(deltaZ) >= radiusSq) { - RcCompactSpan s = chf.spans[i]; + continue; + } - if (chf.areas[i] == RC_NULL_AREA) - continue; + // Mark all overlapping spans + for (int spanIndex = cell.index; spanIndex < maxSpanIndex; ++spanIndex) + { + RcCompactSpan span = compactHeightfield.spans[spanIndex]; - if (s.y >= miny && s.y <= maxy) + // Skip if span is removed. + if (compactHeightfield.areas[spanIndex] == RC_NULL_AREA) { - float sx = chf.bmin.x + (x + 0.5f) * chf.cs; - float sz = chf.bmin.z + (z + 0.5f) * chf.cs; - float dx = sx - pos[0]; - float dz = sz - pos[2]; + continue; + } - if (dx * dx + dz * dz < r2) - { - chf.areas[i] = areaMod.Apply(chf.areas[i]); - } + // Mark if y extents overlap. + if (span.y >= miny && span.y <= maxy) + { + compactHeightfield.areas[spanIndex] = areaId.Apply(compactHeightfield.areas[spanIndex]); } } } diff --git a/test/DotRecast.Recast.Test/RecastSoloMeshTest.cs b/test/DotRecast.Recast.Test/RecastSoloMeshTest.cs index 8eb50b9..4242b4b 100644 --- a/test/DotRecast.Recast.Test/RecastSoloMeshTest.cs +++ b/test/DotRecast.Recast.Test/RecastSoloMeshTest.cs @@ -110,6 +110,7 @@ public class RecastSoloMeshTest m_agentMaxClimb, m_agentMaxSlope, m_regionMinSize, m_regionMergeSize, m_edgeMaxLen, m_edgeMaxError, m_vertsPerPoly, m_detailSampleDist, m_detailSampleMaxError, SampleAreaModifications.SAMPLE_AREAMOD_GROUND); RecastBuilderConfig bcfg = new RecastBuilderConfig(cfg, bmin, bmax); + // // Step 2. Rasterize input polygon soup. // @@ -125,22 +126,20 @@ public class RecastSoloMeshTest // Allocate array that can hold triangle area types. // If you have multiple meshes you need to process, allocate - // and array which can hold the max number of triangles you need to - // process. + // and array which can hold the max number of triangles you need to process. - // Find triangles which are walkable based on their slope and rasterize - // them. - // If your input data is multiple meshes, you can transform them here, - // calculate + // Find triangles which are walkable based on their slope and rasterize them. + // If your input data is multiple meshes, you can transform them here, calculate // the are type for each of the meshes and rasterize them. - int[] m_triareas = Recast.MarkWalkableTriangles(m_ctx, cfg.walkableSlopeAngle, verts, tris, ntris, - cfg.walkableAreaMod); + int[] m_triareas = Recast.MarkWalkableTriangles(m_ctx, cfg.walkableSlopeAngle, verts, tris, ntris, cfg.walkableAreaMod); RecastRasterization.RasterizeTriangles(m_solid, verts, tris, m_triareas, ntris, cfg.walkableClimb, m_ctx); - // - // Step 3. Filter walkables surfaces. - // - } + } + + // + // Step 3. Filter walkable surfaces. + // + // Once all geometry is rasterized, we do initial pass of filtering to // remove unwanted overhangs caused by the conservative rasterization // as well as filter spans where the character cannot possibly stand.