// Test: Dual Contouring mesh generation.
// Build:
// g++ -std=c++17 -O2 -I src test/test_contour.cpp src/dc_contour.cpp \
// src/dc_octree.cpp src/dc_qef.cpp -o test/test_contour -lm
#include "dc_contour.h"
#include "dc_octree.h"
#include <cmath>
#include <cstdio>
#include <cstdlib>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, msg) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf(" FAIL: %s\n", msg); } \
} while(0)
// ---------------------------------------------------------------------------
// Density functions
// ---------------------------------------------------------------------------
// Flat plane at y = threshold. density = -(y - threshold)
// Positive below (solid), negative above (air).
static float density_plane(float x, float y, float z, void* userdata) {
float threshold = *(float*)userdata;
(void)x; (void)z;
return -(y - threshold);
}
// Sphere: density = radius - sqrt(x^2 + y^2 + z^2)
// Positive inside (solid), negative outside (air).
struct SphereParams { float cx, cy, cz, radius; };
static float density_sphere(float x, float y, float z, void* userdata) {
SphereParams* p = (SphereParams*)userdata;
float dx = x - p->cx, dy = y - p->cy, dz = z - p->cz;
return p->radius - std::sqrt(dx*dx + dy*dy + dz*dz);
}
// ---------------------------------------------------------------------------
// Helper: compute triangle area from positions array
// ---------------------------------------------------------------------------
static float triangle_area(const float* positions, int i0, int i1, int i2) {
float ax = positions[i1*3+0] - positions[i0*3+0];
float ay = positions[i1*3+1] - positions[i0*3+1];
float az = positions[i1*3+2] - positions[i0*3+2];
float bx = positions[i2*3+0] - positions[i0*3+0];
float by = positions[i2*3+1] - positions[i0*3+1];
float bz = positions[i2*3+2] - positions[i0*3+2];
// cross product magnitude / 2
float cx = ay*bz - az*by;
float cy = az*bx - ax*bz;
float cz = ax*by - ay*bx;
return 0.5f * std::sqrt(cx*cx + cy*cy + cz*cz);
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
static void test_flat_plane() {
printf("Test: Flat plane at y=5...\n");
float threshold = 5.0f;
int min[3] = {0, 0, 0};
int size = 16;
float voxel_scale = 1.0f;
float origin[3] = {0.0f, 0.0f, 0.0f};
OctreeNode* root = construct_octree(min, size, voxel_scale, origin,
density_plane, &threshold);
CHECK(root != nullptr, "octree should have sign changes");
DCMeshData mesh;
dc_generate_mesh(root, voxel_scale, origin, density_plane, &threshold, mesh);
int vert_count = (int)(mesh.positions.size() / 3);
int tri_count = (int)(mesh.indices.size() / 3);
printf(" vertices: %d, triangles: %d\n", vert_count, tri_count);
CHECK(vert_count > 0, "should produce vertices");
CHECK(tri_count > 0, "should produce triangles");
// All vertex y-coordinates should be near threshold (5.0).
float max_y_error = 0.0f;
for (int i = 0; i < vert_count; i++) {
float y = mesh.positions[i*3 + 1];
float err = std::fabs(y - threshold);
if (err > max_y_error) max_y_error = err;
}
printf(" max y error from plane: %.4f\n", max_y_error);
CHECK(max_y_error < 0.05f, "vertex y should be close to plane (within 0.05)");
destroy_octree(root);
}
static void test_sphere() {
printf("Test: Sphere r=8 at (4,4,4), octree [0,16]...\n");
// Center offset from cube center ensures mixed corners at root level.
// Corner (0,0,0): dist 6.93 < 8 -> solid. Corner (16,16,16): dist 20.8 > 8 -> air.
SphereParams sp = {4.0f, 4.0f, 4.0f, 8.0f};
int min[3] = {0, 0, 0};
int size = 16;
float voxel_scale = 1.0f;
float origin[3] = {0.0f, 0.0f, 0.0f};
OctreeNode* root = construct_octree(min, size, voxel_scale, origin,
density_sphere, &sp);
CHECK(root != nullptr, "sphere octree should have sign changes");
DCMeshData mesh;
dc_generate_mesh(root, voxel_scale, origin, density_sphere, &sp, mesh);
int vert_count = (int)(mesh.positions.size() / 3);
int tri_count = (int)(mesh.indices.size() / 3);
printf(" vertices: %d, triangles: %d\n", vert_count, tri_count);
CHECK(vert_count > 0, "sphere should produce vertices");
CHECK(tri_count > 0, "sphere should produce triangles");
// All vertices should be near the sphere surface.
float max_dist_error = 0.0f;
for (int i = 0; i < vert_count; i++) {
float x = mesh.positions[i*3+0] - sp.cx;
float y = mesh.positions[i*3+1] - sp.cy;
float z = mesh.positions[i*3+2] - sp.cz;
float dist = std::sqrt(x*x + y*y + z*z);
float err = std::fabs(dist - sp.radius);
if (err > max_dist_error) max_dist_error = err;
}
printf(" max distance error from sphere surface: %.4f\n", max_dist_error);
CHECK(max_dist_error < 0.5f, "vertices should be near sphere surface (within 0.5)");
destroy_octree(root);
}
static void test_non_empty_output() {
printf("Test: Non-empty output...\n");
SphereParams sp = {4.0f, 4.0f, 4.0f, 8.0f};
int min[3] = {0, 0, 0};
int size = 16;
float voxel_scale = 1.0f;
float origin[3] = {0.0f, 0.0f, 0.0f};
OctreeNode* root = construct_octree(min, size, voxel_scale, origin,
density_sphere, &sp);
DCMeshData mesh;
dc_generate_mesh(root, voxel_scale, origin, density_sphere, &sp, mesh);
CHECK(!mesh.positions.empty(), "positions should not be empty");
CHECK(!mesh.indices.empty(), "indices should not be empty");
CHECK(mesh.positions.size() == mesh.normals.size(),
"positions and normals should have same size");
destroy_octree(root);
}
static void test_no_degenerate_triangles() {
printf("Test: No degenerate triangles...\n");
SphereParams sp = {4.0f, 4.0f, 4.0f, 8.0f};
int min[3] = {0, 0, 0};
int size = 16;
float voxel_scale = 1.0f;
float origin[3] = {0.0f, 0.0f, 0.0f};
OctreeNode* root = construct_octree(min, size, voxel_scale, origin,
density_sphere, &sp);
DCMeshData mesh;
dc_generate_mesh(root, voxel_scale, origin, density_sphere, &sp, mesh);
int tri_count = (int)(mesh.indices.size() / 3);
int degenerate = 0;
float epsilon = 1e-8f;
for (int t = 0; t < tri_count; t++) {
int i0 = mesh.indices[t*3+0];
int i1 = mesh.indices[t*3+1];
int i2 = mesh.indices[t*3+2];
float area = triangle_area(mesh.positions.data(), i0, i1, i2);
if (area < epsilon) degenerate++;
}
printf(" degenerate triangles: %d / %d\n", degenerate, tri_count);
CHECK(degenerate == 0, "no degenerate triangles");
destroy_octree(root);
}
static void test_index_bounds() {
printf("Test: Index bounds...\n");
SphereParams sp = {4.0f, 4.0f, 4.0f, 8.0f};
int min[3] = {0, 0, 0};
int size = 16;
float voxel_scale = 1.0f;
float origin[3] = {0.0f, 0.0f, 0.0f};
OctreeNode* root = construct_octree(min, size, voxel_scale, origin,
density_sphere, &sp);
DCMeshData mesh;
dc_generate_mesh(root, voxel_scale, origin, density_sphere, &sp, mesh);
int vert_count = (int)(mesh.positions.size() / 3);
bool all_valid = true;
for (int idx : mesh.indices) {
if (idx < 0 || idx >= vert_count) {
all_valid = false;
printf(" out-of-bounds index: %d (vert_count=%d)\n", idx, vert_count);
break;
}
}
CHECK(all_valid, "all indices should be within bounds");
destroy_octree(root);
}
static void test_normal_quality() {
printf("Test: Normal quality...\n");
SphereParams sp = {4.0f, 4.0f, 4.0f, 8.0f};
int min[3] = {0, 0, 0};
int size = 16;
float voxel_scale = 1.0f;
float origin[3] = {0.0f, 0.0f, 0.0f};
OctreeNode* root = construct_octree(min, size, voxel_scale, origin,
density_sphere, &sp);
DCMeshData mesh;
dc_generate_mesh(root, voxel_scale, origin, density_sphere, &sp, mesh);
int vert_count = (int)(mesh.positions.size() / 3);
int bad_normals = 0;
for (int i = 0; i < vert_count; i++) {
float nx = mesh.normals[i*3+0];
float ny = mesh.normals[i*3+1];
float nz = mesh.normals[i*3+2];
float len = std::sqrt(nx*nx + ny*ny + nz*nz);
if (len < 0.5f || len > 1.5f) bad_normals++;
}
printf(" bad normals: %d / %d\n", bad_normals, vert_count);
CHECK(bad_normals == 0, "normals should be roughly unit length");
destroy_octree(root);
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
int main() {
printf("=== dc_contour tests ===\n\n");
test_flat_plane();
test_sphere();
test_non_empty_output();
test_no_degenerate_triangles();
test_index_bounds();
test_normal_quality();
printf("\n=== Results: %d passed, %d failed ===\n", g_pass, g_fail);
return g_fail > 0 ? 1 : 0;
}