diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index fa311f055..95eeceb78 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -30,9 +30,10 @@ jobs: - name: Download & install premake working-directory: RecastDemo run: | - curl -L -o premake.tar.gz https://github.com/premake/premake-core/releases/download/v5.0.0-beta2/premake-5.0.0-beta2-macosx.tar.gz + curl -L -o premake.tar.gz https://github.com/premake/premake-core/releases/download/v5.0.0-beta8/premake-5.0.0-beta8-macosx.tar.gz tar -xzf premake.tar.gz rm premake.tar.gz + chmod +x ./premake5 - name: Run premake working-directory: RecastDemo @@ -98,16 +99,17 @@ jobs: - name: Download & Install premake working-directory: RecastDemo run: | - curl -L -o premake.tar.gz https://github.com/premake/premake-core/releases/download/v5.0.0-beta2/premake-5.0.0-beta2-linux.tar.gz + curl -L -o premake.tar.gz https://github.com/premake/premake-core/releases/download/v5.0.0-beta8/premake-5.0.0-beta8-linux.tar.gz tar -xzf premake.tar.gz rm premake.tar.gz + chmod +x ./premake5 - name: Run premake working-directory: RecastDemo - run: ./premake5 --cc=${{matrix.compiler}} gmake2 + run: ./premake5 --cc=${{matrix.compiler}} gmake - name: Build - working-directory: RecastDemo/Build/gmake2 + working-directory: RecastDemo/Build/gmake run: make config=${{matrix.conf}} verbose=true linux-cmake: @@ -140,15 +142,15 @@ jobs: - Debug - Release vs-version: - - vs2019 - vs2022 + - vs2026 include: - - vs-version: vs2019 - version-range: '16.0' - runner: windows-2019 - vs-version: vs2022 version-range: '17.0' runner: windows-2022 + - vs-version: vs2026 + version-range: '18.0' + runner: windows-2025 runs-on: ${{matrix.runner}} @@ -173,7 +175,7 @@ jobs: working-directory: RecastDemo shell: pwsh run: | - (new-object System.Net.WebClient).DownloadFile("https://github.com/premake/premake-core/releases/download/v5.0.0-beta2/premake-5.0.0-beta2-windows.zip","${{github.workspace}}/RecastDemo/premake.zip") + (new-object System.Net.WebClient).DownloadFile("https://github.com/premake/premake-core/releases/download/v5.0.0-beta8/premake-5.0.0-beta8-windows.zip","${{github.workspace}}/RecastDemo/premake.zip") tar -xf premake.zip del premake.zip @@ -192,15 +194,15 @@ jobs: - Debug - Release vs-version: - - vs2019 - vs2022 + - vs2026 include: - - vs-version: vs2019 - cmake-generator: Visual Studio 16 2019 - runner: windows-2019 - vs-version: vs2022 cmake-generator: Visual Studio 17 2022 runner: windows-2022 + - vs-version: vs2026 + cmake-generator: Visual Studio 18 2026 + runner: windows-2025 runs-on: ${{matrix.runner}} diff --git a/.github/workflows/Tests.yaml b/.github/workflows/Tests.yaml index 681c8eabc..8e8dab73a 100644 --- a/.github/workflows/Tests.yaml +++ b/.github/workflows/Tests.yaml @@ -24,9 +24,10 @@ jobs: - name: Download & install premake working-directory: RecastDemo run: | - curl -L -o premake.tar.gz https://github.com/premake/premake-core/releases/download/v5.0.0-beta2/premake-5.0.0-beta2-macosx.tar.gz + curl -L -o premake.tar.gz https://github.com/premake/premake-core/releases/download/v5.0.0-beta8/premake-5.0.0-beta8-macosx.tar.gz tar -xzf premake.tar.gz rm premake.tar.gz + chmod +x ./premake5 - name: Run premake working-directory: RecastDemo @@ -55,16 +56,17 @@ jobs: - name: Download & Install premake working-directory: RecastDemo run: | - curl -L -o premake.tar.gz https://github.com/premake/premake-core/releases/download/v5.0.0-beta2/premake-5.0.0-beta2-linux.tar.gz + curl -L -o premake.tar.gz https://github.com/premake/premake-core/releases/download/v5.0.0-beta8/premake-5.0.0-beta8-linux.tar.gz tar -xzf premake.tar.gz rm premake.tar.gz + chmod +x ./premake5 - name: Run premake working-directory: RecastDemo - run: ./premake5 --cc=clang gmake2 + run: ./premake5 --cc=clang gmake - name: Build - working-directory: RecastDemo/Build/gmake2 + working-directory: RecastDemo/Build/gmake run: make config=debug verbose=true - name: Run Tests @@ -93,7 +95,7 @@ jobs: working-directory: RecastDemo shell: pwsh run: | - (new-object System.Net.WebClient).DownloadFile("https://github.com/premake/premake-core/releases/download/v5.0.0-beta2/premake-5.0.0-beta2-windows.zip","${{github.workspace}}/RecastDemo/premake.zip") + (new-object System.Net.WebClient).DownloadFile("https://github.com/premake/premake-core/releases/download/v5.0.0-beta8/premake-5.0.0-beta8-windows.zip","${{github.workspace}}/RecastDemo/premake.zip") tar -xf premake.zip del premake.zip diff --git a/Recast/Source/RecastFilter.cpp b/Recast/Source/RecastFilter.cpp index 8a022d64f..a55644ddc 100644 --- a/Recast/Source/RecastFilter.cpp +++ b/Recast/Source/RecastFilter.cpp @@ -17,6 +17,7 @@ // #include "Recast.h" +#include "RecastAlloc.h" #include "RecastAssert.h" #include @@ -24,6 +25,14 @@ namespace { const int MAX_HEIGHTFIELD_HEIGHT = 0xffff; // TODO (graham): Move this to a more visible constant and update usages. + + struct FilterSpan + { + rcSpan* source; + unsigned short smin; + unsigned short smax; + unsigned char area; + }; } void rcFilterLowHangingWalkableObstacles(rcContext* context, const int walkableClimb, rcHeightfield& heightfield) @@ -72,22 +81,75 @@ void rcFilterLedgeSpans(rcContext* context, const int walkableHeight, const int const int xSize = heightfield.width; const int zSize = heightfield.height; + const int columnCount = xSize * zSize; + + // Ledge filtering repeatedly scans the four neighbouring columns for every + // span. Flatten the linked lists once so those scans read contiguous data. + rcTempVector columnOffsets(columnCount + 1, 0); + int spanCount = 0; + for (int columnIndex = 0; columnIndex < columnCount; ++columnIndex) + { + columnOffsets[columnIndex] = spanCount; + for (const rcSpan* span = heightfield.spans[columnIndex]; span != NULL; span = span->next) + { + ++spanCount; + } + } + columnOffsets[columnCount] = spanCount; + + rcTempVector spans(spanCount); + int spanIndex = 0; + for (int columnIndex = 0; columnIndex < columnCount; ++columnIndex) + { + for (rcSpan* span = heightfield.spans[columnIndex]; span != NULL; span = span->next) + { + FilterSpan& filterSpan = spans[spanIndex++]; + filterSpan.source = span; + filterSpan.smin = (unsigned short)span->smin; + filterSpan.smax = (unsigned short)span->smax; + filterSpan.area = (unsigned char)span->area; + } + } // 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) + const int columnIndex = x + z * xSize; + const int columnEnd = columnOffsets[columnIndex + 1]; + int neighborColumnStarts[4]; + int neighborColumnEnds[4]; + int firstHigherNeighborCeilings[4]; + for (int direction = 0; direction < 4; ++direction) { + const int neighborX = x + rcGetDirOffsetX(direction); + const int neighborZ = z + rcGetDirOffsetY(direction); + if (neighborX < 0 || neighborZ < 0 || neighborX >= xSize || neighborZ >= zSize) + { + neighborColumnStarts[direction] = -1; + neighborColumnEnds[direction] = -1; + firstHigherNeighborCeilings[direction] = -1; + continue; + } + + const int neighborColumnIndex = neighborX + neighborZ * xSize; + neighborColumnStarts[direction] = columnOffsets[neighborColumnIndex]; + neighborColumnEnds[direction] = columnOffsets[neighborColumnIndex + 1]; + firstHigherNeighborCeilings[direction] = neighborColumnStarts[direction] + 1; + } + for (int currentSpanIndex = columnOffsets[columnIndex]; currentSpanIndex < columnEnd; ++currentSpanIndex) + { + FilterSpan& span = spans[currentSpanIndex]; // Skip non-walkable spans. - if (span->area == RC_NULL_AREA) + if (span.area == RC_NULL_AREA) { continue; } - const int floor = (int)(span->smax); - const int ceiling = span->next ? (int)(span->next->smin) : MAX_HEIGHTFIELD_HEIGHT; + const int floor = (int)(span.smax); + const int ceiling = currentSpanIndex + 1 < columnEnd + ? (int)(spans[currentSpanIndex + 1].smin) : 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 @@ -95,26 +157,25 @@ void rcFilterLedgeSpans(rcContext* context, const int walkableHeight, const int int lowestNeighborFloorDifference = MAX_HEIGHTFIELD_HEIGHT; // Min and max height of accessible neighbours. - int lowestTraversableNeighborFloor = span->smax; - int highestTraversableNeighborFloor = span->smax; + int lowestTraversableNeighborFloor = span.smax; + int highestTraversableNeighborFloor = span.smax; for (int direction = 0; direction < 4; ++direction) { - const int neighborX = x + rcGetDirOffsetX(direction); - const int neighborZ = z + rcGetDirOffsetY(direction); - // Skip neighbours which are out of bounds. - if (neighborX < 0 || neighborZ < 0 || neighborX >= xSize || neighborZ >= zSize) + if (neighborColumnStarts[direction] < 0) { lowestNeighborFloorDifference = -walkableClimb - 1; break; } - const rcSpan* neighborSpan = heightfield.spans[neighborX + neighborZ * xSize]; + int neighborSpanIndex = neighborColumnStarts[direction]; + const int neighborColumnEnd = neighborColumnEnds[direction]; // 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; + int neighborCeiling = neighborSpanIndex < neighborColumnEnd + ? (int)spans[neighborSpanIndex].smin : MAX_HEIGHTFIELD_HEIGHT; // Skip neighbour if the gap between the spans is too small. if (rcMin(ceiling, neighborCeiling) - floor > walkableHeight) @@ -123,11 +184,32 @@ void rcFilterLedgeSpans(rcContext* context, const int walkableHeight, const int break; } + // Spans are sorted by height. Any neighbour whose next ceiling is at or below + // floor + walkableHeight will take the overlap test's continue branch below. + // Seek past that prefix instead of rescanning it for every higher span. + const int minNeighborCeiling = floor + walkableHeight; + if (ceiling <= minNeighborCeiling) + { + continue; + } + int& firstHigherCeiling = firstHigherNeighborCeilings[direction]; + while (firstHigherCeiling < neighborColumnEnd + && (int)spans[firstHigherCeiling].smin <= minNeighborCeiling) + { + ++firstHigherCeiling; + } + neighborSpanIndex = firstHigherCeiling - 1; + // For each span in the neighboring column... - for (; neighborSpan != NULL; neighborSpan = neighborSpan->next) + for (; neighborSpanIndex < neighborColumnEnd; ++neighborSpanIndex) { - const int neighborFloor = (int)neighborSpan->smax; - neighborCeiling = neighborSpan->next ? (int)neighborSpan->next->smin : MAX_HEIGHTFIELD_HEIGHT; + const int neighborFloor = (int)spans[neighborSpanIndex].smax; + if (neighborFloor + walkableHeight >= ceiling) + { + break; + } + neighborCeiling = neighborSpanIndex + 1 < neighborColumnEnd + ? (int)spans[neighborSpanIndex + 1].smin : MAX_HEIGHTFIELD_HEIGHT; // Only consider neighboring areas that have enough overlap to be potentially traversable. if (rcMin(ceiling, neighborCeiling) - rcMax(floor, neighborFloor) <= walkableHeight) @@ -161,12 +243,14 @@ void rcFilterLedgeSpans(rcContext* context, const int walkableHeight, const int // the magnitude of the delta) if (lowestNeighborFloorDifference < -walkableClimb) { - span->area = RC_NULL_AREA; + span.area = RC_NULL_AREA; + span.source->area = RC_NULL_AREA; } // 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; + span.area = RC_NULL_AREA; + span.source->area = RC_NULL_AREA; } } } diff --git a/Recast/Source/RecastRasterization.cpp b/Recast/Source/RecastRasterization.cpp index d54ae7202..d3eb49ce7 100644 --- a/Recast/Source/RecastRasterization.cpp +++ b/Recast/Source/RecastRasterization.cpp @@ -17,6 +17,9 @@ // #include +#if defined(_MSC_VER) +#include +#endif #include "Recast.h" #include "RecastAlloc.h" #include "RecastAssert.h" @@ -188,6 +191,293 @@ static bool addSpan(rcHeightfield& heightfield, return true; } +// A span produced while rasterizing a triangle. Batching spans while the +// heightfield is empty lets each column be merged in contiguous memory and +// emits the final linked lists column-by-column. The per-column input order is +// retained, so area merging has the same semantics as repeated addSpan calls. +struct RasterizedSpan +{ + int columnIndex; + unsigned int span; +}; + +static const unsigned int SPAN_HEIGHT_BITS = 13; +static const unsigned int SPAN_HEIGHT_MASK = (1u << SPAN_HEIGHT_BITS) - 1; + +static unsigned int packRasterizedSpan( + const unsigned short smin, const unsigned short smax, const unsigned char area) +{ + return (unsigned int)smin | ((unsigned int)smax << SPAN_HEIGHT_BITS) + | ((unsigned int)area << (SPAN_HEIGHT_BITS * 2)); +} + +static unsigned short getRasterizedSpanMin(const unsigned int span) +{ + return (unsigned short)(span & SPAN_HEIGHT_MASK); +} + +static unsigned short getRasterizedSpanMax(const unsigned int span) +{ + return (unsigned short)((span >> SPAN_HEIGHT_BITS) & SPAN_HEIGHT_MASK); +} + +static unsigned char getRasterizedSpanArea(const unsigned int span) +{ + return (unsigned char)(span >> (SPAN_HEIGHT_BITS * 2)); +} + +static const int SPAN_WORD_BITS = 64; +static const int SPAN_WORD_COUNT = (SPAN_HEIGHT_MASK + 1) / SPAN_WORD_BITS; +static const int SPAN_WORD_GROUP_COUNT = (SPAN_WORD_COUNT + SPAN_WORD_BITS - 1) / SPAN_WORD_BITS; + +static int findFirstSetBit(const unsigned long long value) +{ +#if defined(_MSC_VER) + unsigned long index; +#if defined(_M_X64) || defined(_M_ARM64) + _BitScanForward64(&index, value); + return (int)index; +#else + if (_BitScanForward(&index, (unsigned long)value)) + { + return (int)index; + } + _BitScanForward(&index, (unsigned long)(value >> 32)); + return (int)index + 32; +#endif +#elif defined(__GNUC__) || defined(__clang__) + return __builtin_ctzll(value); +#else + int index = 0; + unsigned long long remaining = value; + while ((remaining & 1) == 0) + { + remaining >>= 1; + ++index; + } + return index; +#endif +} + +static int findLastSetBit(const unsigned long long value) +{ +#if defined(_MSC_VER) + unsigned long index; +#if defined(_M_X64) || defined(_M_ARM64) + _BitScanReverse64(&index, value); + return (int)index; +#else + if (_BitScanReverse(&index, (unsigned long)(value >> 32))) + { + return (int)index + 32; + } + _BitScanReverse(&index, (unsigned long)value); + return (int)index; +#endif +#elif defined(__GNUC__) || defined(__clang__) + return 63 - __builtin_clzll(value); +#else + int index = 0; + unsigned long long remaining = value; + while (remaining >>= 1) + { + ++index; + } + return index; +#endif +} + +static int findActiveSpanAtOrBefore(const unsigned long long* occupied, + const unsigned long long* occupiedWords, const int height) +{ + const int word = height / SPAN_WORD_BITS; + const int bit = height % SPAN_WORD_BITS; + const unsigned long long throughBit = bit == SPAN_WORD_BITS - 1 + ? ~0ull : (1ull << (bit + 1)) - 1; + const unsigned long long inWord = occupied[word] & throughBit; + if (inWord != 0) + { + return word * SPAN_WORD_BITS + findLastSetBit(inWord); + } + + const int firstGroup = word / SPAN_WORD_BITS; + for (int group = firstGroup; group >= 0; --group) + { + unsigned long long candidates = occupiedWords[group]; + if (group == firstGroup) + { + const int wordInGroup = word % SPAN_WORD_BITS; + candidates &= wordInGroup == 0 ? 0 : (1ull << wordInGroup) - 1; + } + if (candidates == 0) + { + continue; + } + const int candidateWord = group * SPAN_WORD_BITS + findLastSetBit(candidates); + return candidateWord * SPAN_WORD_BITS + findLastSetBit(occupied[candidateWord]); + } + return -1; +} + +static int findActiveSpanAtOrAfter(const unsigned long long* occupied, + const unsigned long long* occupiedWords, const int height) +{ + const int word = height / SPAN_WORD_BITS; + const int bit = height % SPAN_WORD_BITS; + const unsigned long long inWord = occupied[word] & (~0ull << bit); + if (inWord != 0) + { + return word * SPAN_WORD_BITS + findFirstSetBit(inWord); + } + + const int firstGroup = word / SPAN_WORD_BITS; + for (int group = firstGroup; group < SPAN_WORD_GROUP_COUNT; ++group) + { + unsigned long long candidates = occupiedWords[group]; + if (group == firstGroup) + { + const int wordInGroup = word % SPAN_WORD_BITS; + candidates &= wordInGroup == SPAN_WORD_BITS - 1 ? 0 : ~0ull << (wordInGroup + 1); + } + if (candidates == 0) + { + continue; + } + const int candidateWord = group * SPAN_WORD_BITS + findFirstSetBit(candidates); + return candidateWord * SPAN_WORD_BITS + findFirstSetBit(occupied[candidateWord]); + } + return -1; +} + +static int findActiveSpanAfter(const unsigned long long* occupied, + const unsigned long long* occupiedWords, const int height) +{ + return height == (int)SPAN_HEIGHT_MASK ? -1 + : findActiveSpanAtOrAfter(occupied, occupiedWords, height + 1); +} + +static void setActiveSpan(unsigned long long* occupied, unsigned long long* occupiedWords, const int height) +{ + const int word = height / SPAN_WORD_BITS; + occupied[word] |= 1ull << (height % SPAN_WORD_BITS); + occupiedWords[word / SPAN_WORD_BITS] |= 1ull << (word % SPAN_WORD_BITS); +} + +static void clearActiveSpan(unsigned long long* occupied, unsigned long long* occupiedWords, const int height) +{ + const int word = height / SPAN_WORD_BITS; + occupied[word] &= ~(1ull << (height % SPAN_WORD_BITS)); + if (occupied[word] == 0) + { + occupiedWords[word / SPAN_WORD_BITS] &= ~(1ull << (word % SPAN_WORD_BITS)); + } +} + +static void mergeRasterizedSpan(unsigned int* spans, unsigned long long* occupied, + unsigned long long* occupiedWords, unsigned short spanMin, unsigned short spanMax, + unsigned char spanArea, const int flagMergeThreshold) +{ + int current = findActiveSpanAtOrBefore(occupied, occupiedWords, spanMin); + if (current == -1 || getRasterizedSpanMax(spans[current]) < spanMin) + { + current = findActiveSpanAtOrAfter(occupied, occupiedWords, spanMin); + } + + while (current != -1 && current <= spanMax) + { + const unsigned int currentSpan = spans[current]; + const unsigned short currentMin = getRasterizedSpanMin(currentSpan); + const unsigned short currentMax = getRasterizedSpanMax(currentSpan); + const int next = findActiveSpanAfter(occupied, occupiedWords, current); + spanMin = rcMin(spanMin, currentMin); + spanMax = rcMax(spanMax, currentMax); + if (rcAbs((int)spanMax - (int)currentMax) <= flagMergeThreshold) + { + spanArea = rcMax(spanArea, getRasterizedSpanArea(currentSpan)); + } + clearActiveSpan(occupied, occupiedWords, current); + current = next; + } + + spans[spanMin] = packRasterizedSpan(spanMin, spanMax, spanArea); + setActiveSpan(occupied, occupiedWords, spanMin); +} + +static bool addRasterizedSpans( + rcHeightfield& heightfield, rcTempVector& rasterized, const int flagMergeThreshold) +{ + const int columnCount = heightfield.width * heightfield.height; + rcTempVector offsets(columnCount + 1, 0); + for (int i = 0; i < rasterized.size(); ++i) + { + ++offsets[rasterized[i].columnIndex + 1]; + } + for (int i = 1; i <= columnCount; ++i) + { + offsets[i] += offsets[i - 1]; + } + + rcTempVector next(offsets); + rcTempVector ordered(rasterized.size()); + for (int i = 0; i < rasterized.size(); ++i) + { + const RasterizedSpan& span = rasterized[i]; + ordered[next[span.columnIndex]++] = span.span; + } + + rcTempVector merged(SPAN_HEIGHT_MASK + 1); + unsigned long long occupied[SPAN_WORD_COUNT] = {}; + unsigned long long occupiedWords[SPAN_WORD_GROUP_COUNT] = {}; + for (int columnIndex = 0; columnIndex < columnCount; ++columnIndex) + { + for (int i = offsets[columnIndex]; i < offsets[columnIndex + 1]; ++i) + { + const unsigned int span = ordered[i]; + mergeRasterizedSpan(merged.data(), occupied, occupiedWords, getRasterizedSpanMin(span), + getRasterizedSpanMax(span), getRasterizedSpanArea(span), flagMergeThreshold); + } + + rcSpan* previous = NULL; + for (int group = 0; group < SPAN_WORD_GROUP_COUNT; ++group) + { + unsigned long long activeWords = occupiedWords[group]; + while (activeWords != 0) + { + const int word = group * SPAN_WORD_BITS + findFirstSetBit(activeWords); + unsigned long long activeSpans = occupied[word]; + while (activeSpans != 0) + { + const int active = word * SPAN_WORD_BITS + findFirstSetBit(activeSpans); + const unsigned int mergedSpan = merged[active]; + rcSpan* span = allocSpan(heightfield); + if (span == NULL) + { + return false; + } + span->smin = getRasterizedSpanMin(mergedSpan); + span->smax = getRasterizedSpanMax(mergedSpan); + span->area = getRasterizedSpanArea(mergedSpan); + span->next = NULL; + if (previous == NULL) + { + heightfield.spans[columnIndex] = span; + } + else + { + previous->next = span; + } + previous = span; + activeSpans &= activeSpans - 1; + } + occupied[word] = 0; + activeWords &= activeWords - 1; + } + occupiedWords[group] = 0; + } + } + return true; +} + bool rcAddSpan(rcContext* context, rcHeightfield& heightfield, const int x, const int z, const unsigned short spanMin, const unsigned short spanMax, @@ -204,87 +494,221 @@ bool rcAddSpan(rcContext* context, rcHeightfield& heightfield, return true; } -enum rcAxis +static void includeRowX(const float x, const int count, float& minX, float& maxX) { - RC_AXIS_X = 0, - RC_AXIS_Y = 1, - RC_AXIS_Z = 2 -}; + if (count == 0) + { + minX = x; + maxX = x; + } + else + { + if (minX > x) + { + minX = x; + } + if (maxX < x) + { + maxX = x; + } + } +} -/// Divides a convex polygon of max 12 vertices into two convex polygons -/// across a separating axis. +/// Divides a convex polygon of max 12 vertices into a grid row and the +/// remainder above it. /// /// @param[in] inVerts The input polygon vertices /// @param[in] inVertsCount The number of input polygon vertices -/// @param[out] outVerts1 Resulting polygon 1's vertices -/// @param[out] outVerts1Count The number of resulting polygon 1 vertices -/// @param[out] outVerts2 Resulting polygon 2's vertices -/// @param[out] outVerts2Count The number of resulting polygon 2 vertices -/// @param[in] axisOffset THe offset along the specified axis -/// @param[in] axis The separating axis -static void dividePoly(const float* inVerts, int inVertsCount, - float* outVerts1, int* outVerts1Count, - float* outVerts2, int* outVerts2Count, - float axisOffset, rcAxis axis) +/// @param[out] outRow Resulting row polygon vertices +/// @param[out] outRowCount The number of resulting row polygon vertices +/// @param[out] outRemainder Resulting remainder polygon vertices +/// @param[out] outRemainderCount The number of resulting remainder polygon vertices +/// @param[in] axisOffset The row's upper Z bound +/// @param[out] minX Minimum X extent of the row polygon +/// @param[out] maxX Maximum X extent of the row polygon +static void dividePolyRow(const float* inVerts, int inVertsCount, + float* outRow, int* outRowCount, + float* outRemainder, int* outRemainderCount, + float axisOffset, float& minX, float& maxX) { rcAssert(inVertsCount <= 12); - - // How far positive or negative away from the separating axis is each vertex. - float inVertAxisDelta[12]; - for (int inVert = 0; inVert < inVertsCount; ++inVert) + if (inVertsCount == 0) { - inVertAxisDelta[inVert] = axisOffset - inVerts[inVert * 3 + axis]; + *outRowCount = 0; + *outRemainderCount = 0; + return; } - int poly1Vert = 0; - int poly2Vert = 0; + int rowVert = 0; + int remainderVert = 0; + float inVertBDelta = axisOffset - inVerts[(inVertsCount - 1) * 3 + 2]; for (int inVertA = 0, inVertB = inVertsCount - 1; inVertA < inVertsCount; inVertB = inVertA, ++inVertA) { + const float inVertADelta = axisOffset - inVerts[inVertA * 3 + 2]; // If the two vertices are on the same side of the separating axis - bool sameSide = (inVertAxisDelta[inVertA] >= 0) == (inVertAxisDelta[inVertB] >= 0); + bool sameSide = (inVertADelta >= 0) == (inVertBDelta >= 0); if (!sameSide) { - float s = inVertAxisDelta[inVertB] / (inVertAxisDelta[inVertB] - inVertAxisDelta[inVertA]); - outVerts1[poly1Vert * 3 + 0] = inVerts[inVertB * 3 + 0] + (inVerts[inVertA * 3 + 0] - inVerts[inVertB * 3 + 0]) * s; - outVerts1[poly1Vert * 3 + 1] = inVerts[inVertB * 3 + 1] + (inVerts[inVertA * 3 + 1] - inVerts[inVertB * 3 + 1]) * s; - outVerts1[poly1Vert * 3 + 2] = inVerts[inVertB * 3 + 2] + (inVerts[inVertA * 3 + 2] - inVerts[inVertB * 3 + 2]) * s; - rcVcopy(&outVerts2[poly2Vert * 3], &outVerts1[poly1Vert * 3]); - poly1Vert++; - poly2Vert++; + float s = inVertBDelta / (inVertBDelta - inVertADelta); + outRow[rowVert * 3 + 0] = inVerts[inVertB * 3 + 0] + (inVerts[inVertA * 3 + 0] - inVerts[inVertB * 3 + 0]) * s; + outRow[rowVert * 3 + 1] = inVerts[inVertB * 3 + 1] + (inVerts[inVertA * 3 + 1] - inVerts[inVertB * 3 + 1]) * s; + outRow[rowVert * 3 + 2] = inVerts[inVertB * 3 + 2] + (inVerts[inVertA * 3 + 2] - inVerts[inVertB * 3 + 2]) * s; + includeRowX(outRow[rowVert * 3], rowVert, minX, maxX); + rcVcopy(&outRemainder[remainderVert * 3], &outRow[rowVert * 3]); + rowVert++; + remainderVert++; // add the inVertA point to the right polygon. Do NOT add points that are on the dividing line // since these were already added above - if (inVertAxisDelta[inVertA] > 0) + if (inVertADelta > 0) { - rcVcopy(&outVerts1[poly1Vert * 3], &inVerts[inVertA * 3]); - poly1Vert++; + rcVcopy(&outRow[rowVert * 3], &inVerts[inVertA * 3]); + includeRowX(outRow[rowVert * 3], rowVert, minX, maxX); + rowVert++; } - else if (inVertAxisDelta[inVertA] < 0) + else if (inVertADelta < 0) { - rcVcopy(&outVerts2[poly2Vert * 3], &inVerts[inVertA * 3]); - poly2Vert++; + rcVcopy(&outRemainder[remainderVert * 3], &inVerts[inVertA * 3]); + remainderVert++; } } else { // add the inVertA point to the right polygon. Addition is done even for points on the dividing line - if (inVertAxisDelta[inVertA] >= 0) + if (inVertADelta >= 0) + { + rcVcopy(&outRow[rowVert * 3], &inVerts[inVertA * 3]); + includeRowX(outRow[rowVert * 3], rowVert, minX, maxX); + rowVert++; + if (inVertADelta != 0) + { + inVertBDelta = inVertADelta; + continue; + } + } + rcVcopy(&outRemainder[remainderVert * 3], &inVerts[inVertA * 3]); + remainderVert++; + } + inVertBDelta = inVertADelta; + } + + *outRowCount = rowVert; + *outRemainderCount = remainderVert; +} + +static void includeSpanHeight(const float height, int& count, float& spanMin, float& spanMax) +{ + if (count == 0) + { + spanMin = height; + spanMax = height; + } + else + { + spanMin = rcMin(spanMin, height); + spanMax = rcMax(spanMax, height); + } + ++count; +} + +// The positive output of the X-axis clip is consumed only through its vertex +// count and Y extents. Preserve the original clipping order and remainder +// polygon, but reduce those heights as they are produced instead of writing +// and rereading a full temporary polygon. +static void dividePolyCell(const float* inVerts, const int inVertsCount, + int* outVertsCount, float& spanMin, float& spanMax, + float* outRemainder, int* outRemainderCount, const float axisOffset) +{ + rcAssert(inVertsCount <= 12); + if (inVertsCount == 0) + { + *outVertsCount = 0; + *outRemainderCount = 0; + return; + } + + int cellVert = 0; + int remainderVert = 0; + float inVertBDelta = axisOffset - inVerts[(inVertsCount - 1) * 3]; + for (int inVertA = 0, inVertB = inVertsCount - 1; + inVertA < inVertsCount; inVertB = inVertA, ++inVertA) + { + const float inVertADelta = axisOffset - inVerts[inVertA * 3]; + const bool sameSide = (inVertADelta >= 0) == (inVertBDelta >= 0); + if (!sameSide) + { + const float s = inVertBDelta / (inVertBDelta - inVertADelta); + outRemainder[remainderVert * 3 + 0] = inVerts[inVertB * 3 + 0] + + (inVerts[inVertA * 3 + 0] - inVerts[inVertB * 3 + 0]) * s; + outRemainder[remainderVert * 3 + 1] = inVerts[inVertB * 3 + 1] + + (inVerts[inVertA * 3 + 1] - inVerts[inVertB * 3 + 1]) * s; + outRemainder[remainderVert * 3 + 2] = inVerts[inVertB * 3 + 2] + + (inVerts[inVertA * 3 + 2] - inVerts[inVertB * 3 + 2]) * s; + includeSpanHeight(outRemainder[remainderVert * 3 + 1], cellVert, spanMin, spanMax); + ++remainderVert; + + if (inVertADelta > 0) + { + includeSpanHeight(inVerts[inVertA * 3 + 1], cellVert, spanMin, spanMax); + } + else if (inVertADelta < 0) { - rcVcopy(&outVerts1[poly1Vert * 3], &inVerts[inVertA * 3]); - poly1Vert++; - if (inVertAxisDelta[inVertA] != 0) + rcVcopy(&outRemainder[remainderVert * 3], &inVerts[inVertA * 3]); + ++remainderVert; + } + } + else + { + if (inVertADelta >= 0) + { + includeSpanHeight(inVerts[inVertA * 3 + 1], cellVert, spanMin, spanMax); + if (inVertADelta != 0) { + inVertBDelta = inVertADelta; continue; } } - rcVcopy(&outVerts2[poly2Vert * 3], &inVerts[inVertA * 3]); - poly2Vert++; + rcVcopy(&outRemainder[remainderVert * 3], &inVerts[inVertA * 3]); + ++remainderVert; } + inVertBDelta = inVertADelta; } - *outVerts1Count = poly1Vert; - *outVerts2Count = poly2Vert; + *outVertsCount = cellVert; + *outRemainderCount = remainderVert; +} + +static bool addRasterizedCellSpan(rcHeightfield& heightfield, const int x, const int z, + float spanMin, float spanMax, const float heightfieldMin, const float heightfieldHeight, + const float inverseCellHeight, const unsigned char areaID, const int flagMergeThreshold, + rcTempVector* rasterizedSpans) +{ + spanMin -= heightfieldMin; + spanMax -= heightfieldMin; + + if (spanMax < 0.0f || spanMin > heightfieldHeight) + { + return true; + } + + spanMin = rcMax(spanMin, 0.0f); + spanMax = rcMin(spanMax, heightfieldHeight); + const unsigned short spanMinCellIndex = (unsigned short)rcClamp( + (int)floorf(spanMin * inverseCellHeight), 0, RC_SPAN_MAX_HEIGHT); + const unsigned short spanMaxCellIndex = (unsigned short)rcClamp( + (int)ceilf(spanMax * inverseCellHeight), (int)spanMinCellIndex + 1, RC_SPAN_MAX_HEIGHT); + + if (rasterizedSpans != NULL) + { + RasterizedSpan rasterizedSpan; + rasterizedSpan.columnIndex = x + z * heightfield.width; + rasterizedSpan.span = packRasterizedSpan( + spanMinCellIndex, spanMaxCellIndex, (unsigned char)(areaID & 0x3f)); + rasterizedSpans->push_back(rasterizedSpan); + return true; + } + return addSpan(heightfield, x, z, spanMinCellIndex, spanMaxCellIndex, areaID, flagMergeThreshold); } /// Rasterize a single triangle to the heightfield. @@ -307,7 +731,7 @@ static bool rasterizeTri(const float* v0, const float* v1, const float* v2, const unsigned char areaID, rcHeightfield& heightfield, const float* heightfieldBBMin, const float* heightfieldBBMax, const float cellSize, const float inverseCellSize, const float inverseCellHeight, - const int flagMergeThreshold) + const int flagMergeThreshold, rcTempVector* rasterizedSpans) { // Calculate the bounding box of the triangle. float triBBMin[3]; @@ -338,6 +762,20 @@ static bool rasterizeTri(const float* v0, const float* v1, const float* v2, z0 = rcClamp(z0, -1, h - 1); z1 = rcClamp(z1, 0, h - 1); + const int triangleX0 = (int)((triBBMin[0] - heightfieldBBMin[0]) * inverseCellSize); + const int triangleX1 = (int)((triBBMax[0] - heightfieldBBMin[0]) * inverseCellSize); + if (z0 == z1 && z0 >= 0 && triangleX0 == triangleX1 && triangleX0 >= 0 && triangleX0 < w) + { + const float cellX = heightfieldBBMin[0] + (float)triangleX0 * cellSize; + const float cellZ = heightfieldBBMin[2] + (float)z0 * cellSize; + if (triBBMin[0] >= cellX && triBBMax[0] <= cellX + cellSize + && triBBMin[2] >= cellZ && triBBMax[2] <= cellZ + cellSize) + { + return addRasterizedCellSpan(heightfield, triangleX0, z0, triBBMin[1], triBBMax[1], + heightfieldBBMin[1], by, inverseCellHeight, areaID, flagMergeThreshold, rasterizedSpans); + } + } + // Clip the triangle into all grid cells it touches. float buf[7 * 3 * 4]; float* in = buf; @@ -355,8 +793,24 @@ static bool rasterizeTri(const float* v0, const float* v1, const float* v2, { // Clip polygon to row. Store the remaining polygon as well const float cellZ = heightfieldBBMin[2] + (float)z * cellSize; - dividePoly(in, nvIn, inRow, &nvRow, p1, &nvIn, cellZ + cellSize, RC_AXIS_Z); - rcSwap(in, p1); + float minX = 0.0f; + float maxX = 0.0f; + if (z == z1 && triBBMax[2] <= cellZ + cellSize) + { + // The residual polygon is already wholly inside the final row. + // Consume it directly; there is no next row that needs a remainder. + inRow = in; + nvRow = nvIn; + for (int vert = 0; vert < nvRow; ++vert) + { + includeRowX(inRow[vert * 3], vert, minX, maxX); + } + } + else + { + dividePolyRow(in, nvIn, inRow, &nvRow, p1, &nvIn, cellZ + cellSize, minX, maxX); + rcSwap(in, p1); + } if (nvRow < 3) { @@ -367,20 +821,6 @@ static bool rasterizeTri(const float* v0, const float* v1, const float* v2, continue; } - // find X-axis bounds of the row - float minX = inRow[0]; - float maxX = inRow[0]; - for (int vert = 1; vert < nvRow; ++vert) - { - if (minX > inRow[vert * 3]) - { - minX = inRow[vert * 3]; - } - if (maxX < inRow[vert * 3]) - { - maxX = inRow[vert * 3]; - } - } int x0 = (int)((minX - heightfieldBBMin[0]) * inverseCellSize); int x1 = (int)((maxX - heightfieldBBMin[0]) * inverseCellSize); if (x1 < 0 || x0 >= w) @@ -397,54 +837,41 @@ static bool rasterizeTri(const float* v0, const float* v1, const float* v2, { // Clip polygon to column. store the remaining polygon as well const float cx = heightfieldBBMin[0] + (float)x * cellSize; - dividePoly(inRow, nv2, p1, &nv, p2, &nv2, cx + cellSize, RC_AXIS_X); - rcSwap(inRow, p2); - - if (nv < 3) + float spanMin = 0.0f; + float spanMax = 0.0f; + if (x == x1 && maxX <= cx + cellSize) { - continue; - } - if (x < 0) - { - continue; + // The residual polygon is already wholly inside the final cell. + // Reduce it directly; there is no next cell that needs a remainder. + nv = nv2; + if (nv >= 3) + { + spanMin = inRow[1]; + spanMax = inRow[1]; + for (int vert = 1; vert < nv; ++vert) + { + spanMin = rcMin(spanMin, inRow[vert * 3 + 1]); + spanMax = rcMax(spanMax, inRow[vert * 3 + 1]); + } + } } - - // Calculate min and max of the span. - float spanMin = p1[1]; - float spanMax = p1[1]; - for (int vert = 1; vert < nv; ++vert) + else { - spanMin = rcMin(spanMin, p1[vert * 3 + 1]); - spanMax = rcMax(spanMax, p1[vert * 3 + 1]); + dividePolyCell(inRow, nv2, &nv, spanMin, spanMax, p2, &nv2, cx + cellSize); + rcSwap(inRow, p2); } - spanMin -= heightfieldBBMin[1]; - spanMax -= heightfieldBBMin[1]; - // Skip the span if it's completely outside the heightfield bounding box - if (spanMax < 0.0f) + if (nv < 3) { continue; } - if (spanMin > by) + if (x < 0) { continue; } - // Clamp the span to the heightfield bounding box. - if (spanMin < 0.0f) - { - spanMin = 0; - } - if (spanMax > by) - { - spanMax = by; - } - - // Snap the span to the heightfield height grid. - unsigned short spanMinCellIndex = (unsigned short)rcClamp((int)floorf(spanMin * inverseCellHeight), 0, RC_SPAN_MAX_HEIGHT); - unsigned short spanMaxCellIndex = (unsigned short)rcClamp((int)ceilf(spanMax * inverseCellHeight), (int)spanMinCellIndex + 1, RC_SPAN_MAX_HEIGHT); - - if (!addSpan(heightfield, x, z, spanMinCellIndex, spanMaxCellIndex, areaID, flagMergeThreshold)) + if (!addRasterizedCellSpan(heightfield, x, z, spanMin, spanMax, heightfieldBBMin[1], by, + inverseCellHeight, areaID, flagMergeThreshold, rasterizedSpans)) { return false; } @@ -465,7 +892,7 @@ bool rcRasterizeTriangle(rcContext* context, // Rasterize the single triangle. const float inverseCellSize = 1.0f / heightfield.cs; const float inverseCellHeight = 1.0f / heightfield.ch; - if (!rasterizeTri(v0, v1, v2, areaID, heightfield, heightfield.bmin, heightfield.bmax, heightfield.cs, inverseCellSize, inverseCellHeight, flagMergeThreshold)) + if (!rasterizeTri(v0, v1, v2, areaID, heightfield, heightfield.bmin, heightfield.bmax, heightfield.cs, inverseCellSize, inverseCellHeight, flagMergeThreshold, NULL)) { context->log(RC_LOG_ERROR, "rcRasterizeTriangle: Out of memory."); return false; @@ -486,17 +913,24 @@ bool rcRasterizeTriangles(rcContext* context, // Rasterize the triangles. const float inverseCellSize = 1.0f / heightfield.cs; const float inverseCellHeight = 1.0f / heightfield.ch; + const bool batchSpans = heightfield.pools == NULL && numTris >= 64; + rcTempVector rasterizedSpans; for (int triIndex = 0; triIndex < numTris; ++triIndex) { const float* v0 = &verts[tris[triIndex * 3 + 0] * 3]; const float* v1 = &verts[tris[triIndex * 3 + 1] * 3]; const float* v2 = &verts[tris[triIndex * 3 + 2] * 3]; - if (!rasterizeTri(v0, v1, v2, triAreaIDs[triIndex], heightfield, heightfield.bmin, heightfield.bmax, heightfield.cs, inverseCellSize, inverseCellHeight, flagMergeThreshold)) + if (!rasterizeTri(v0, v1, v2, triAreaIDs[triIndex], heightfield, heightfield.bmin, heightfield.bmax, heightfield.cs, inverseCellSize, inverseCellHeight, flagMergeThreshold, batchSpans ? &rasterizedSpans : NULL)) { context->log(RC_LOG_ERROR, "rcRasterizeTriangles: Out of memory."); return false; } } + if (batchSpans && !addRasterizedSpans(heightfield, rasterizedSpans, flagMergeThreshold)) + { + context->log(RC_LOG_ERROR, "rcRasterizeTriangles: Out of memory."); + return false; + } return true; } @@ -513,17 +947,24 @@ bool rcRasterizeTriangles(rcContext* context, // Rasterize the triangles. const float inverseCellSize = 1.0f / heightfield.cs; const float inverseCellHeight = 1.0f / heightfield.ch; + const bool batchSpans = heightfield.pools == NULL && numTris >= 64; + rcTempVector rasterizedSpans; for (int triIndex = 0; triIndex < numTris; ++triIndex) { const float* v0 = &verts[tris[triIndex * 3 + 0] * 3]; const float* v1 = &verts[tris[triIndex * 3 + 1] * 3]; const float* v2 = &verts[tris[triIndex * 3 + 2] * 3]; - if (!rasterizeTri(v0, v1, v2, triAreaIDs[triIndex], heightfield, heightfield.bmin, heightfield.bmax, heightfield.cs, inverseCellSize, inverseCellHeight, flagMergeThreshold)) + if (!rasterizeTri(v0, v1, v2, triAreaIDs[triIndex], heightfield, heightfield.bmin, heightfield.bmax, heightfield.cs, inverseCellSize, inverseCellHeight, flagMergeThreshold, batchSpans ? &rasterizedSpans : NULL)) { context->log(RC_LOG_ERROR, "rcRasterizeTriangles: Out of memory."); return false; } } + if (batchSpans && !addRasterizedSpans(heightfield, rasterizedSpans, flagMergeThreshold)) + { + context->log(RC_LOG_ERROR, "rcRasterizeTriangles: Out of memory."); + return false; + } return true; } @@ -539,17 +980,24 @@ bool rcRasterizeTriangles(rcContext* context, // Rasterize the triangles. const float inverseCellSize = 1.0f / heightfield.cs; const float inverseCellHeight = 1.0f / heightfield.ch; + const bool batchSpans = heightfield.pools == NULL && numTris >= 64; + rcTempVector rasterizedSpans; for (int triIndex = 0; triIndex < numTris; ++triIndex) { const float* v0 = &verts[(triIndex * 3 + 0) * 3]; const float* v1 = &verts[(triIndex * 3 + 1) * 3]; const float* v2 = &verts[(triIndex * 3 + 2) * 3]; - if (!rasterizeTri(v0, v1, v2, triAreaIDs[triIndex], heightfield, heightfield.bmin, heightfield.bmax, heightfield.cs, inverseCellSize, inverseCellHeight, flagMergeThreshold)) + if (!rasterizeTri(v0, v1, v2, triAreaIDs[triIndex], heightfield, heightfield.bmin, heightfield.bmax, heightfield.cs, inverseCellSize, inverseCellHeight, flagMergeThreshold, batchSpans ? &rasterizedSpans : NULL)) { context->log(RC_LOG_ERROR, "rcRasterizeTriangles: Out of memory."); return false; } } + if (batchSpans && !addRasterizedSpans(heightfield, rasterizedSpans, flagMergeThreshold)) + { + context->log(RC_LOG_ERROR, "rcRasterizeTriangles: Out of memory."); + return false; + } return true; } diff --git a/RecastDemo/premake5.lua b/RecastDemo/premake5.lua index f02398ed0..6f9594cb6 100644 --- a/RecastDemo/premake5.lua +++ b/RecastDemo/premake5.lua @@ -24,9 +24,13 @@ workspace "recastnavigation" exceptionhandling "Off" rtti "Off" symbols "On" - flags { "FatalCompileWarnings" } + fatalwarnings { "All" } cppdialect "C++98" + -- Xcode never treated warnings as errors here; keep it that way. + filter "action:xcode4" + removefatalwarnings { "All" } + -- debug configs filter "configurations:Debug" defines { "DEBUG" } diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index c37a6c9c3..fd9d280d6 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -3,10 +3,12 @@ include_directories(../Recast/Include) add_executable(Tests Detour/Tests_Detour.cpp + Recast/Bench_RecastHotPaths.cpp Recast/Bench_rcVector.cpp Recast/Tests_Alloc.cpp Recast/Tests_Recast.cpp Recast/Tests_RecastFilter.cpp + Recast/Tests_RecastRasterization.cpp DetourCrowd/Tests_DetourPathCorridor.cpp ) diff --git a/Tests/Recast/Bench_RecastHotPaths.cpp b/Tests/Recast/Bench_RecastHotPaths.cpp new file mode 100644 index 000000000..33d895c0c --- /dev/null +++ b/Tests/Recast/Bench_RecastHotPaths.cpp @@ -0,0 +1,155 @@ +#include +#include + +#include "catch2/catch_all.hpp" + +#include "Recast.h" + +namespace +{ + struct SpanSpec + { + int x; + int z; + unsigned short min; + unsigned short max; + }; + + std::uint32_t nextRandom(std::uint32_t& state) + { + state = state * 1664525u + 1013904223u; + return state; + } + + float randomFloat(std::uint32_t& state, const float min, const float max) + { + const float unit = (float)(nextRandom(state) >> 8) * (1.0f / 16777216.0f); + return min + unit * (max - min); + } + + std::vector makeOverlappingTriangles(const int triangleCount) + { + std::uint32_t random = 0x5eed1234u; + std::vector result((std::size_t)triangleCount * 9); + for (int triangle = 0; triangle < triangleCount; ++triangle) + { + for (int vertex = 0; vertex < 3; ++vertex) + { + const std::size_t offset = (std::size_t)(triangle * 3 + vertex) * 3; + result[offset] = randomFloat(random, -4.0f, 68.0f); + result[offset + 1] = randomFloat(random, -2.0f, 66.0f); + result[offset + 2] = randomFloat(random, -4.0f, 68.0f); + } + } + return result; + } + + std::vector makeSingleCellTriangles(const int triangleCount) + { + std::uint32_t random = 0x12345eedu; + std::vector result((std::size_t)triangleCount * 9); + for (int triangle = 0; triangle < triangleCount; ++triangle) + { + const int x = (int)(nextRandom(random) % 64); + const int z = (int)(nextRandom(random) % 64); + for (int vertex = 0; vertex < 3; ++vertex) + { + const std::size_t offset = (std::size_t)(triangle * 3 + vertex) * 3; + result[offset] = (float)x + randomFloat(random, 0.05f, 0.95f); + result[offset + 1] = randomFloat(random, 0.0f, 64.0f); + result[offset + 2] = (float)z + randomFloat(random, 0.05f, 0.95f); + } + } + return result; + } + + std::vector makeTallColumns(const int width, const int height, const int spansPerColumn) + { + std::vector result; + result.reserve((std::size_t)width * height * spansPerColumn); + for (int z = 0; z < height; ++z) + { + for (int x = 0; x < width; ++x) + { + unsigned short min = (unsigned short)((x + z) % 3); + for (int i = 0; i < spansPerColumn; ++i) + { + const unsigned short max = (unsigned short)(min + 1 + (x + z + i) % 2); + result.push_back(SpanSpec{ x, z, min, max }); + min = (unsigned short)(max + 2 + (x * 3 + z + i) % 4); + } + } + } + return result; + } + + unsigned int spanChecksum(const rcHeightfield& heightfield) + { + unsigned int result = 0; + for (int column = 0; column < heightfield.width * heightfield.height; ++column) + { + for (const rcSpan* span = heightfield.spans[column]; span != NULL; span = span->next) + result = result * 33u + span->smin + span->smax + span->area; + } + return result; + } +} + +TEST_CASE("Recast hot path benchmarks", "[.benchmark][recast]") +{ + const float boundsMin[3] = { 0.0f, 0.0f, 0.0f }; + const float boundsMax[3] = { 64.0f, 64.0f, 64.0f }; + const std::vector overlappingAreas(1024, RC_WALKABLE_AREA); + const std::vector overlapping = makeOverlappingTriangles((int)overlappingAreas.size()); + const std::vector singleCellAreas(4096, RC_WALKABLE_AREA); + const std::vector singleCell = makeSingleCellTriangles((int)singleCellAreas.size()); + + BENCHMARK("rasterize 1024 overlapping triangles") + { + rcContext context(false); + rcHeightfield heightfield; + if (!rcCreateHeightfield( + &context, heightfield, 64, 64, boundsMin, boundsMax, 1.0f, 0.25f)) + return 0u; + const bool result = rcRasterizeTriangles(&context, overlapping.data(), overlappingAreas.data(), + (int)overlappingAreas.size(), heightfield, 2); + const unsigned int checksum = result ? spanChecksum(heightfield) : 0u; + return checksum; + }; + + BENCHMARK("rasterize 4096 single-cell triangles") + { + rcContext context(false); + rcHeightfield heightfield; + if (!rcCreateHeightfield( + &context, heightfield, 64, 64, boundsMin, boundsMax, 1.0f, 0.25f)) + return 0u; + const bool result = rcRasterizeTriangles(&context, singleCell.data(), singleCellAreas.data(), + (int)singleCellAreas.size(), heightfield, 2); + const unsigned int checksum = result ? spanChecksum(heightfield) : 0u; + return checksum; + }; + + const int filterWidth = 32; + const int filterHeight = 32; + const float filterBoundsMax[3] = { (float)filterWidth, 512.0f, (float)filterHeight }; + const std::vector tallColumns = makeTallColumns(filterWidth, filterHeight, 24); + BENCHMARK("filter 32x32 columns with 24 spans") + { + rcContext context(false); + rcHeightfield heightfield; + if (!rcCreateHeightfield(&context, heightfield, filterWidth, filterHeight, + boundsMin, filterBoundsMax, 1.0f, 1.0f)) + return 0u; + for (const SpanSpec& span : tallColumns) + { + if (!rcAddSpan(&context, heightfield, span.x, span.z, span.min, span.max, RC_WALKABLE_AREA, 0)) + { + return 0u; + } + } + rcFilterLedgeSpans(&context, 4, 2, heightfield); + const unsigned int checksum = spanChecksum(heightfield); + return checksum; + }; +} diff --git a/Tests/Recast/Tests_RecastFilter.cpp b/Tests/Recast/Tests_RecastFilter.cpp index 13c0b49a4..ce1b8d49d 100644 --- a/Tests/Recast/Tests_RecastFilter.cpp +++ b/Tests/Recast/Tests_RecastFilter.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include "catch2/catch_all.hpp" @@ -12,7 +14,15 @@ namespace void freeHeightfieldSpans(const rcHeightfield& heightfield) { for (int i = 0; i < heightfield.height * heightfield.width; ++i) - rcFree(heightfield.spans[i]); + { + rcSpan* span = heightfield.spans[i]; + while (span != NULL) + { + rcSpan* next = span->next; + rcFree(span); + span = next; + } + } } std::vector getAreas(const rcHeightfield& heightfield) @@ -23,6 +33,106 @@ namespace result[i] = span->area; return result; } + + using SpanValue = std::array; + + std::vector getSpans(const rcHeightfield& heightfield) + { + std::vector result; + for (int column = 0; column < heightfield.height * heightfield.width; ++column) + { + for (const rcSpan* span = heightfield.spans[column]; span != NULL; span = span->next) + { + result.push_back(SpanValue{ (unsigned int)column, (unsigned int)span->smin, + (unsigned int)span->smax, (unsigned int)span->area }); + } + } + return result; + } + + // Keep the pre-optimization linked-list scan as a test oracle. + void filterLedgeSpansReference( + const int walkableHeight, const int walkableClimb, rcHeightfield& heightfield) + { + const int maxHeight = 0xffff; + for (int z = 0; z < heightfield.height; ++z) + { + for (int x = 0; x < heightfield.width; ++x) + { + for (rcSpan* span = heightfield.spans[x + z * heightfield.width]; span; span = span->next) + { + if (span->area == RC_NULL_AREA) + continue; + + const int floor = (int)span->smax; + const int ceiling = span->next ? (int)span->next->smin : maxHeight; + int lowestNeighborFloorDifference = maxHeight; + int lowestTraversableNeighborFloor = span->smax; + int highestTraversableNeighborFloor = span->smax; + + for (int direction = 0; direction < 4; ++direction) + { + const int neighborX = x + rcGetDirOffsetX(direction); + const int neighborZ = z + rcGetDirOffsetY(direction); + if (neighborX < 0 || neighborZ < 0 + || neighborX >= heightfield.width || neighborZ >= heightfield.height) + { + lowestNeighborFloorDifference = -walkableClimb - 1; + break; + } + + const rcSpan* neighborSpan + = heightfield.spans[neighborX + neighborZ * heightfield.width]; + int neighborCeiling = neighborSpan ? (int)neighborSpan->smin : maxHeight; + if (rcMin(ceiling, neighborCeiling) - floor > walkableHeight) + { + lowestNeighborFloorDifference = -walkableClimb - 1; + break; + } + + for (; neighborSpan != NULL; neighborSpan = neighborSpan->next) + { + const int neighborFloor = (int)neighborSpan->smax; + neighborCeiling + = neighborSpan->next ? (int)neighborSpan->next->smin : maxHeight; + if (rcMin(ceiling, neighborCeiling) - rcMax(floor, neighborFloor) + <= walkableHeight) + { + continue; + } + + const int difference = neighborFloor - floor; + lowestNeighborFloorDifference + = rcMin(lowestNeighborFloorDifference, difference); + if (rcAbs(difference) <= walkableClimb) + { + lowestTraversableNeighborFloor + = rcMin(lowestTraversableNeighborFloor, neighborFloor); + highestTraversableNeighborFloor + = rcMax(highestTraversableNeighborFloor, neighborFloor); + } + else if (difference < -walkableClimb) + { + break; + } + } + } + + if (lowestNeighborFloorDifference < -walkableClimb + || highestTraversableNeighborFloor - lowestTraversableNeighborFloor > walkableClimb) + { + span->area = RC_NULL_AREA; + } + } + } + } + } + + std::uint32_t nextRandom(std::uint32_t& state) + { + state = state * 1664525u + 1013904223u; + return state; + } } TEST_CASE("rcFilterLowHangingWalkableObstacles", "[recast, filtering]") @@ -313,6 +423,52 @@ TEST_CASE("rcFilterLedgeSpans", "[recast, filtering]") } } +TEST_CASE("rcFilterLedgeSpans matches the reference scan for multi-span columns", "[recast, filtering]") +{ + const int width = 12; + const int height = 12; + const float boundsMin[3] = { 0.0f, 0.0f, 0.0f }; + const float boundsMax[3] = { (float)width, 256.0f, (float)height }; + rcContext context; + + for (std::uint32_t seed = 1; seed <= 32; ++seed) + { + rcHeightfield reference; + rcHeightfield optimized; + CAPTURE(seed); + REQUIRE(rcCreateHeightfield( + &context, reference, width, height, boundsMin, boundsMax, 1.0f, 1.0f)); + REQUIRE(rcCreateHeightfield( + &context, optimized, width, height, boundsMin, boundsMax, 1.0f, 1.0f)); + + std::uint32_t random = seed * 7919u; + for (int z = 0; z < height; ++z) + { + for (int x = 0; x < width; ++x) + { + unsigned short spanMin = (unsigned short)(nextRandom(random) % 4); + const int spanCount = 1 + (int)(nextRandom(random) % 8); + for (int spanIndex = 0; spanIndex < spanCount; ++spanIndex) + { + const unsigned short spanMax + = (unsigned short)(spanMin + 1 + nextRandom(random) % 3); + const unsigned char area + = nextRandom(random) % 5 == 0 ? RC_NULL_AREA : (unsigned char)1; + REQUIRE(rcAddSpan(&context, reference, x, z, spanMin, spanMax, area, 0)); + REQUIRE(rcAddSpan(&context, optimized, x, z, spanMin, spanMax, area, 0)); + spanMin = (unsigned short)(spanMax + 1 + nextRandom(random) % 6); + } + } + } + + const int walkableHeight = 2 + (int)(seed % 5); + const int walkableClimb = 1 + (int)(seed % 4); + filterLedgeSpansReference(walkableHeight, walkableClimb, reference); + rcFilterLedgeSpans(&context, walkableHeight, walkableClimb, optimized); + CHECK_THAT(getSpans(optimized), Catch::Matchers::Equals(getSpans(reference))); + } +} + TEST_CASE("rcFilterWalkableLowHeightSpans", "[recast, filtering]") { rcContext context; diff --git a/Tests/Recast/Tests_RecastRasterization.cpp b/Tests/Recast/Tests_RecastRasterization.cpp new file mode 100644 index 000000000..7727838a3 --- /dev/null +++ b/Tests/Recast/Tests_RecastRasterization.cpp @@ -0,0 +1,382 @@ +#include +#include +#include +#include +#include + +#include "catch2/catch_all.hpp" + +#include "Recast.h" + +namespace +{ + enum class TriangleOverload + { + Unindexed, + IndexedInt, + IndexedShort, + }; + + using SpanValue = std::array; + + std::vector getSpans(const rcHeightfield& heightfield) + { + std::vector result; + for (int column = 0; column < heightfield.width * heightfield.height; ++column) + { + for (const rcSpan* span = heightfield.spans[column]; span != NULL; span = span->next) + { + result.push_back(SpanValue{ (unsigned int)column, (unsigned int)span->smin, + (unsigned int)span->smax, (unsigned int)span->area }); + } + } + return result; + } + + std::uint32_t nextRandom(std::uint32_t& state) + { + state = state * 1664525u + 1013904223u; + return state; + } + + float randomFloat(std::uint32_t& state, const float min, const float max) + { + const float unit = (float)(nextRandom(state) >> 8) * (1.0f / 16777216.0f); + return min + unit * (max - min); + } + + bool overlapBoundsReference( + const float* aMin, const float* aMax, const float* bMin, const float* bMax) + { + return aMin[0] <= bMax[0] && aMax[0] >= bMin[0] + && aMin[1] <= bMax[1] && aMax[1] >= bMin[1] + && aMin[2] <= bMax[2] && aMax[2] >= bMin[2]; + } + + enum class Axis + { + X = 0, + Z = 2, + }; + + // Keep the generic pre-optimization polygon split as an independent clipping oracle. + void dividePolyReference(const float* inVerts, const int inVertsCount, + float* outVerts1, int& outVerts1Count, float* outVerts2, int& outVerts2Count, + const float axisOffset, const Axis axis) + { + if (inVertsCount > 12) + throw std::logic_error("reference clipping input exceeds 12 vertices"); + const int axisIndex = (int)axis; + float inVertAxisDelta[12]; + for (int inVert = 0; inVert < inVertsCount; ++inVert) + inVertAxisDelta[inVert] = axisOffset - inVerts[inVert * 3 + axisIndex]; + + int poly1Vert = 0; + int poly2Vert = 0; + for (int inVertA = 0, inVertB = inVertsCount - 1; + inVertA < inVertsCount; inVertB = inVertA, ++inVertA) + { + const bool sameSide + = (inVertAxisDelta[inVertA] >= 0) == (inVertAxisDelta[inVertB] >= 0); + if (!sameSide) + { + const float s = inVertAxisDelta[inVertB] + / (inVertAxisDelta[inVertB] - inVertAxisDelta[inVertA]); + for (int component = 0; component < 3; ++component) + { + outVerts1[poly1Vert * 3 + component] = inVerts[inVertB * 3 + component] + + (inVerts[inVertA * 3 + component] - inVerts[inVertB * 3 + component]) * s; + } + rcVcopy(&outVerts2[poly2Vert * 3], &outVerts1[poly1Vert * 3]); + ++poly1Vert; + ++poly2Vert; + + if (inVertAxisDelta[inVertA] > 0) + { + rcVcopy(&outVerts1[poly1Vert * 3], &inVerts[inVertA * 3]); + ++poly1Vert; + } + else if (inVertAxisDelta[inVertA] < 0) + { + rcVcopy(&outVerts2[poly2Vert * 3], &inVerts[inVertA * 3]); + ++poly2Vert; + } + } + else + { + if (inVertAxisDelta[inVertA] >= 0) + { + rcVcopy(&outVerts1[poly1Vert * 3], &inVerts[inVertA * 3]); + ++poly1Vert; + if (inVertAxisDelta[inVertA] != 0) + continue; + } + rcVcopy(&outVerts2[poly2Vert * 3], &inVerts[inVertA * 3]); + ++poly2Vert; + } + } + + outVerts1Count = poly1Vert; + outVerts2Count = poly2Vert; + } + + bool rasterizeTriangleReference(rcContext& context, const float* v0, const float* v1, const float* v2, + const unsigned char area, rcHeightfield& heightfield, const int mergeThreshold) + { + float triangleMin[3]; + rcVcopy(triangleMin, v0); + rcVmin(triangleMin, v1); + rcVmin(triangleMin, v2); + float triangleMax[3]; + rcVcopy(triangleMax, v0); + rcVmax(triangleMax, v1); + rcVmax(triangleMax, v2); + if (!overlapBoundsReference(triangleMin, triangleMax, heightfield.bmin, heightfield.bmax)) + return true; + + const int width = heightfield.width; + const int height = heightfield.height; + const float inverseCellSize = 1.0f / heightfield.cs; + const float inverseCellHeight = 1.0f / heightfield.ch; + const float boundsHeight = heightfield.bmax[1] - heightfield.bmin[1]; + int z0 = (int)((triangleMin[2] - heightfield.bmin[2]) * inverseCellSize); + int z1 = (int)((triangleMax[2] - heightfield.bmin[2]) * inverseCellSize); + z0 = rcClamp(z0, -1, height - 1); + z1 = rcClamp(z1, 0, height - 1); + + float buffer[7 * 3 * 4]; + float* in = buffer; + float* inRow = buffer + 7 * 3; + float* polygon1 = inRow + 7 * 3; + float* polygon2 = polygon1 + 7 * 3; + rcVcopy(&in[0], v0); + rcVcopy(&in[3], v1); + rcVcopy(&in[6], v2); + int inCount = 3; + + for (int z = z0; z <= z1; ++z) + { + int rowCount = 0; + const float cellZ = heightfield.bmin[2] + (float)z * heightfield.cs; + dividePolyReference(in, inCount, inRow, rowCount, polygon1, inCount, + cellZ + heightfield.cs, Axis::Z); + rcSwap(in, polygon1); + if (rowCount < 3 || z < 0) + continue; + + float minX = inRow[0]; + float maxX = inRow[0]; + for (int vertex = 1; vertex < rowCount; ++vertex) + { + minX = rcMin(minX, inRow[vertex * 3]); + maxX = rcMax(maxX, inRow[vertex * 3]); + } + int x0 = (int)((minX - heightfield.bmin[0]) * inverseCellSize); + int x1 = (int)((maxX - heightfield.bmin[0]) * inverseCellSize); + if (x1 < 0 || x0 >= width) + continue; + x0 = rcClamp(x0, -1, width - 1); + x1 = rcClamp(x1, 0, width - 1); + + int remainderCount = rowCount; + for (int x = x0; x <= x1; ++x) + { + int cellCount = 0; + const float cellX = heightfield.bmin[0] + (float)x * heightfield.cs; + dividePolyReference(inRow, remainderCount, polygon1, cellCount, polygon2, remainderCount, + cellX + heightfield.cs, Axis::X); + rcSwap(inRow, polygon2); + if (cellCount < 3 || x < 0) + continue; + + float spanMin = polygon1[1]; + float spanMax = polygon1[1]; + for (int vertex = 1; vertex < cellCount; ++vertex) + { + spanMin = rcMin(spanMin, polygon1[vertex * 3 + 1]); + spanMax = rcMax(spanMax, polygon1[vertex * 3 + 1]); + } + spanMin -= heightfield.bmin[1]; + spanMax -= heightfield.bmin[1]; + if (spanMax < 0.0f || spanMin > boundsHeight) + continue; + spanMin = rcMax(spanMin, 0.0f); + spanMax = rcMin(spanMax, boundsHeight); + const unsigned short minCell = (unsigned short)rcClamp( + (int)floorf(spanMin * inverseCellHeight), 0, RC_SPAN_MAX_HEIGHT); + const unsigned short maxCell = (unsigned short)rcClamp( + (int)ceilf(spanMax * inverseCellHeight), (int)minCell + 1, RC_SPAN_MAX_HEIGHT); + if (!rcAddSpan(&context, heightfield, x, z, minCell, maxCell, area, mergeThreshold)) + return false; + } + } + return true; + } + + bool rasterizeTriangles(rcContext& context, const TriangleOverload overload, + const std::vector& vertices, const std::vector& areas, + rcHeightfield& heightfield, const int mergeThreshold) + { + const int triangleCount = (int)areas.size(); + if (overload == TriangleOverload::Unindexed) + { + return rcRasterizeTriangles( + &context, vertices.data(), areas.data(), triangleCount, heightfield, mergeThreshold); + } + + if (overload == TriangleOverload::IndexedInt) + { + std::vector indices((std::size_t)triangleCount * 3); + for (std::size_t i = 0; i < indices.size(); ++i) + indices[i] = (int)i; + return rcRasterizeTriangles(&context, vertices.data(), (int)indices.size(), indices.data(), + areas.data(), triangleCount, heightfield, mergeThreshold); + } + + std::vector indices((std::size_t)triangleCount * 3); + for (std::size_t i = 0; i < indices.size(); ++i) + indices[i] = (unsigned short)i; + return rcRasterizeTriangles(&context, vertices.data(), (int)indices.size(), indices.data(), + areas.data(), triangleCount, heightfield, mergeThreshold); + } +} + +TEST_CASE("Triangle rasterization matches reference clipping and incremental insertion", "[recast, rasterization]") +{ + const float boundsMin[3] = { 0.0f, 0.0f, 0.0f }; + const float boundsMax[3] = { 32.0f, 64.0f, 32.0f }; + const int triangleCounts[] = { 1, 63, 64, 65, 192 }; + + for (int overloadIndex = 0; overloadIndex < 3; ++overloadIndex) + { + for (const int triangleCount : triangleCounts) + { + for (std::uint32_t seed = 1; seed <= 4; ++seed) + { + const TriangleOverload overload = (TriangleOverload)overloadIndex; + const int mergeThreshold = (int)((seed + triangleCount + overloadIndex) % 5); + std::uint32_t random + = seed * 7919u + (std::uint32_t)triangleCount * 104729u + (std::uint32_t)overloadIndex; + std::vector vertices((std::size_t)triangleCount * 9); + std::vector areas((std::size_t)triangleCount); + for (int triangle = 0; triangle < triangleCount; ++triangle) + { + for (int vertex = 0; vertex < 3; ++vertex) + { + const std::size_t offset = (std::size_t)(triangle * 3 + vertex) * 3; + vertices[offset] = randomFloat(random, -4.0f, 36.0f); + vertices[offset + 1] = randomFloat(random, -2.0f, 66.0f); + vertices[offset + 2] = randomFloat(random, -4.0f, 36.0f); + } + areas[triangle] = (unsigned char)(nextRandom(random) % 64); + } + + rcContext context; + rcHeightfield optimized; + rcHeightfield reference; + CAPTURE(overloadIndex, seed, triangleCount, mergeThreshold); + REQUIRE(rcCreateHeightfield( + &context, optimized, 32, 32, boundsMin, boundsMax, 1.0f, 0.25f)); + REQUIRE(rcCreateHeightfield( + &context, reference, 32, 32, boundsMin, boundsMax, 1.0f, 0.25f)); + + REQUIRE(rasterizeTriangles(context, overload, vertices, areas, optimized, mergeThreshold)); + for (int triangle = 0; triangle < triangleCount; ++triangle) + { + REQUIRE(rasterizeTriangleReference(context, &vertices[(std::size_t)triangle * 9], + &vertices[(std::size_t)triangle * 9 + 3], &vertices[(std::size_t)triangle * 9 + 6], + areas[triangle], reference, mergeThreshold)); + } + + CHECK_THAT(getSpans(optimized), Catch::Matchers::Equals(getSpans(reference))); + } + } + } +} + +TEST_CASE("Bulk rasterization preserves a non-empty heightfield", "[recast, rasterization]") +{ + const float boundsMin[3] = { 0.0f, 0.0f, 0.0f }; + const float boundsMax[3] = { 8.0f, 32.0f, 8.0f }; + const int triangleCount = 64; + std::uint32_t random = 0x51a7e123u; + std::vector vertices((std::size_t)triangleCount * 9); + std::vector areas((std::size_t)triangleCount); + for (int triangle = 0; triangle < triangleCount; ++triangle) + { + for (int vertex = 0; vertex < 3; ++vertex) + { + const std::size_t offset = (std::size_t)(triangle * 3 + vertex) * 3; + vertices[offset] = randomFloat(random, -1.0f, 9.0f); + vertices[offset + 1] = randomFloat(random, -1.0f, 33.0f); + vertices[offset + 2] = randomFloat(random, -1.0f, 9.0f); + } + areas[triangle] = (unsigned char)(nextRandom(random) % 64); + } + + rcContext context; + rcHeightfield optimized; + rcHeightfield reference; + REQUIRE(rcCreateHeightfield(&context, optimized, 8, 8, boundsMin, boundsMax, 1.0f, 0.25f)); + REQUIRE(rcCreateHeightfield(&context, reference, 8, 8, boundsMin, boundsMax, 1.0f, 0.25f)); + REQUIRE(rcAddSpan(&context, optimized, 3, 3, 1, 3, 7, 2)); + REQUIRE(rcAddSpan(&context, reference, 3, 3, 1, 3, 7, 2)); + REQUIRE(rcRasterizeTriangles( + &context, vertices.data(), areas.data(), triangleCount, optimized, 2)); + for (int triangle = 0; triangle < triangleCount; ++triangle) + { + REQUIRE(rasterizeTriangleReference(context, &vertices[(std::size_t)triangle * 9], + &vertices[(std::size_t)triangle * 9 + 3], &vertices[(std::size_t)triangle * 9 + 6], + areas[triangle], reference, 2)); + } + CHECK_THAT(getSpans(optimized), Catch::Matchers::Equals(getSpans(reference))); +} + +TEST_CASE("Specialized clipping matches generic clipping at grid and heightfield boundaries", + "[recast, rasterization]") +{ + const float boundsMin[3] = { 0.0f, 0.0f, 0.0f }; + const float boundsMax[3] = { 4.0f, 8.0f, 4.0f }; + const std::array, 5> triangles = { { + { 0.0f, 1.0f, 0.0f, 1.0f, 2.0f, 0.0f, 0.0f, 3.0f, 1.0f }, + { -1.0f, -1.0f, 0.5f, 2.0f, 4.0f, 0.5f, 0.5f, 9.0f, 2.0f }, + { 3.5f, 1.0f, 3.5f, 4.5f, 2.0f, 3.5f, 3.5f, 3.0f, 4.5f }, + { 1.0f, 1.0f, 1.0f, 2.0f, 2.0f, 1.0f, 1.0f, 3.0f, 2.0f }, + { 1.2f, 1.0f, 1.2f, 2.8f, 2.0f, 1.7f, 1.7f, 3.0f, 2.8f }, + } }; + + for (std::size_t caseIndex = 0; caseIndex < triangles.size(); ++caseIndex) + { + rcContext context; + rcHeightfield optimized; + rcHeightfield reference; + CAPTURE(caseIndex); + REQUIRE(rcCreateHeightfield(&context, optimized, 4, 4, boundsMin, boundsMax, 1.0f, 0.5f)); + REQUIRE(rcCreateHeightfield(&context, reference, 4, 4, boundsMin, boundsMax, 1.0f, 0.5f)); + const std::array& triangle = triangles[caseIndex]; + REQUIRE(rcRasterizeTriangle( + &context, &triangle[0], &triangle[3], &triangle[6], 42, optimized, 1)); + REQUIRE(rasterizeTriangleReference( + context, &triangle[0], &triangle[3], &triangle[6], 42, reference, 1)); + CHECK_THAT(getSpans(optimized), Catch::Matchers::Equals(getSpans(reference))); + } +} + +TEST_CASE("Rasterizing a triangle contained by one voxel produces its height span", "[recast, rasterization]") +{ + rcContext context; + const float boundsMin[3] = { 0.0f, 0.0f, 0.0f }; + const float boundsMax[3] = { 4.0f, 8.0f, 4.0f }; + rcHeightfield heightfield; + REQUIRE(rcCreateHeightfield(&context, heightfield, 4, 4, boundsMin, boundsMax, 1.0f, 0.5f)); + + const float vertices[9] = { + 1.1f, 1.1f, 2.1f, + 1.8f, 2.4f, 2.2f, + 1.3f, 1.6f, 2.9f, + }; + REQUIRE(rcRasterizeTriangle(&context, &vertices[0], &vertices[3], &vertices[6], 42, heightfield, 1)); + + const std::vector expected = { SpanValue{ 9, 2, 5, 42 } }; + CHECK_THAT(getSpans(heightfield), Catch::Matchers::Equals(expected)); +}