feat: LOD mesh generation + incremental BVH
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
#include "vde/mesh/halfedge_mesh.h"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace vde::brep { class BrepModel; }
|
||||
|
||||
namespace vde::mesh {
|
||||
|
||||
/**
|
||||
* @brief Level-of-detail mesh with multiple detail levels
|
||||
*
|
||||
* Stores a sequence of simplified meshes from highest detail (index 0)
|
||||
* to lowest. Screen-space error thresholds determine which level to
|
||||
* use for rendering.
|
||||
*
|
||||
* @ingroup mesh
|
||||
*/
|
||||
struct LODMesh {
|
||||
std::string name;
|
||||
/// Meshes from highest detail (index 0) to lowest
|
||||
std::vector<HalfedgeMesh> levels;
|
||||
/// Screen-space error thresholds for each level (in pixels)
|
||||
std::vector<double> thresholds;
|
||||
|
||||
/**
|
||||
* @brief Get the appropriate LOD level for a given screen-space error
|
||||
*
|
||||
* Selects the lowest-detail level whose threshold is ≤ error.
|
||||
* Walks thresholds from coarsest to finest.
|
||||
*
|
||||
* @param error Screen-space error in pixels
|
||||
* @return LOD level index (0 = highest detail)
|
||||
*/
|
||||
[[nodiscard]] int level_for_error(double error) const;
|
||||
|
||||
/**
|
||||
* @brief Get the mesh at a specific LOD level (clamped)
|
||||
*
|
||||
* @param level Desired LOD level
|
||||
* @return Reference to the mesh at that level
|
||||
*/
|
||||
[[nodiscard]] const HalfedgeMesh& mesh_at(int level) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Generate LOD meshes from a high-detail mesh
|
||||
*
|
||||
* Iteratively applies QEM simplification, reducing face count
|
||||
* by `reduction_ratio` at each level.
|
||||
*
|
||||
* @param mesh High-detail input mesh
|
||||
* @param num_levels Number of LOD levels (including base)
|
||||
* @param reduction_ratio Ratio of faces to keep at each level (0.5 = half)
|
||||
* @return LOD meshes with thresholds proportional to level index
|
||||
*
|
||||
* @code{.cpp}
|
||||
* auto lod = generate_lod(high_res, 4, 0.5);
|
||||
* int level = lod.level_for_error(screen_error);
|
||||
* render(lod.mesh_at(level));
|
||||
* @endcode
|
||||
*/
|
||||
[[nodiscard]] LODMesh generate_lod(const HalfedgeMesh& mesh,
|
||||
int num_levels = 3, double reduction_ratio = 0.5);
|
||||
|
||||
/**
|
||||
* @brief Generate LOD from B-Rep body (tessellate + simplify)
|
||||
*
|
||||
* Tessellates the B-Rep body with default deflection, then
|
||||
* generates LOD levels via QEM simplification.
|
||||
*
|
||||
* @param body B-Rep model
|
||||
* @param num_levels Number of LOD levels
|
||||
* @return LOD meshes
|
||||
*/
|
||||
[[nodiscard]] LODMesh generate_brep_lod(const brep::BrepModel& body,
|
||||
int num_levels = 3);
|
||||
|
||||
} // namespace vde::mesh
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
#include "vde/spatial/bvh.h"
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace vde::spatial {
|
||||
|
||||
/**
|
||||
* @brief Incremental BVH that supports insert/remove without full rebuild
|
||||
*
|
||||
* Wraps the standard BVH with a dirty flag. Triangles can be inserted,
|
||||
* removed, or updated incrementally. The tree is lazily rebuilt on the
|
||||
* next query when dirty. Suitable for moderately dynamic scenes where
|
||||
* queries are less frequent than modifications.
|
||||
*
|
||||
* @ingroup spatial
|
||||
*/
|
||||
class IncrementalBVH {
|
||||
public:
|
||||
/// @brief Build from initial set of triangles
|
||||
void build(const std::vector<core::Triangle3D>& triangles);
|
||||
|
||||
/// @brief Insert a new triangle (marks dirty, no immediate rebuild)
|
||||
void insert(const core::Triangle3D& tri);
|
||||
|
||||
/// @brief Remove a triangle by index (marks dirty)
|
||||
void remove(size_t index);
|
||||
|
||||
/// @brief Update a triangle's geometry in-place (marks dirty)
|
||||
void update(size_t index, const core::Triangle3D& tri);
|
||||
|
||||
/**
|
||||
* @brief Ray query (rebuilds if dirty, then delegates to BVH)
|
||||
*
|
||||
* @param ray Query ray
|
||||
* @return All hit results (t, triangle, hit point)
|
||||
*/
|
||||
std::vector<BVH::HitResult> ray_query(const Ray3Dd& ray) const;
|
||||
|
||||
/// @brief Number of triangles
|
||||
[[nodiscard]] size_t size() const { return triangles_.size(); }
|
||||
|
||||
private:
|
||||
std::vector<core::Triangle3D> triangles_;
|
||||
std::vector<core::AABB3D> bboxes_;
|
||||
BVH bvh_;
|
||||
bool dirty_ = true;
|
||||
|
||||
void rebuild_if_dirty();
|
||||
};
|
||||
|
||||
} // namespace vde::spatial
|
||||
@@ -72,6 +72,7 @@ add_library(vde_mesh STATIC
|
||||
mesh/geodesic.cpp
|
||||
mesh/mesh_curvature.cpp
|
||||
mesh/marching_cubes.cpp
|
||||
mesh/mesh_lod.cpp
|
||||
)
|
||||
target_include_directories(vde_mesh
|
||||
PUBLIC ${CMAKE_SOURCE_DIR}/include
|
||||
@@ -87,6 +88,7 @@ add_library(vde_spatial STATIC
|
||||
spatial/octree.cpp
|
||||
spatial/kd_tree.cpp
|
||||
spatial/r_tree.cpp
|
||||
spatial/incremental_bvh.cpp
|
||||
)
|
||||
target_include_directories(vde_spatial
|
||||
PUBLIC ${CMAKE_SOURCE_DIR}/include
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
#include "vde/mesh/mesh_lod.h"
|
||||
#include "vde/mesh/mesh_simplify.h"
|
||||
#include "vde/brep/brep.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace vde::mesh {
|
||||
|
||||
LODMesh generate_lod(const HalfedgeMesh& mesh, int num_levels, double reduction_ratio) {
|
||||
LODMesh result;
|
||||
result.name = "lod";
|
||||
result.levels.push_back(mesh);
|
||||
result.thresholds.push_back(0.0); // base level: always use
|
||||
|
||||
HalfedgeMesh current = mesh;
|
||||
for (int i = 1; i < num_levels; ++i) {
|
||||
int current_faces = static_cast<int>(current.num_faces());
|
||||
int target_faces = static_cast<int>(current_faces * reduction_ratio);
|
||||
if (target_faces < 4) break;
|
||||
|
||||
SimplifyOptions opts;
|
||||
opts.target_ratio = static_cast<double>(target_faces) / current_faces;
|
||||
opts.max_iterations = 100;
|
||||
opts.preserve_boundary = true;
|
||||
|
||||
current = simplify_mesh(current, opts);
|
||||
result.levels.push_back(current);
|
||||
result.thresholds.push_back(static_cast<double>(i) * 100.0);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
LODMesh generate_brep_lod(const brep::BrepModel& body, int num_levels) {
|
||||
auto mesh = body.to_mesh(0.1);
|
||||
return generate_lod(mesh, num_levels);
|
||||
}
|
||||
|
||||
int LODMesh::level_for_error(double error) const {
|
||||
for (int i = static_cast<int>(thresholds.size()) - 1; i >= 0; --i) {
|
||||
if (error >= thresholds[i]) return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const HalfedgeMesh& LODMesh::mesh_at(int level) const {
|
||||
if (levels.empty()) {
|
||||
static HalfedgeMesh empty;
|
||||
return empty;
|
||||
}
|
||||
if (level < 0) level = 0;
|
||||
if (level >= static_cast<int>(levels.size())) level = static_cast<int>(levels.size()) - 1;
|
||||
return levels[level];
|
||||
}
|
||||
|
||||
} // namespace vde::mesh
|
||||
@@ -0,0 +1,115 @@
|
||||
#include "vde/spatial/incremental_bvh.h"
|
||||
#include "vde/core/line.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
namespace vde::spatial {
|
||||
|
||||
using core::Point3D;
|
||||
using core::Vector3D;
|
||||
|
||||
namespace {
|
||||
|
||||
/// Compute AABB from triangle vertices
|
||||
AABB3D triangle_bounds(const Triangle3D& tri) {
|
||||
double min_x = std::min({tri.v(0).x(), tri.v(1).x(), tri.v(2).x()});
|
||||
double min_y = std::min({tri.v(0).y(), tri.v(1).y(), tri.v(2).y()});
|
||||
double min_z = std::min({tri.v(0).z(), tri.v(1).z(), tri.v(2).z()});
|
||||
double max_x = std::max({tri.v(0).x(), tri.v(1).x(), tri.v(2).x()});
|
||||
double max_y = std::max({tri.v(0).y(), tri.v(1).y(), tri.v(2).y()});
|
||||
double max_z = std::max({tri.v(0).z(), tri.v(1).z(), tri.v(2).z()});
|
||||
return AABB3D(Point3D(min_x, min_y, min_z), Point3D(max_x, max_y, max_z));
|
||||
}
|
||||
|
||||
/// Möller–Trumbore ray-triangle intersection
|
||||
bool mt_intersect(const Ray3Dd& ray, const Triangle3D& tri,
|
||||
double& t_out, Point3D& point_out) {
|
||||
const auto& v0 = tri.v(0);
|
||||
const auto& v1 = tri.v(1);
|
||||
const auto& v2 = tri.v(2);
|
||||
|
||||
Vector3D e1 = v1 - v0;
|
||||
Vector3D e2 = v2 - v0;
|
||||
Vector3D pvec = ray.direction().cross(e2);
|
||||
double det = e1.dot(pvec);
|
||||
|
||||
if (std::abs(det) < 1e-12) return false;
|
||||
|
||||
double inv_det = 1.0 / det;
|
||||
Vector3D tvec = ray.origin() - v0;
|
||||
double u = tvec.dot(pvec) * inv_det;
|
||||
if (u < 0.0 || u > 1.0) return false;
|
||||
|
||||
Vector3D qvec = tvec.cross(e1);
|
||||
double v = ray.direction().dot(qvec) * inv_det;
|
||||
if (v < 0.0 || u + v > 1.0) return false;
|
||||
|
||||
t_out = e2.dot(qvec) * inv_det;
|
||||
if (t_out <= 0.0) return false;
|
||||
|
||||
point_out = ray.origin() + ray.direction() * t_out;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void IncrementalBVH::build(const std::vector<Triangle3D>& triangles) {
|
||||
triangles_ = triangles;
|
||||
bboxes_.resize(triangles.size());
|
||||
for (size_t i = 0; i < triangles.size(); ++i) {
|
||||
bboxes_[i] = triangle_bounds(triangles_[i]);
|
||||
}
|
||||
bvh_.build(triangles_);
|
||||
dirty_ = false;
|
||||
}
|
||||
|
||||
void IncrementalBVH::insert(const Triangle3D& tri) {
|
||||
triangles_.push_back(tri);
|
||||
bboxes_.push_back(triangle_bounds(tri));
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void IncrementalBVH::remove(size_t index) {
|
||||
if (index < triangles_.size()) {
|
||||
triangles_.erase(triangles_.begin() + static_cast<ptrdiff_t>(index));
|
||||
bboxes_.erase(bboxes_.begin() + static_cast<ptrdiff_t>(index));
|
||||
dirty_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void IncrementalBVH::update(size_t index, const Triangle3D& tri) {
|
||||
if (index < triangles_.size()) {
|
||||
triangles_[index] = tri;
|
||||
bboxes_[index] = triangle_bounds(tri);
|
||||
dirty_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void IncrementalBVH::rebuild_if_dirty() {
|
||||
if (dirty_ && !triangles_.empty()) {
|
||||
bvh_ = BVH();
|
||||
bvh_.build(triangles_);
|
||||
dirty_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<BVH::HitResult> IncrementalBVH::ray_query(const Ray3Dd& ray) const {
|
||||
// Rebuild tree if modifications have occurred since last query
|
||||
const_cast<IncrementalBVH*>(this)->rebuild_if_dirty();
|
||||
|
||||
// Use BVH to find candidate triangles
|
||||
auto candidates = bvh_.query_ray(ray);
|
||||
|
||||
// Compute exact intersections for each candidate
|
||||
std::vector<BVH::HitResult> hits;
|
||||
for (const auto& tri : candidates) {
|
||||
double t;
|
||||
Point3D point;
|
||||
if (mt_intersect(ray, tri, t, point)) {
|
||||
hits.push_back({t, tri, point});
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
} // namespace vde::spatial
|
||||
@@ -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