#include "Recast.h" #include "catch2/catch_amalgamated.hpp" TEST_CASE("rcAddSpan", "[recast][rasterization]") { rcContext ctx(false); constexpr int xSize = 4; constexpr int ySize = 10; constexpr int zSize = 4; constexpr float cellSize = 1.0f; constexpr float cellHeight = 2.0f; constexpr float minBounds[3] {0.0f, 0.0f, 0.0f}; constexpr float maxBounds[3] {cellSize * xSize, cellHeight * ySize, cellSize * zSize}; rcHeightfield hf; REQUIRE(rcCreateHeightfield(&ctx, hf, xSize, zSize, minBounds, maxBounds, cellSize, cellHeight)); constexpr unsigned char area = 42; constexpr int flagMergeThr = 1; SECTION("Add a span to an empty heightfield.") { REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 0, 1, area, flagMergeThr)); REQUIRE(hf.spans[0] != nullptr); REQUIRE(hf.spans[0]->smin == 0); REQUIRE(hf.spans[0]->smax == 1); REQUIRE(hf.spans[0]->area == area); REQUIRE(hf.spans[0]->next == nullptr); } SECTION("Adding invalid or zero-size spans does nothing.") { // min == max REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 0, 0, area, flagMergeThr)); REQUIRE(hf.spans[0] == nullptr); // min > maxs REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 1, 0, area, flagMergeThr)); REQUIRE(hf.spans[0] == nullptr); } SECTION("Two spans that are not touching are not merged.") { REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 0, 1, area, flagMergeThr)); REQUIRE(hf.spans[0] != nullptr); REQUIRE(hf.spans[0]->smin == 0); REQUIRE(hf.spans[0]->smax == 1); REQUIRE(hf.spans[0]->area == area); REQUIRE(hf.spans[0]->next == nullptr); REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 2, 3, area, flagMergeThr)); REQUIRE(hf.spans[0] != nullptr); REQUIRE(hf.spans[0]->next != nullptr); REQUIRE(hf.spans[0]->next->smin == 2); REQUIRE(hf.spans[0]->next->smax == 3); REQUIRE(hf.spans[0]->next->area == area); REQUIRE(hf.spans[0]->next->next == nullptr); } SECTION("Two spans with different area ids within the flag merge threshold are merged and the highest area ID is used.") { REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 0, 1, 42, flagMergeThr)); REQUIRE(hf.spans[0] != nullptr); REQUIRE(hf.spans[0]->smin == 0); REQUIRE(hf.spans[0]->smax == 1); REQUIRE(hf.spans[0]->area == 42); REQUIRE(hf.spans[0]->next == nullptr); REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 1, 2, 24, flagMergeThr)); REQUIRE(hf.spans[0] != nullptr); REQUIRE(hf.spans[0]->smin == 0); REQUIRE(hf.spans[0]->smax == 2); REQUIRE(hf.spans[0]->area == 42); // Higher area ID takes precedent REQUIRE(hf.spans[0]->next == nullptr); } SECTION("Two spans with different area ids outside the flag merge threshold are merged and the area ID of the last span added is used.") { REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 0, 1, 42, flagMergeThr)); REQUIRE(hf.spans[0] != nullptr); REQUIRE(hf.spans[0]->smin == 0); REQUIRE(hf.spans[0]->smax == 1); REQUIRE(hf.spans[0]->area == 42); REQUIRE(hf.spans[0]->next == nullptr); REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 1, 8, 24, flagMergeThr)); REQUIRE(hf.spans[0] != nullptr); REQUIRE(hf.spans[0]->smin == 0); REQUIRE(hf.spans[0]->smax == 8); REQUIRE(hf.spans[0]->area == 24); // Area ID of the last-added span takes precedent REQUIRE(hf.spans[0]->next == nullptr); } SECTION("Add a span that gets merged with an existing span.") { REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 0, 1, area, flagMergeThr)); REQUIRE(hf.spans[0] != nullptr); REQUIRE(hf.spans[0]->smin == 0); REQUIRE(hf.spans[0]->smax == 1); REQUIRE(hf.spans[0]->area == area); REQUIRE(hf.spans[0]->next == nullptr); REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 1, 2, area, flagMergeThr)); REQUIRE(hf.spans[0] != nullptr); REQUIRE(hf.spans[0]->smin == 0); REQUIRE(hf.spans[0]->smax == 2); REQUIRE(hf.spans[0]->area == area); REQUIRE(hf.spans[0]->next == nullptr); } SECTION("Add a span that merges with two spans above and below.") { REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 0, 1, area, flagMergeThr)); REQUIRE(hf.spans[0] != nullptr); REQUIRE(hf.spans[0]->smin == 0); REQUIRE(hf.spans[0]->smax == 1); REQUIRE(hf.spans[0]->area == area); REQUIRE(hf.spans[0]->next == nullptr); REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 2, 3, area, flagMergeThr)); REQUIRE(hf.spans[0]->next != nullptr); REQUIRE(hf.spans[0]->next->smin == 2); REQUIRE(hf.spans[0]->next->smax == 3); REQUIRE(hf.spans[0]->next->area == area); REQUIRE(hf.spans[0]->next->next == nullptr); // After adding the third span, they should all get merged into a single span. REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 1, 2, area, flagMergeThr)); REQUIRE(hf.spans[0] != nullptr); REQUIRE(hf.spans[0]->smin == 0); REQUIRE(hf.spans[0]->smax == 3); REQUIRE(hf.spans[0]->area == area); REQUIRE(hf.spans[0]->next == nullptr); } SECTION("Spans are insertion-sorted in ascending order of Y value.") { REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 2, 3, area, flagMergeThr)); REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 0, 1, area, flagMergeThr)); REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 6, 7, area, flagMergeThr)); REQUIRE(hf.spans[0] != nullptr); REQUIRE(hf.spans[0]->smin == 0); REQUIRE(hf.spans[0]->smax == 1); REQUIRE(hf.spans[0]->area == area); REQUIRE(hf.spans[0]->next != nullptr); REQUIRE(hf.spans[0]->next->smin == 2); REQUIRE(hf.spans[0]->next->smax == 3); REQUIRE(hf.spans[0]->next->area == area); REQUIRE(hf.spans[0]->next->next != nullptr); REQUIRE(hf.spans[0]->next->next->smin == 6); REQUIRE(hf.spans[0]->next->next->smax == 7); REQUIRE(hf.spans[0]->next->next->area == area); REQUIRE(hf.spans[0]->next->next->next == nullptr); } SECTION("Adding a span inside another span merges them.") { REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 0, 8, area, flagMergeThr)); REQUIRE(hf.spans[0] != nullptr); REQUIRE(hf.spans[0]->smin == 0); REQUIRE(hf.spans[0]->smax == 8); REQUIRE(hf.spans[0]->area == area); REQUIRE(hf.spans[0]->next == nullptr); REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 2, 3, area, flagMergeThr)); REQUIRE(hf.spans[0] != nullptr); REQUIRE(hf.spans[0]->smin == 0); REQUIRE(hf.spans[0]->smax == 8); REQUIRE(hf.spans[0]->area == area); REQUIRE(hf.spans[0]->next == nullptr); } SECTION("Overlapping spans are merged.") { REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 0, 4, area, flagMergeThr)); REQUIRE(hf.spans[0] != nullptr); REQUIRE(hf.spans[0]->smin == 0); REQUIRE(hf.spans[0]->smax == 4); REQUIRE(hf.spans[0]->area == area); REQUIRE(hf.spans[0]->next == nullptr); REQUIRE(rcAddSpan(&ctx, hf, 0, 0, 2, 6, area, flagMergeThr)); REQUIRE(hf.spans[0] != nullptr); REQUIRE(hf.spans[0]->smin == 0); REQUIRE(hf.spans[0]->smax == 6); REQUIRE(hf.spans[0]->area == area); REQUIRE(hf.spans[0]->next == nullptr); } } TEST_CASE("allocSpan", "[recast][rasterization]") { rcContext ctx(false); constexpr int xSize = 50; constexpr int zSize = 50; constexpr int ySize = 10; constexpr float cellSize = 1.0f; constexpr float cellHeight = 2.0f; constexpr float minBounds[3] {0.0f, 0.0f, 0.0f}; constexpr float maxBounds[3] {cellSize * xSize, cellHeight * ySize, cellSize * zSize}; rcHeightfield hf; REQUIRE(rcCreateHeightfield(&ctx, hf, xSize, zSize, minBounds, maxBounds, cellSize, cellHeight)); constexpr unsigned char area = 42; constexpr int flagMergeThr = 1; SECTION("Attempting to add more spans than the span pool size allocates a new page") { for (int x = 0; x < xSize; x++) { for (int z = 0; z < zSize; z++) { REQUIRE(rcAddSpan(&ctx, hf, x, z, 0, 1, area, flagMergeThr)); } } } } TEST_CASE("rcRasterizeTriangle", "[recast][rasterization]") { rcContext ctx(false); constexpr int xSize = 10; constexpr int ySize = 10; constexpr int zSize = 10; constexpr float cellSize = 1.0f; constexpr float cellHeight = 1.0f; constexpr float minBounds[3] {0.0f, 0.0f, 0.0f}; constexpr float maxBounds[3] {cellSize * xSize, cellHeight * ySize, cellSize * zSize}; rcHeightfield hf; REQUIRE(rcCreateHeightfield(&ctx, hf, xSize, zSize, minBounds, maxBounds, cellSize, cellHeight)); SECTION("Simple triangle in XZ plane") { // Triangle in the XZ plane with vertices (0,0,0), (2,0,0), (0,0,2) // // Z ^ // | // |\ // |_\__> X // // // Z .... // X... // XX.. X // // Should fill cells: (0,0), (0,1), (1,0) // Clockwise winding order so the normal points in the positive Y float v0[] = {0.0f, 0.0f, 0.0f}; float v1[] = {2.0f, 0.0f, 0.0f}; float v2[] = {0.0f, 0.0f, 2.0f}; REQUIRE(rcRasterizeTriangle(&ctx, v0, v1, v2, RC_WALKABLE_AREA, hf, 1)); // Check that only expected cells have spans for (int x = 0; x < xSize; x++) { for (int z = 0; z < zSize; z++) { rcSpan* span = hf.spans[x + z * hf.width]; if ((x == 0 && z == 0) || (x == 0 && z == 1) || (x == 1 && z == 0)) { REQUIRE(span != nullptr); REQUIRE(span->smin == 0); REQUIRE(span->smax == 1); REQUIRE(span->area == RC_WALKABLE_AREA); REQUIRE(span->next == nullptr); } else { REQUIRE(span == nullptr); } } } } SECTION("Simple triangle inside a single voxel") { // Clockwise winding order so the normal points in the positive Y float v0[] = {0.0f, 0.0f, 0.0f}; float v1[] = {1.0f, 0.0f, 0.0f}; float v2[] = {0.0f, 0.0f, 1.0f}; REQUIRE(rcRasterizeTriangle(&ctx, v0, v1, v2, RC_WALKABLE_AREA, hf, 1)); // Check that only expected cells have spans for (int x = 0; x < xSize; x++) { for (int z = 0; z < zSize; z++) { rcSpan* span = hf.spans[x + z * hf.width]; if (x == 0 && z == 0) { REQUIRE(span != nullptr); REQUIRE(span->smin == 0); REQUIRE(span->smax == 1); REQUIRE(span->area == RC_WALKABLE_AREA); REQUIRE(span->next == nullptr); } else { REQUIRE(span == nullptr); } } } } SECTION("Triangles are clipped by the heightfield bounds") { // When clipped, this should be the same as the triangle in the previous test // Should fill cells: (0,0), (0,1), (1,0) // Clockwise winding order so the normal points in the positive Y float v0[] = {-2.0f, 0.0f, -2.0f}; float v1[] = {4.0f, 0.0f, -2.0f}; float v2[] = {-2.0f, 0.0f, 4.0f}; REQUIRE(rcRasterizeTriangle(&ctx, v0, v1, v2, RC_WALKABLE_AREA, hf, 1)); // Check that only expected cells have spans for (int x = 0; x < xSize; x++) { for (int z = 0; z < zSize; z++) { rcSpan* span = hf.spans[x + z * hf.width]; if ((x == 0 && z == 0) || (x == 0 && z == 1) || (x == 1 && z == 0)) { REQUIRE(span != nullptr); REQUIRE(span->smin == 0); REQUIRE(span->smax == 1); REQUIRE(span->area == RC_WALKABLE_AREA); REQUIRE(span->next == nullptr); } else { REQUIRE(span == nullptr); } } } } SECTION("Triangle outside of heightfield bounds rasterizes to nothing") { { // Outside xz bounds float v0[] = {-5.0f, 0.0f, -5.0f}; float v1[] = {-5.0f, 0.0f, 5.0f}; float v2[] = {5.0f, 0.0f, -5.0f}; REQUIRE(rcRasterizeTriangle(&ctx, v0, v1, v2, RC_WALKABLE_AREA, hf, 1)); // Check that no spans were added for (int x = 0; x < xSize; x++) { for (int z = 0; z < zSize; z++) { REQUIRE(hf.spans[x + z * hf.width] == nullptr); } } } { // below y bounds float v0[] = {0.0f, -1.0f, 0.0f}; float v1[] = {5.0f, -1.0f, 5.0f}; float v2[] = {5.0f, -1.0f, 0.0f}; REQUIRE(rcRasterizeTriangle(&ctx, v0, v1, v2, RC_WALKABLE_AREA, hf, 1)); // Check that no spans were added for (int x = 0; x < xSize; x++) { for (int z = 0; z < zSize; z++) { REQUIRE(hf.spans[x + z * hf.width] == nullptr); } } } { // above y bounds float v0[] = {0.0f, 40.0f, 0.0f}; float v1[] = {5.0f, 40.0f, 5.0f}; float v2[] = {5.0f, 40.0f, 0.0f}; REQUIRE(rcRasterizeTriangle(&ctx, v0, v1, v2, RC_WALKABLE_AREA, hf, 1)); // Check that no spans were added for (int x = 0; x < xSize; x++) { for (int z = 0; z < zSize; z++) { REQUIRE(hf.spans[x + z * hf.width] == nullptr); } } } } SECTION("Voxels are rasterized if the triangle overlaps any part of it at all") { // This triangle when clipped barely overlaps the area of the cell at 0,0 // Clockwise winding order so the normal points in the positive Y float v0[] = {-1.0f, 0.0f, -1.0f}; float v1[] = {1.01f, 0.0f, -1.0f}; float v2[] = {-1.0f, 0.0f, 1.01f}; REQUIRE(rcRasterizeTriangle(&ctx, v0, v1, v2, RC_WALKABLE_AREA, hf, 1)); // Check that only expected cells have spans for (int x = 0; x < xSize; x++) { for (int z = 0; z < zSize; z++) { rcSpan* span = hf.spans[x + z * hf.width]; if (x == 0 && z == 0) { REQUIRE(span != nullptr); REQUIRE(span->smin == 0); REQUIRE(span->smax == 1); REQUIRE(span->area == RC_WALKABLE_AREA); REQUIRE(span->next == nullptr); } else { REQUIRE(span == nullptr); } } } } SECTION("A rasterized triangle vertical span includes all the voxels it is in any way part of.") { // Clockwise winding order so the normal points in the positive Y float v0[] = {0.0f, 0.0f, 0.0f}; float v1[] = {0.5f, 0.0f, 0.5f}; float v2[] = {0.5f, 2.01f, 0.5f}; REQUIRE(rcRasterizeTriangle(&ctx, v0, v1, v2, RC_WALKABLE_AREA, hf, 1)); // Check that only expected cells have spans for (int x = 0; x < xSize; x++) { for (int z = 0; z < zSize; z++) { rcSpan* span = hf.spans[x + z * hf.width]; if (x == 0 && z == 0) { REQUIRE(span != nullptr); REQUIRE(span->smin == 0); REQUIRE(span->smax == 3); REQUIRE(span->area == RC_WALKABLE_AREA); REQUIRE(span->next == nullptr); } else { REQUIRE(span == nullptr); } } } } SECTION("Sloped triangle produces varying span heights") { // Y ^ // 2| /v2 // | / \ // 1| / \ // | / \ // --+-v0------v1----> X // | 0 2 4 // // Should rasterize to... // // Y ^ // 2| |--|--| // | | | // 1|--| + |--| // | | // --+--------------> X // | // float v0[] = {0.0f, 0.0f, 0.5f}; float v1[] = {4.0f, 0.0f, 0.5f}; float v2[] = {2.0f, 2.0f, 0.5f}; REQUIRE(rcRasterizeTriangle(&ctx, v0, v1, v2, RC_WALKABLE_AREA, hf, 1)); // Check that only expected cells have spans for (int x = 0; x < xSize; x++) { for (int z = 0; z < zSize; z++) { rcSpan* span = hf.spans[x + z * hf.width]; if ((x == 0 && z == 0) || (x == 3 && z == 0)) { REQUIRE(span != nullptr); REQUIRE(span->smin == 0); REQUIRE(span->smax == 1); REQUIRE(span->area == RC_WALKABLE_AREA); REQUIRE(span->next == nullptr); } else if ((x == 1 && z == 0) || (x == 2 && z == 0)) { REQUIRE(span != nullptr); REQUIRE(span->smin == 0); REQUIRE(span->smax == 2); REQUIRE(span->area == RC_WALKABLE_AREA); REQUIRE(span->next == nullptr); } else { REQUIRE(span == nullptr); } } } } SECTION("Triangle crossing Y bounds gets clamped") { // This trangle should be clipped to both the max and min Y bounds of the heightfield. float v0[] = {0.0f, -5.0f, 0.0f}; float v1[] = {1.0f, -5.0f, 1.0f}; float v2[] = {0.5f, 15.0f, 0.5f}; REQUIRE(rcRasterizeTriangle(&ctx, v0, v1, v2, RC_WALKABLE_AREA, hf, 1)); // Check that only expected cells have spans for (int x = 0; x < xSize; x++) { for (int z = 0; z < zSize; z++) { rcSpan* span = hf.spans[x + z * hf.width]; if (x == 0 && z == 0) { REQUIRE(span != nullptr); REQUIRE(span->smin == 0); REQUIRE(span->smax == 10); REQUIRE(span->area == RC_WALKABLE_AREA); REQUIRE(span->next == nullptr); } else { REQUIRE(span == nullptr); } } } } SECTION("Degenerate triangles rasterize to nothing") { SKIP("Currently Recast rasterizes degenerate triangles as if they were a line or point of non-zero volume."); { // Co-linear points float v0[] = {1.0f, 0.0f, 0.5f}; float v1[] = {2.0f, 0.0f, 0.5f}; float v2[] = {4.0f, 0.0f, 0.5f}; REQUIRE(rcRasterizeTriangle(&ctx, v0, v1, v2, RC_WALKABLE_AREA, hf, 1)); // Check that no spans were added for (int x = 0; x < xSize; x++) { for (int z = 0; z < zSize; z++) { rcSpan* span = hf.spans[x + z * hf.width]; REQUIRE(span == nullptr); } } } { // All vertices are the same point float v0[] = {0.5f, 0.0f, 0.5f}; REQUIRE(rcRasterizeTriangle(&ctx, v0, v0, v0, RC_WALKABLE_AREA, hf, 1)); // Check that no spans were added for (int x = 0; x < xSize; x++) { for (int z = 0; z < zSize; z++) { rcSpan* span = hf.spans[x + z * hf.width]; REQUIRE(span == nullptr); } } } } SECTION("Triangles outside the heightfield with bounding boxes that overlap the heightfield rasterize nothing") { // This is a minimal repro case for the issue fixed in PR #476 (https://github.com/recastnavigation/recastnavigation/pull/476) float v0[] = {-10.0, 5.5, -10.0}; float v1[] = {-10.0, 5.5, 3}; float v2[] = {3.0, 5.5, -10.0}; REQUIRE(rcRasterizeTriangle(&ctx, v0, v1, v2, RC_WALKABLE_AREA, hf, 1)); // Check that no spans were added for (int x = 0; x < xSize; x++) { for (int z = 0; z < zSize; z++) { REQUIRE(hf.spans[x + z * hf.width] == nullptr); } } } }