255 lines
9.6 KiB
C++
255 lines
9.6 KiB
C++
|
|
/**
|
|||
|
|
* @file test_gpu_acceleration.cpp
|
|||
|
|
* @brief GPU 加速模块测试(8 项)
|
|||
|
|
*
|
|||
|
|
* 测试覆盖:
|
|||
|
|
* 1. gpu_available — 检测 CUDA 可用性
|
|||
|
|
* 2. gpu_marching_cubes — SDF → 网格(球体)
|
|||
|
|
* 3. gpu_marching_cubes — 低分辨率
|
|||
|
|
* 4. gpu_marching_cubes — 立方体 SDF
|
|||
|
|
* 5. gpu_mesh_simplify — 基本简化
|
|||
|
|
* 6. gpu_mesh_simplify — 微小 target_ratio
|
|||
|
|
* 7. gpu_boolean_intersect — 两个相交球体
|
|||
|
|
* 8. gpu_boolean_intersect — 不相交网格
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
#include "vde/gpu/gpu_acceleration.h"
|
|||
|
|
#include "vde/mesh/marching_cubes.h"
|
|||
|
|
#include "vde/mesh/halfedge_mesh.h"
|
|||
|
|
#include <gtest/gtest.h>
|
|||
|
|
#include <cmath>
|
|||
|
|
#include <memory>
|
|||
|
|
|
|||
|
|
using namespace vde::gpu;
|
|||
|
|
using vde::core::AABB3D;
|
|||
|
|
using vde::core::Point3D;
|
|||
|
|
using vde::core::Vector3D;
|
|||
|
|
using vde::mesh::HalfedgeMesh;
|
|||
|
|
|
|||
|
|
// ── 帮助函数 ─────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
/// 创建一个简单球体 SDF 函数
|
|||
|
|
static auto make_sphere_sdf(double cx, double cy, double cz, double r) {
|
|||
|
|
return [=](double x, double y, double z) -> double {
|
|||
|
|
return std::sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy) + (z - cz) * (z - cz)) - r;
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 创建立方体 SDF 函数
|
|||
|
|
static auto make_box_sdf(double hx, double hy, double hz) {
|
|||
|
|
return [=](double x, double y, double z) -> double {
|
|||
|
|
double dx = std::abs(x) - hx, dy = std::abs(y) - hy, dz = std::abs(z) - hz;
|
|||
|
|
return std::sqrt(std::max(dx, 0.0) * std::max(dx, 0.0) +
|
|||
|
|
std::max(dy, 0.0) * std::max(dy, 0.0) +
|
|||
|
|
std::max(dz, 0.0) * std::max(dz, 0.0)) +
|
|||
|
|
std::min(std::max({dx, dy, dz}), 0.0);
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 快速创建简单三角网格(正四面体)
|
|||
|
|
static HalfedgeMesh make_tetrahedron(double ox, double oy, double oz, double s) {
|
|||
|
|
HalfedgeMesh mesh;
|
|||
|
|
std::vector<Point3D> verts = {
|
|||
|
|
{ox, oy + s, oz},
|
|||
|
|
{ox - s * 0.866, oy, oz - s * 0.5},
|
|||
|
|
{ox + s * 0.866, oy, oz - s * 0.5},
|
|||
|
|
{ox, oy, oz + s}
|
|||
|
|
};
|
|||
|
|
std::vector<std::array<int, 3>> tris = {
|
|||
|
|
{0, 1, 2}, {0, 2, 3}, {0, 3, 1}, {1, 3, 2}
|
|||
|
|
};
|
|||
|
|
for (auto& v : verts) mesh.add_vertex(v);
|
|||
|
|
for (auto& t : tris) mesh.add_face({t[0], t[1], t[2]});
|
|||
|
|
return mesh;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 验证网格基本有效性
|
|||
|
|
static void expect_valid_mesh(const HalfedgeMesh& mesh) {
|
|||
|
|
EXPECT_GT(mesh.num_vertices(), 0u);
|
|||
|
|
EXPECT_GT(mesh.num_faces(), 0u);
|
|||
|
|
EXPECT_GT(mesh.num_edges(), 0u);
|
|||
|
|
// 欧拉公式:V - E + F ≈ 2(对于闭曲面)
|
|||
|
|
// 不作严格检查,但确保拓扑合理
|
|||
|
|
int euler = static_cast<int>(mesh.num_vertices()) -
|
|||
|
|
static_cast<int>(mesh.num_edges()) +
|
|||
|
|
static_cast<int>(mesh.num_faces());
|
|||
|
|
EXPECT_GE(euler, 0) << "Euler characteristic should be non-negative";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ======================================================================
|
|||
|
|
// 测试 1:gpu_available
|
|||
|
|
// ======================================================================
|
|||
|
|
TEST(GpuAcceleration, GpuAvailable) {
|
|||
|
|
// 无论 CUDA 是否可用,函数必须不崩溃并返回布尔值
|
|||
|
|
bool avail = gpu_available();
|
|||
|
|
EXPECT_TRUE(avail == true || avail == false);
|
|||
|
|
#if VDE_USE_CUDA
|
|||
|
|
// CUDA 编译时开启 → 运行时至少应无错误
|
|||
|
|
// (CUDA 设备可能为 0,但调用不崩溃)
|
|||
|
|
SUCCEED() << "CUDA compiled in, runtime detection: " << (avail ? "YES" : "NO");
|
|||
|
|
#else
|
|||
|
|
EXPECT_FALSE(avail) << "Without CUDA build, gpu_available must return false";
|
|||
|
|
#endif
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ======================================================================
|
|||
|
|
// 测试 2:gpu_marching_cubes — 球体 SDF
|
|||
|
|
// ======================================================================
|
|||
|
|
TEST(GpuAcceleration, MarchingCubesSphere) {
|
|||
|
|
auto sdf = make_sphere_sdf(0, 0, 0, 1.0);
|
|||
|
|
AABB3D bounds({-1.5, -1.5, -1.5}, {1.5, 1.5, 1.5});
|
|||
|
|
|
|||
|
|
HalfedgeMesh mesh = gpu_marching_cubes(sdf, bounds, 32);
|
|||
|
|
|
|||
|
|
expect_valid_mesh(mesh);
|
|||
|
|
|
|||
|
|
// 球体应当构成闭曲面
|
|||
|
|
EXPECT_GE(mesh.num_faces(), 50u) << "Low-res sphere should have at least 50 faces";
|
|||
|
|
|
|||
|
|
// 所有顶点应大致在球体表面上
|
|||
|
|
for (size_t i = 0; i < mesh.num_vertices(); ++i) {
|
|||
|
|
const auto& p = mesh.vertex(i);
|
|||
|
|
double dist = std::sqrt(p.x() * p.x() + p.y() * p.y() + p.z() * p.z());
|
|||
|
|
EXPECT_NEAR(dist, 1.0, 0.15) << "Vertex " << i << " distance from origin";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ======================================================================
|
|||
|
|
// 测试 3:gpu_marching_cubes — 低分辨率
|
|||
|
|
// ======================================================================
|
|||
|
|
TEST(GpuAcceleration, MarchingCubesLowRes) {
|
|||
|
|
auto sdf = make_sphere_sdf(0, 0, 0, 1.0);
|
|||
|
|
AABB3D bounds({-1.2, -1.2, -1.2}, {1.2, 1.2, 1.2});
|
|||
|
|
|
|||
|
|
HalfedgeMesh mesh = gpu_marching_cubes(sdf, bounds, 8);
|
|||
|
|
|
|||
|
|
// 极低分辨率仍有输出
|
|||
|
|
EXPECT_GT(mesh.num_vertices(), 0u);
|
|||
|
|
EXPECT_GT(mesh.num_faces(), 0u);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ======================================================================
|
|||
|
|
// 测试 4:gpu_marching_cubes — 立方体 SDF
|
|||
|
|
// ======================================================================
|
|||
|
|
TEST(GpuAcceleration, MarchingCubesBox) {
|
|||
|
|
auto sdf = make_box_sdf(1.0, 0.5, 0.3);
|
|||
|
|
AABB3D bounds({-1.5, -1.0, -0.8}, {1.5, 1.0, 0.8});
|
|||
|
|
|
|||
|
|
HalfedgeMesh mesh = gpu_marching_cubes(sdf, bounds, 24);
|
|||
|
|
|
|||
|
|
expect_valid_mesh(mesh);
|
|||
|
|
|
|||
|
|
// 检查包围盒尺寸与预期一致
|
|||
|
|
auto bb = mesh.bounds();
|
|||
|
|
double max_dim = std::max({bb.extent().x(), bb.extent().y(), bb.extent().z()});
|
|||
|
|
EXPECT_GT(max_dim, 0.5);
|
|||
|
|
EXPECT_LT(max_dim, 4.0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ======================================================================
|
|||
|
|
// 测试 5:gpu_mesh_simplify — 基本简化
|
|||
|
|
// ======================================================================
|
|||
|
|
TEST(GpuAcceleration, MeshSimplifyBasic) {
|
|||
|
|
auto sdf = make_sphere_sdf(0, 0, 0, 1.0);
|
|||
|
|
AABB3D bounds({-1.5, -1.5, -1.5}, {1.5, 1.5, 1.5});
|
|||
|
|
|
|||
|
|
HalfedgeMesh original = gpu_marching_cubes(sdf, bounds, 32);
|
|||
|
|
size_t orig_faces = original.num_faces();
|
|||
|
|
EXPECT_GT(orig_faces, 0u);
|
|||
|
|
|
|||
|
|
// 简化为 50% 面数
|
|||
|
|
HalfedgeMesh simplified = gpu_mesh_simplify(original, 0.5f);
|
|||
|
|
|
|||
|
|
expect_valid_mesh(simplified);
|
|||
|
|
|
|||
|
|
// 简化后面数应减少
|
|||
|
|
EXPECT_LE(simplified.num_faces(), orig_faces)
|
|||
|
|
<< "Simplified mesh should have ≤ original face count";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ======================================================================
|
|||
|
|
// 测试 6:gpu_mesh_simplify — 极低 target_ratio
|
|||
|
|
// ======================================================================
|
|||
|
|
TEST(GpuAcceleration, MeshSimplifyTinyRatio) {
|
|||
|
|
auto sdf = make_sphere_sdf(0, 0, 0, 1.0);
|
|||
|
|
AABB3D bounds({-1.5, -1.5, -1.5}, {1.5, 1.5, 1.5});
|
|||
|
|
|
|||
|
|
HalfedgeMesh original = gpu_marching_cubes(sdf, bounds, 20);
|
|||
|
|
ASSERT_GT(original.num_faces(), 10u);
|
|||
|
|
|
|||
|
|
// target_ratio = 0.1 (极低比例)
|
|||
|
|
HalfedgeMesh simplified = gpu_mesh_simplify(original, 0.1f);
|
|||
|
|
|
|||
|
|
// 不应崩溃且至少保留一些面
|
|||
|
|
EXPECT_GT(simplified.num_vertices(), 3u);
|
|||
|
|
EXPECT_GT(simplified.num_faces(), 1u);
|
|||
|
|
EXPECT_LT(simplified.num_faces(), original.num_faces());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ======================================================================
|
|||
|
|
// 测试 7:gpu_boolean_intersect — 两个相交球体
|
|||
|
|
// ======================================================================
|
|||
|
|
TEST(GpuAcceleration, BooleanIntersectIntersecting) {
|
|||
|
|
// 两个相交球体(中心相距 1.0,半径各 1.2)
|
|||
|
|
auto sdf_a = make_sphere_sdf(-0.5, 0, 0, 1.2);
|
|||
|
|
auto sdf_b = make_sphere_sdf(0.5, 0, 0, 1.2);
|
|||
|
|
AABB3D bounds({-2.0, -1.5, -1.5}, {2.0, 1.5, 1.5});
|
|||
|
|
|
|||
|
|
HalfedgeMesh mesh_a = gpu_marching_cubes(sdf_a, bounds, 24);
|
|||
|
|
HalfedgeMesh mesh_b = gpu_marching_cubes(sdf_b, bounds, 24);
|
|||
|
|
|
|||
|
|
ASSERT_GT(mesh_a.num_faces(), 0u);
|
|||
|
|
ASSERT_GT(mesh_b.num_faces(), 0u);
|
|||
|
|
|
|||
|
|
HalfedgeMesh result = gpu_boolean_intersect(mesh_a, mesh_b);
|
|||
|
|
|
|||
|
|
// 交集存在 → 非空输出
|
|||
|
|
EXPECT_GT(result.num_vertices(), 0u) << "Intersection of overlapping spheres should not be empty";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ======================================================================
|
|||
|
|
// 测试 8:gpu_boolean_intersect — 不相交网格
|
|||
|
|
// ======================================================================
|
|||
|
|
TEST(GpuAcceleration, BooleanIntersectDisjoint) {
|
|||
|
|
// 两个远距离球体(中心相距 10,半径各 1)
|
|||
|
|
auto sdf_a = make_sphere_sdf(-5, 0, 0, 1.0);
|
|||
|
|
auto sdf_b = make_sphere_sdf(5, 0, 0, 1.0);
|
|||
|
|
AABB3D bounds({-6.5, -1.5, -1.5}, {6.5, 1.5, 1.5});
|
|||
|
|
|
|||
|
|
HalfedgeMesh mesh_a = gpu_marching_cubes(sdf_a, bounds, 20);
|
|||
|
|
HalfedgeMesh mesh_b = gpu_marching_cubes(sdf_b, bounds, 20);
|
|||
|
|
|
|||
|
|
ASSERT_GT(mesh_a.num_faces(), 0u);
|
|||
|
|
ASSERT_GT(mesh_b.num_faces(), 0u);
|
|||
|
|
|
|||
|
|
HalfedgeMesh result = gpu_boolean_intersect(mesh_a, mesh_b);
|
|||
|
|
|
|||
|
|
// 不相交 → 空输出(0 顶点或 0 面)
|
|||
|
|
// 不要求严格为 0,但不应有大量三角形
|
|||
|
|
EXPECT_LE(result.num_faces(), mesh_a.num_faces() / 2u)
|
|||
|
|
<< "Disjoint meshes should have few or no intersection faces";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ======================================================================
|
|||
|
|
// 额外测试:空网格安全
|
|||
|
|
// ======================================================================
|
|||
|
|
TEST(GpuAcceleration, EmptyMeshSafety) {
|
|||
|
|
HalfedgeMesh empty;
|
|||
|
|
|
|||
|
|
// simplify 空网格不崩溃
|
|||
|
|
HalfedgeMesh r1 = gpu_mesh_simplify(empty, 0.5f);
|
|||
|
|
EXPECT_EQ(r1.num_vertices(), 0u);
|
|||
|
|
EXPECT_EQ(r1.num_faces(), 0u);
|
|||
|
|
|
|||
|
|
// boolean intersect 空网格不崩溃
|
|||
|
|
auto sdf = make_sphere_sdf(0, 0, 0, 1.0);
|
|||
|
|
AABB3D bounds({-1.5, -1.5, -1.5}, {1.5, 1.5, 1.5});
|
|||
|
|
HalfedgeMesh sphere = gpu_marching_cubes(sdf, bounds, 16);
|
|||
|
|
|
|||
|
|
HalfedgeMesh r2 = gpu_boolean_intersect(empty, sphere);
|
|||
|
|
EXPECT_EQ(r2.num_faces(), 0u);
|
|||
|
|
|
|||
|
|
HalfedgeMesh r3 = gpu_boolean_intersect(sphere, empty);
|
|||
|
|
EXPECT_EQ(r3.num_faces(), 0u);
|
|||
|
|
}
|