Added more rcRasterizeTriangle unit tests and moved them to Tests_RecastRasterization.cpp

This commit is contained in:
Graham Pentheny
2026-02-08 17:19:31 -05:00
parent 762ecb5d79
commit 78f2d462af
2 changed files with 385 additions and 157 deletions

View File

@@ -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;

View File

@@ -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);
}
}
}
}