netradiant-custom/libs/quickhull/Tests/QuickHullTests.cpp
2021-05-24 20:23:09 +03:00

280 lines
8.7 KiB
C++

#include "../QuickHull.hpp"
#include "../MathUtils.hpp"
#include <iostream>
#include <random>
#include <cassert>
namespace quickhull {
namespace tests {
using FloatType = float;
using vec3 = Vector3<FloatType>;
// Setup RNG
static std::mt19937 rng;
static std::uniform_real_distribution<> dist(0,1);
FloatType rnd(FloatType from, FloatType to) {
return from + (FloatType)dist(rng)*(to-from);
};
void assertSameValue(FloatType a, FloatType b) {
assert(std::abs(a-b)<0.0001f);
}
void testVector3() {
typedef Vector3<FloatType> vec3;
vec3 a(1,0,0);
vec3 b(1,0,0);
vec3 c = a.projection(b);
assert( (c-a).getLength()<0.00001f);
a = vec3(1,1,0);
b = vec3(1,3,0);
c = b.projection(a);
assert( (c-vec3(2,2,0)).getLength()<0.00001f);
}
template <typename T>
std::vector<Vector3<T>> createSphere(T radius, size_t M, Vector3<T> offset = Vector3<T>(0,0,0)) {
std::vector<Vector3<T>> pc;
const T pi = 3.14159f;
for (int i=0;i<=M;i++) {
FloatType y = std::sin(pi/2 + (FloatType)i/(M)*pi);
FloatType r = std::cos(pi/2 + (FloatType)i/(M)*pi);
FloatType K = FloatType(1)-std::abs((FloatType)((FloatType)i-M/2.0f))/(FloatType)(M/2.0f);
const size_t pcount = (size_t)(1 + K*M + FloatType(1)/2);
for (size_t j=0;j<pcount;j++) {
FloatType x = pcount>1 ? r*std::cos((FloatType)j/pcount*pi*2) : 0;
FloatType z = pcount>1 ? r*std::sin((FloatType)j/pcount*pi*2) : 0;
pc.emplace_back(x+offset.x,y+offset.y,z+offset.z);
}
}
return pc;
}
void sphereTest() {
QuickHull<FloatType> qh;
FloatType y = 1;
for (;;) {
auto pc = createSphere<FloatType>(1, 100, Vector3<FloatType>(0,y,0));
auto hull = qh.getConvexHull(pc,true,false);
y *= 15;
y /= 10;
if (hull.getVertexBuffer().size()==4) {
break;
}
}
// Test worst case scenario: more and more points on the unit sphere. All points should be part of the convex hull, as long as we can make epsilon smaller without
// running out of numerical accuracy.
size_t i = 1;
FloatType eps = 0.002f;
for (;;) {
auto pc = createSphere<FloatType>(1, i, Vector3<FloatType>(0,0,0));
auto hull = qh.getConvexHull(pc,true,false,eps);
std::cout << i << ":" << pc.size() << " : " << hull.getVertexBuffer().size() << " at eps=" << eps << std::endl;
if (qh.getDiagnostics().m_failedHorizonEdges) {
// This should not happen
assert(false);
break;
}
if (pc.size() == hull.getVertexBuffer().size()) {
// Fine, all the points on unit sphere do belong to the convex mesh.
i += 1;
}
else {
eps *= 0.5f;
std::cout << "Epsilon to " << eps << std::endl;
}
if (i == 500) {
break;
}
}
}
void testPlanarCase() {
QuickHull<FloatType> qh;
std::vector<vec3> pc;
pc.emplace_back(-3.000000f, -0.250000f, -0.800000f);
pc.emplace_back(-3.000000f, 0.250000f, -0.800000f);
pc.emplace_back(-3.125000f, 0.250000f, -0.750000);
pc.emplace_back(-3.125000f, -0.250000f, -0.750000);
auto hull = qh.getConvexHull(pc,true,false);
assert(hull.getIndexBuffer().size()==12);
assert(hull.getVertexBuffer().size()==4);
}
void testHalfEdgeOutput() {
QuickHull<FloatType> qh;
// 8 corner vertices of a cube + tons of vertices inside. Output should be a half edge mesh with 12 faces (6 cube faces with 2 triangles per face) and 36 half edges (3 half edges per face).
std::vector<vec3> pc;
for (int h=0;h<1000;h++) {
pc.emplace_back(rnd(-1,1),rnd(-1,1),rnd(-1,1));
}
for (int h=0;h<8;h++) {
pc.emplace_back(h&1?-2:2, h&2?-2:2, h&4?-2:2);
}
HalfEdgeMesh<FloatType, size_t> mesh = qh.getConvexHullAsMesh(&pc[0].x, pc.size(), true);
assert(mesh.m_faces.size() == 12);
assert(mesh.m_halfEdges.size() == 36);
assert(mesh.m_vertices.size() == 8);
}
void testPlanes() {
Vector3<FloatType> N(1,0,0);
Vector3<FloatType> p(2,0,0);
Plane<FloatType> P(N,p);
auto dist = mathutils::getSignedDistanceToPlane(Vector3<FloatType>(3,0,0), P);
assertSameValue(dist,1);
dist = mathutils::getSignedDistanceToPlane(Vector3<FloatType>(1,0,0), P);
assertSameValue(dist,-1);
dist = mathutils::getSignedDistanceToPlane(Vector3<FloatType>(1,0,0), P);
assertSameValue(dist,-1);
N = Vector3<FloatType>(2,0,0);
P = Plane<FloatType>(N,p);
dist = mathutils::getSignedDistanceToPlane(Vector3<FloatType>(6,0,0), P);
assertSameValue(dist,8);
}
void run() {
// Setup test env
const size_t N = 200;
std::vector<vec3> pc;
QuickHull<FloatType> qh;
ConvexHull<FloatType> hull;
// Seed RNG using Unix time
std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
auto seed = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
rng.seed((unsigned int)seed);
// Test 1 : Put N points inside unit cube. Result mesh must have exactly 8 vertices because the convex hull is the unit cube.
pc.clear();
for (int i=0;i<8;i++) {
pc.emplace_back(i&1 ? -1 : 1,i&2 ? -1 : 1,i&4 ? -1 : 1);
}
for (size_t i=0;i<N;i++)
{
pc.emplace_back(rnd(-1,1),rnd(-1,1),rnd(-1,1));
}
hull = qh.getConvexHull(pc,true,false);
assert(hull.getVertexBuffer().size()==8);
assert(hull.getIndexBuffer().size()==3*2*6); // 6 cube faces, 2 triangles per face, 3 indices per triangle
assert(&(hull.getVertexBuffer()[0])!=&(pc[0]));
auto hull2 = hull;
assert(hull2.getVertexBuffer().size()==hull.getVertexBuffer().size());
assert(hull2.getVertexBuffer()[0].x==hull.getVertexBuffer()[0].x);
assert(hull2.getIndexBuffer().size()==hull.getIndexBuffer().size());
auto hull3 = std::move(hull);
assert(hull.getIndexBuffer().size()==0);
// Test 1.1 : Same test, but using the original indices.
hull = qh.getConvexHull(pc,true,true);
assert(hull.getIndexBuffer().size()==3*2*6);
assert(hull.getVertexBuffer().size()==pc.size());
assert(&(hull.getVertexBuffer()[0])==&(pc[0]));
// Test 2 : random N points from the boundary of unit sphere. Result mesh must have exactly N points.
pc = createSphere<FloatType>(1, 50);
hull = qh.getConvexHull(pc,true,false);
assert(pc.size() == hull.getVertexBuffer().size());
hull = qh.getConvexHull(pc,true,true);
// Add every vertex twice. This should not affect final mesh
auto s = pc.size();
for (size_t i=0;i<s;i++) {
const auto& p = pc[i];
pc.push_back(p);
}
hull = qh.getConvexHull(pc,true,false);
assert(pc.size()/2 == hull.getVertexBuffer().size());
// Test 2.1 : Multiply x components of the unit sphere vectors by a huge number => essentially we get a line
const FloatType mul = 2*2*2;
while (true) {
for (auto& p : pc) {
p.x *= mul;
}
hull = qh.getConvexHull(pc,true,false);
if (hull.getVertexBuffer().size() == 4) {
break;
}
}
// Test 3: 0D
pc.clear();
vec3 centerPoint(2,2,2);
pc.push_back(centerPoint);
for (size_t i=0;i<100;i++) {
auto newp = centerPoint + vec3(rnd(-0.000001f,0.000001f),rnd(-0.000001f,0.000001f),rnd(-0.000001f,0.000001f));
pc.push_back(newp);
}
hull = qh.getConvexHull(pc,true,false);
assert(hull.getIndexBuffer().size()==12);
// Test 4: 2d degenerate case
testPlanarCase();
// Test 5: first a planar circle, then make a cylinder out of it
pc.clear();
for (size_t i=0;i<N;i++) {
const FloatType alpha = (FloatType)i/N*2*3.14159f;
pc.emplace_back(std::cos(alpha),0,std::sin(alpha));
}
hull = qh.getConvexHull(pc,true,false);
assert(hull.getVertexBuffer().size() == pc.size());
for (size_t i=0;i<N;i++) {
pc.push_back(pc[i] + vec3(0,1,0));
}
hull = qh.getConvexHull(pc,true,false);
assert(hull.getVertexBuffer().size() == pc.size());
assert(hull.getIndexBuffer().size()/3 == pc.size()*2-4);
// Test 6
for (int x=0;;x++) {
pc.clear();
const FloatType l = 1;
const FloatType r = l/(std::pow(10, x));
for (size_t i=0;i<N;i++) {
vec3 p = vec3(1,0,0)*i*l/(N-1);
FloatType a = rnd(0,2*3.1415f);
vec3 d = vec3(0,std::sin(a),std::cos(a))*r;
pc.push_back(p+d);
}
hull = qh.getConvexHull(pc,true,false);
if (hull.getVertexBuffer().size()==4) {
break;
}
}
// Test 7
for (int h=0;h<100;h++) {
pc.clear();
const vec3 v1(rnd(-1,1),rnd(-1,1),rnd(-1,1));
const vec3 v2(rnd(-1,1),rnd(-1,1),rnd(-1,1));
pc.push_back(v1);
pc.push_back(v2);
for (int i=0;i<N;i++) {
auto t1 = rnd(0,1);
auto t2 = rnd(0,1);
pc.push_back(t1*v1 + t2*v2);
}
hull = qh.getConvexHull(pc,true,false);
}
// Other tests
testPlanes();
sphereTest();
testVector3();
testHalfEdgeOutput();
std::cout << "QuickHull tests successfully passed." << std::endl;
}
}
}