diff --git a/tests/testMesh.cpp b/tests/testMesh.cpp new file mode 100644 index 0000000..c431dc1 --- /dev/null +++ b/tests/testMesh.cpp @@ -0,0 +1,109 @@ +#include "testHarness.h" +#include "vf/Mesh.h" + +#include + +using namespace vf; + +namespace { + +float quadAreaSum(const Mesh& mesh) { + float sum = 0.0f; + for (std::size_t q = 0; q < mesh.quadCount(); ++q) { + const Vertex& corner = mesh.vertices[q * 4 + 2]; // uv here holds (width, height) + sum += corner.u * corner.v; + } + return sum; +} + +bool hasNormal(const Mesh& mesh, float nx, float ny, float nz) { + for (const Vertex& v : mesh.vertices) + if (std::fabs(v.nx - nx) < 1e-5f && std::fabs(v.ny - ny) < 1e-5f && + std::fabs(v.nz - nz) < 1e-5f) + return true; + return false; +} + +bool hasTile(const Mesh& mesh, int tile) { + for (const Vertex& v : mesh.vertices) + if (static_cast(v.tile) == tile) return true; + return false; +} + +} // namespace + +VF_TEST(meshSingleBlockHasSixFaces) { + World world; + world.setBlock(1, 1, 1, Block::Stone); + Mesh mesh = greedyMeshChunk(world, {0, 0, 0}); + CHECK_EQ(mesh.quadCount(), static_cast(6)); + CHECK_EQ(mesh.indices.size(), static_cast(36)); + CHECK_EQ(culledFaceCount(world, {0, 0, 0}), 6); + CHECK(hasNormal(mesh, 1, 0, 0)); + CHECK(hasNormal(mesh, -1, 0, 0)); + CHECK(hasNormal(mesh, 0, 1, 0)); + CHECK(hasNormal(mesh, 0, -1, 0)); + CHECK(hasNormal(mesh, 0, 0, 1)); + CHECK(hasNormal(mesh, 0, 0, -1)); +} + +VF_TEST(meshInteriorFacesAreCulled) { + World world; + for (int x = 0; x < chunkSize; ++x) + for (int y = 0; y < chunkSize; ++y) + for (int z = 0; z < chunkSize; ++z) + world.setBlock(x, y, z, Block::Stone); + + Mesh mesh = greedyMeshChunk(world, {0, 0, 0}); + int culled = culledFaceCount(world, {0, 0, 0}); + CHECK_EQ(culled, 6 * chunkSize * chunkSize); // only the outer shell + CHECK_EQ(mesh.quadCount(), static_cast(6)); // each side merges to one quad + CHECK(mesh.quadCount() < static_cast(culled)); + CHECK_NEAR(quadAreaSum(mesh), static_cast(culled), 1e-3f); +} + +VF_TEST(meshGreedyPreservesVisibleArea) { + World world; + world.setBlock(1, 1, 1, Block::Stone); + world.setBlock(2, 1, 1, Block::Stone); + world.setBlock(3, 1, 1, Block::Stone); + + Mesh mesh = greedyMeshChunk(world, {0, 0, 0}); + int culled = culledFaceCount(world, {0, 0, 0}); + CHECK_EQ(culled, 3 * 6 - 2 * 2); // two shared faces hidden on both sides + CHECK_NEAR(quadAreaSum(mesh), static_cast(culled), 1e-3f); + CHECK(mesh.quadCount() <= static_cast(culled)); +} + +VF_TEST(meshCullsAcrossChunkBoundary) { + World world; + for (int x = 0; x < 2 * chunkSize; ++x) + for (int y = 0; y < chunkSize; ++y) + for (int z = 0; z < chunkSize; ++z) + world.setBlock(x, y, z, Block::Stone); + + Mesh left = greedyMeshChunk(world, {0, 0, 0}); + int culled = culledFaceCount(world, {0, 0, 0}); + CHECK_EQ(culled, 5 * chunkSize * chunkSize); // +x boundary hidden by neighbour chunk + CHECK_EQ(left.quadCount(), static_cast(5)); + CHECK(!hasNormal(left, 1, 0, 0)); // no +x face at the shared border + CHECK_NEAR(quadAreaSum(left), static_cast(culled), 1e-3f); +} + +VF_TEST(meshGrassIsSided) { + World world; + world.setBlock(4, 4, 4, Block::Grass); + Mesh mesh = greedyMeshChunk(world, {0, 0, 0}); + CHECK(hasTile(mesh, faceTile(Block::Grass, Face::PosY))); // top + CHECK(hasTile(mesh, faceTile(Block::Grass, Face::NegY))); // bottom + CHECK(hasTile(mesh, faceTile(Block::Grass, Face::PosX))); // side + CHECK(faceTile(Block::Grass, Face::PosY) != faceTile(Block::Grass, Face::PosX)); +} + +VF_TEST(meshEmptyChunkProducesNothing) { + World world; + world.ensureChunk({0, 0, 0}); + Mesh mesh = greedyMeshChunk(world, {0, 0, 0}); + CHECK(mesh.empty()); + CHECK_EQ(mesh.quadCount(), static_cast(0)); +}