feat: LOD mesh generation + incremental BVH
This commit is contained in:
@@ -3,3 +3,4 @@ add_vde_test(test_delaunay)
|
||||
add_vde_test(test_quality)
|
||||
add_vde_test(test_smooth)
|
||||
add_vde_test(test_delaunay_3d)
|
||||
add_vde_test(test_mesh_lod)
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/mesh/mesh_lod.h"
|
||||
#include "vde/mesh/halfedge_mesh.h"
|
||||
#include <cmath>
|
||||
|
||||
using namespace vde::mesh;
|
||||
|
||||
/// Create a simple box mesh (12 triangles, 8 vertices)
|
||||
static HalfedgeMesh make_box_mesh() {
|
||||
// Unit cube vertices
|
||||
std::vector<Point3D> verts = {
|
||||
{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}, // bottom
|
||||
{0, 0, 1}, {1, 0, 1}, {1, 1, 1}, {0, 1, 1}, // top
|
||||
};
|
||||
// 12 triangles (2 per face)
|
||||
std::vector<std::array<int, 3>> tris = {
|
||||
{0, 2, 1}, {0, 3, 2}, // front
|
||||
{1, 2, 6}, {1, 6, 5}, // right
|
||||
{5, 6, 7}, {5, 7, 4}, // back
|
||||
{4, 7, 3}, {4, 3, 0}, // left
|
||||
{3, 7, 6}, {3, 6, 2}, // top
|
||||
{4, 0, 1}, {4, 1, 5}, // bottom
|
||||
};
|
||||
|
||||
HalfedgeMesh mesh;
|
||||
mesh.build_from_triangles(verts, tris);
|
||||
return mesh;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// LOD generation
|
||||
// ──────────────────────────────────────────────────────
|
||||
|
||||
TEST(LODMeshTest, GenerateLOD_DefaultLevels) {
|
||||
auto box = make_box_mesh();
|
||||
auto lod = generate_lod(box, 3, 0.5);
|
||||
|
||||
// Default: 3 levels (base + 2 simplified)
|
||||
EXPECT_EQ(lod.levels.size(), 3u);
|
||||
EXPECT_EQ(lod.thresholds.size(), 3u);
|
||||
|
||||
// Level 0 is the original mesh (12 faces)
|
||||
EXPECT_GE(lod.levels[0].num_faces(), 8u);
|
||||
// Each level should have ≤ faces than previous
|
||||
EXPECT_LE(lod.levels[1].num_faces(), lod.levels[0].num_faces());
|
||||
EXPECT_LE(lod.levels[2].num_faces(), lod.levels[1].num_faces());
|
||||
}
|
||||
|
||||
TEST(LODMeshTest, GenerateLOD_SingleLevel) {
|
||||
auto box = make_box_mesh();
|
||||
auto lod = generate_lod(box, 1, 0.5);
|
||||
|
||||
// Only 1 level = base (no simplification)
|
||||
EXPECT_EQ(lod.levels.size(), 1u);
|
||||
EXPECT_EQ(lod.levels[0].num_faces(), 12u);
|
||||
}
|
||||
|
||||
TEST(LODMeshTest, GenerateLOD_AggressiveReduction) {
|
||||
auto box = make_box_mesh();
|
||||
auto lod = generate_lod(box, 5, 0.1); // keep only 10% each level
|
||||
|
||||
// Should stop early when < 4 faces remain
|
||||
EXPECT_GE(lod.levels.size(), 1u);
|
||||
EXPECT_LE(lod.levels.size(), 5u);
|
||||
}
|
||||
|
||||
TEST(LODMeshTest, GenerateLOD_EmptyMesh) {
|
||||
HalfedgeMesh empty;
|
||||
auto lod = generate_lod(empty, 3, 0.5);
|
||||
|
||||
EXPECT_EQ(lod.levels.size(), 1u); // only base
|
||||
EXPECT_EQ(lod.levels[0].num_faces(), 0u);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// level_for_error
|
||||
// ──────────────────────────────────────────────────────
|
||||
|
||||
TEST(LODMeshTest, LevelForError_ZeroError) {
|
||||
auto box = make_box_mesh();
|
||||
auto lod = generate_lod(box, 3, 0.5);
|
||||
|
||||
// error 0 → base level (threshold 0.0)
|
||||
EXPECT_EQ(lod.level_for_error(0.0), 0);
|
||||
}
|
||||
|
||||
TEST(LODMeshTest, LevelForError_ThresholdBoundary) {
|
||||
auto box = make_box_mesh();
|
||||
auto lod = generate_lod(box, 3, 0.5);
|
||||
|
||||
// thresholds: [0.0, 100.0, 200.0]
|
||||
EXPECT_EQ(lod.level_for_error(50.0), 0);
|
||||
EXPECT_EQ(lod.level_for_error(100.0), 1); // ≥ 100 → level 1
|
||||
EXPECT_EQ(lod.level_for_error(150.0), 1);
|
||||
EXPECT_EQ(lod.level_for_error(200.0), 2); // ≥ 200 → level 2
|
||||
EXPECT_EQ(lod.level_for_error(999.0), 2);
|
||||
}
|
||||
|
||||
TEST(LODMeshTest, LevelForError_NegativeError) {
|
||||
auto box = make_box_mesh();
|
||||
auto lod = generate_lod(box, 3, 0.5);
|
||||
|
||||
// Negative error → level 0 (base)
|
||||
EXPECT_EQ(lod.level_for_error(-1.0), 0);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// mesh_at
|
||||
// ──────────────────────────────────────────────────────
|
||||
|
||||
TEST(LODMeshTest, MeshAt_ValidLevels) {
|
||||
auto box = make_box_mesh();
|
||||
auto lod = generate_lod(box, 3, 0.5);
|
||||
|
||||
EXPECT_EQ(lod.mesh_at(0).num_faces(), 12u);
|
||||
EXPECT_LE(lod.mesh_at(1).num_faces(), 12u);
|
||||
EXPECT_LE(lod.mesh_at(2).num_faces(), lod.mesh_at(1).num_faces());
|
||||
}
|
||||
|
||||
TEST(LODMeshTest, MeshAt_ClampNegative) {
|
||||
auto box = make_box_mesh();
|
||||
auto lod = generate_lod(box, 3, 0.5);
|
||||
|
||||
// Negative level → clamped to 0
|
||||
EXPECT_EQ(lod.mesh_at(-1).num_faces(), lod.mesh_at(0).num_faces());
|
||||
EXPECT_EQ(lod.mesh_at(-100).num_faces(), lod.mesh_at(0).num_faces());
|
||||
}
|
||||
|
||||
TEST(LODMeshTest, MeshAt_ClampTooHigh) {
|
||||
auto box = make_box_mesh();
|
||||
auto lod = generate_lod(box, 3, 0.5);
|
||||
|
||||
int last = static_cast<int>(lod.levels.size()) - 1;
|
||||
EXPECT_EQ(lod.mesh_at(999).num_faces(), lod.mesh_at(last).num_faces());
|
||||
}
|
||||
|
||||
TEST(LODMeshTest, MeshAt_EmptyLOD) {
|
||||
LODMesh empty_lod;
|
||||
// mesh_at returns a static empty mesh for empty LOD
|
||||
EXPECT_EQ(empty_lod.mesh_at(0).num_faces(), 0u);
|
||||
EXPECT_EQ(empty_lod.mesh_at(5).num_faces(), 0u);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// generate_brep_lod (integration)
|
||||
// ──────────────────────────────────────────────────────
|
||||
|
||||
// Forward-declare for linking only (no brep::make_box in this TU's link deps)
|
||||
// This test is compile-time only; runtime needs brep linked.
|
||||
// We test through the LOD API directly above; brep path is tested
|
||||
// in the brep test suite.
|
||||
Reference in New Issue
Block a user