From 78f2d462afc6d306b4b3f2db061ca8185e12cd41 Mon Sep 17 00:00:00 2001 From: Graham Pentheny Date: Sun, 8 Feb 2026 17:19:31 -0500 Subject: [PATCH] Added more rcRasterizeTriangle unit tests and moved them to Tests_RecastRasterization.cpp --- Tests/Recast/Tests_Recast.cpp | 157 --------- Tests/Recast/Tests_RecastRasterization.cpp | 385 +++++++++++++++++++++ 2 files changed, 385 insertions(+), 157 deletions(-) diff --git a/Tests/Recast/Tests_Recast.cpp b/Tests/Recast/Tests_Recast.cpp index f02d9175..1989cc15 100644 --- a/Tests/Recast/Tests_Recast.cpp +++ b/Tests/Recast/Tests_Recast.cpp @@ -511,163 +511,6 @@ TEST_CASE("rcClearUnwalkableTriangles", "[recast]") } } - -TEST_CASE("rcRasterizeTriangle", "[recast]") -{ - rcContext ctx; - float verts[] = { - 0, 0, 0, - 1, 0, 0, - 0, 0, -1 - }; - float bmin[3]; - float bmax[3]; - rcCalcBounds(verts, 3, bmin, bmax); - - float cellSize = .5f; - float cellHeight = .5f; - - int width; - int height; - - rcCalcGridSize(bmin, bmax, cellSize, &width, &height); - - rcHeightfield solid; - REQUIRE(rcCreateHeightfield(&ctx, solid, width, height, bmin, bmax, cellSize, cellHeight)); - - unsigned char area = 42; - int flagMergeThr = 1; - - SECTION("Rasterize a triangle") - { - REQUIRE(rcRasterizeTriangle(&ctx, &verts[0], &verts[3], &verts[6], area, solid, flagMergeThr)); - - REQUIRE(solid.spans[0 + 0 * width]); - REQUIRE(!solid.spans[1 + 0 * width]); - REQUIRE(solid.spans[0 + 1 * width]); - REQUIRE(solid.spans[1 + 1 * width]); - - REQUIRE(solid.spans[0 + 0 * width]->smin == 0); - REQUIRE(solid.spans[0 + 0 * width]->smax == 1); - REQUIRE(solid.spans[0 + 0 * width]->area == area); - REQUIRE(!solid.spans[0 + 0 * width]->next); - - REQUIRE(solid.spans[0 + 1 * width]->smin == 0); - REQUIRE(solid.spans[0 + 1 * width]->smax == 1); - REQUIRE(solid.spans[0 + 1 * width]->area == area); - REQUIRE(!solid.spans[0 + 1 * width]->next); - - REQUIRE(solid.spans[1 + 1 * width]->smin == 0); - REQUIRE(solid.spans[1 + 1 * width]->smax == 1); - REQUIRE(solid.spans[1 + 1 * width]->area == area); - REQUIRE(!solid.spans[1 + 1 * width]->next); - } -} - -TEST_CASE("rcRasterizeTriangle overlapping bb but non-overlapping triangle", "[recast]") -{ - // This is a minimal repro case for the issue fixed in PR #476 (https://github.com/recastnavigation/recastnavigation/pull/476) - rcContext ctx; - - // create a heightfield - float cellSize = 1; - float cellHeight = 1; - int width = 10; - int height = 10; - float bmin[] = { 0, 0, 0 }; - float bmax[] = { 10, 10, 10 }; - rcHeightfield heightfield; - REQUIRE(rcCreateHeightfield(&ctx, heightfield, width, height, bmin, bmax, cellSize, cellHeight)); - - // rasterize a triangle outside of the heightfield. - unsigned char area = 42; - int flagMergeThr = 1; - float verts[] = - { - -10.0, 5.5, -10.0, - -10.0, 5.5, 3, - 3.0, 5.5, -10.0 - }; - REQUIRE(rcRasterizeTriangle(&ctx, &verts[0], &verts[3], &verts[6], area, heightfield, flagMergeThr)); - - // ensure that no spans were created - for (int x = 0; x < width; ++x) - { - for (int z = 0; z < height; ++z) - { - rcSpan* span = heightfield.spans[x + z * heightfield.width]; - REQUIRE(span == NULL); - } - } -} - -TEST_CASE("rcRasterizeTriangle smaller than half a voxel size in x", "[recast]") -{ - SECTION("Skinny triangle along x axis") - { - rcContext ctx; - float verts[] = { - 5, 0, 0.005f, - 5, 0, -0.005f, - -5, 0, 0.005f, - - -5, 0, 0.005f, - 5, 0, -0.005f, - -5, 0, -0.005f, - }; - float bmin[3]; - float bmax[3]; - rcCalcBounds(verts, 3, bmin, bmax); - - float cellSize = 1; - float cellHeight = 1; - - int width; - int height; - - rcCalcGridSize(bmin, bmax, cellSize, &width, &height); - - rcHeightfield solid; - REQUIRE(rcCreateHeightfield(&ctx, solid, width, height, bmin, bmax, cellSize, cellHeight)); - - unsigned char areas[] = {42, 42}; - int flagMergeThr = 1; - REQUIRE(rcRasterizeTriangles(&ctx, verts, areas, 2, solid, flagMergeThr)); - } - - SECTION("Skinny triangle along z axis") - { - rcContext ctx; - float verts[] = { - 0.005f, 0, 5, - -0.005f, 0, 5, - 0.005f, 0, -5, - - 0.005f, 0, -5, - -0.005f, 0, 5, - -0.005f, 0, -5 - }; - float bmin[3]; - float bmax[3]; - rcCalcBounds(verts, 3, bmin, bmax); - - float cellSize = 1; - float cellHeight = 1; - - int width; - int height; - - rcCalcGridSize(bmin, bmax, cellSize, &width, &height); - - rcHeightfield solid; - REQUIRE(rcCreateHeightfield(&ctx, solid, width, height, bmin, bmax, cellSize, cellHeight)); - - unsigned char areas[] = {42, 42}; - int flagMergeThr = 1; - REQUIRE(rcRasterizeTriangles(&ctx, verts, areas, 2, solid, flagMergeThr)); - } -} - TEST_CASE("rcRasterizeTriangles", "[recast]") { rcContext ctx; diff --git a/Tests/Recast/Tests_RecastRasterization.cpp b/Tests/Recast/Tests_RecastRasterization.cpp index c29bd870..a5522603 100644 --- a/Tests/Recast/Tests_RecastRasterization.cpp +++ b/Tests/Recast/Tests_RecastRasterization.cpp @@ -193,3 +193,388 @@ TEST_CASE("rcAddSpan", "[recast][rasterization]") REQUIRE(hf.spans[0]->next == nullptr); } } + +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); + } + } + } +}