OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / test / test_contour.cpp

272 lines · cpp ref:
// 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;
}