OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / tests / test_clearance_shell.cpp

223 lines · cpp ref:
// ============================================================================
// CLEARANCE BRUSHES — does the shell enclose air the level owns?
//
// hyper-games #25 §2. dc_level_shell floods EXTERIOR in from the grid border and
// emits the boundary between what it reached and what it did not. That is right
// for a sealed room and wrong for anything open: the bore of an archway driven
// into a hillside is reachable from outside, so it is marked EXTERIOR, no terrain
// is subtracted from it, and the tunnel is full of dirt. The ring renders; you
// cannot walk through it.
//
// A CLEARANCE brush is the fix — a solid that exists only to stop that flood, so
// the sealed volume extends through the passage. It must do exactly three things
// and no others:
//
//   1. BLOCK THE FILL, so the bore ends up inside the shell.
//   2. NOT COUNT AS WALL. sealed() is interior_volume > solid_volume * 0.5
//      (dc_level_shell.h:105). Clearance is air, so it belongs in interior; count
//      it as solid and a big enough passage tips a good level into "not sealed"
//      and drops the map to the moat fallback.
//   3. CHANGE NOTHING ELSE. Closure, and the sign everywhere the level already
//      answered for, are unaffected.
//
// SYNTHETIC ON PURPOSE — no START.MAP dump, no Godot. The property is about the
// flood fill, not about any particular level, and a test that needs a 1183-brush
// binary from a headless export is one nobody runs.
//
// Build: g++ -O2 -std=c++17 -I libdc/include -o /tmp/clearance_shell \
//          tests/test_clearance_shell.cpp libdc/src/dc_csg4.cpp \
//          libdc/src/dc_level_shell.cpp libdc/src/generators/dc_generator_mesh_sdf.cpp
// ============================================================================

#include "dc_level_shell.h"
#include "dc_generator_mesh_sdf.h"

#include <cmath>
#include <cstdio>
#include <vector>

// ---- the scene -------------------------------------------------------------
//
// A sealed room, 7 x 5 x 7 outside and 6 x 4 x 6 of air inside, walls 0.5 thick
// — the same order of magnitude as a Quake wall at world_scale (0.448 m), which
// is the thickness the conservative rasterization exists for.
//
// Then a tunnel: a prism leaving the +X wall's outer face and running 6 m out
// into what would be hillside. Nothing in the shell knows about terrain, so the
// hill is not modelled; what is being tested is whether the shell ENCLOSES that
// prism, because enclosing it is what makes the subtract remove the hill.

struct Box {
    float lo[3], hi[3];
};

static const Box ROOM_WALLS[6] = {
    {{0.0f, 0.0f, 0.0f}, {7.0f, 0.5f, 7.0f}},      // floor
    {{0.0f, 4.5f, 0.0f}, {7.0f, 5.0f, 7.0f}},      // ceiling
    {{0.0f, 0.5f, 0.0f}, {0.5f, 4.5f, 7.0f}},      // -X
    {{6.5f, 0.5f, 0.0f}, {7.0f, 4.5f, 7.0f}},      // +X
    {{0.5f, 0.5f, 0.0f}, {6.5f, 4.5f, 0.5f}},      // -Z
    {{0.5f, 0.5f, 6.5f}, {6.5f, 4.5f, 7.0f}},      // +Z
};

// Abuts the +X wall's outer face at x = 7 and runs out to x = 13.
static const Box TUNNEL = {{7.0f, 0.5f, 3.0f}, {13.0f, 2.5f, 4.0f}};

static const float TUNNEL_VOLUME = 6.0f * 2.0f * 1.0f;   // 12 m^3

// A point in the middle of the bore. THE point: outside the level's own volume,
// inside the passage, and the whole question is which of those the shell agrees
// with.
static const float MID_TUNNEL[3] = {10.0f, 1.5f, 3.5f};
// Directly above the tunnel, in open air the level has no claim on. Guards
// against a "fix" that simply grows the sealed volume.
static const float ABOVE_TUNNEL[3] = {10.0f, 6.0f, 3.5f};
// Inside the floor slab — solid the shell has always enclosed.
static const float IN_FLOOR[3] = {3.5f, 0.25f, 3.5f};

static void box_planes(const Box& b, std::vector<ConvexPlane>& out) {
    out.clear();
    out.push_back({-1.0f, 0.0f, 0.0f, -b.lo[0]});
    out.push_back({ 1.0f, 0.0f, 0.0f,  b.hi[0]});
    out.push_back({ 0.0f,-1.0f, 0.0f, -b.lo[1]});
    out.push_back({ 0.0f, 1.0f, 0.0f,  b.hi[1]});
    out.push_back({ 0.0f, 0.0f,-1.0f, -b.lo[2]});
    out.push_back({ 0.0f, 0.0f, 1.0f,  b.hi[2]});
}

// Storage the CSG4Brush views point into — they are non-owning, so this has to
// outlive every build below.
struct Scene {
    std::vector<std::vector<ConvexPlane>> planes;
    std::vector<CSG4Brush> brushes;
};

// `tunnel`: 0 = absent, 1 = present as CLEARANCE, 2 = present as ordinary solid.
static void build_scene(Scene& s, int tunnel) {
    s.planes.clear();
    s.brushes.clear();
    const int n = 6 + (tunnel ? 1 : 0);
    s.planes.resize(n);
    for (int i = 0; i < 6; i++) box_planes(ROOM_WALLS[i], s.planes[i]);
    if (tunnel) box_planes(TUNNEL, s.planes[6]);
    s.brushes.resize(n);
    for (int i = 0; i < n; i++) {
        s.brushes[i].planes = s.planes[i].data();
        s.brushes[i].plane_count = (uint32_t)s.planes[i].size();
        s.brushes[i].id = (uint32_t)i;
        s.brushes[i].clearance = (i == 6 && tunnel == 1);
    }
    dc_csg4::compute_bounds(s.brushes);
}

// ---- the sign test ---------------------------------------------------------
//
// Through DCGeneratorMeshSDF, because that is what actually consumes the shell:
// stamp_mesh_arrays hands these triangles to the same class. A bespoke parity
// test here could disagree with the consumer and we would not find out.
// Convention (dc_generator_mesh_sdf.h): positive = inside.

static bool sdf_inside(const dc_level_shell::ShellMesh& sh, const float p[3],
                       const char* obj_path) {
    FILE* o = fopen(obj_path, "w");
    for (int i = 0; i < sh.vertex_count(); i++)
        fprintf(o, "v %.6f %.6f %.6f\n",
                sh.vertices[i*3], sh.vertices[i*3+1], sh.vertices[i*3+2]);
    for (size_t i = 0; i + 2 < sh.indices.size(); i += 3)
        fprintf(o, "f %d %d %d\n", sh.indices[i]+1, sh.indices[i+1]+1, sh.indices[i+2]+1);
    fclose(o);
    DCGeneratorMeshSDF sdf;
    if (!sdf.load_obj(obj_path)) { printf("  (could not load %s)\n", obj_path); return false; }
    return sdf.evaluate(p[0], p[1], p[2], dc_level_shell::DEFAULT_CELL) > 0.0f;
}

static int failures = 0;
static void check(const char* label, bool ok, const char* detail) {
    if (!ok) failures++;
    printf("  %-44s %s  %s\n", label, ok ? "OK  " : "FAIL", detail);
}

int main() {
    char buf[160];

    // ---- CONTROL: no tunnel at all --------------------------------------
    // Establishes what the room alone answers, so every later number has
    // something to be a difference from.
    Scene plain;
    build_scene(plain, 0);
    dc_level_shell::ShellMesh sh_plain;
    if (!dc_level_shell::build(plain.brushes, sh_plain)) {
        printf("FAIL — build() returned false on the plain room\n");
        return 1;
    }
    printf("plain room: solid %.1f m^3  interior %.1f m^3  tris %d  boundary_edges %lld  sealed=%s\n",
           sh_plain.solid_volume, sh_plain.interior_volume, sh_plain.triangle_count(),
           sh_plain.boundary_edges, sh_plain.sealed() ? "yes" : "NO");
    check("plain room is closed", sh_plain.boundary_edges == 0, "0 boundary edges expected");
    check("plain room is sealed", sh_plain.sealed(), "air > wall");

    const bool plain_mid = sdf_inside(sh_plain, MID_TUNNEL, "/tmp/clearance_plain.obj");
    // THE BUG, stated as an assertion. With no clearance brush the bore is open
    // to the void, the flood reaches it, and the terrain keeps its hill. This
    // must read OUTSIDE — if it does not, the rest of the test proves nothing.
    check("without clearance, the bore reads OUTSIDE", !plain_mid,
          plain_mid ? "already inside — the premise is wrong" : "as expected");

    // ---- CLEARANCE: the same prism, flagged ------------------------------
    Scene clear_;
    build_scene(clear_, 1);
    dc_level_shell::ShellMesh sh_clear;
    if (!dc_level_shell::build(clear_.brushes, sh_clear)) {
        printf("FAIL — build() returned false with a clearance brush\n");
        return 1;
    }
    printf("with clearance: solid %.1f m^3  interior %.1f m^3  tris %d  boundary_edges %lld  sealed=%s\n",
           sh_clear.solid_volume, sh_clear.interior_volume, sh_clear.triangle_count(),
           sh_clear.boundary_edges, sh_clear.sealed() ? "yes" : "NO");

    // 1. BLOCK THE FILL.
    const bool clear_mid = sdf_inside(sh_clear, MID_TUNNEL, "/tmp/clearance_tunnel.obj");
    check("the bore reads INSIDE the shell", clear_mid,
          clear_mid ? "terrain will be subtracted from it" : "still exterior — the flood got in");

    // 2. NOT COUNT AS WALL. Conservative rasterization rounds outward, so the
    // interior gain is compared with a cell of slack rather than exactly.
    const double solid_delta = sh_clear.solid_volume - sh_plain.solid_volume;
    const double air_delta = sh_clear.interior_volume - sh_plain.interior_volume;
    snprintf(buf, sizeof(buf), "%+.2f m^3", solid_delta);
    check("clearance adds NO wall volume", std::fabs(solid_delta) < 1e-6, buf);
    snprintf(buf, sizeof(buf), "%+.1f m^3 vs %.0f expected", air_delta, (double)TUNNEL_VOLUME);
    check("clearance adds its volume as AIR",
          air_delta > TUNNEL_VOLUME * 0.9 && air_delta < TUNNEL_VOLUME * 1.6, buf);

    // 3. CHANGE NOTHING ELSE.
    check("still closed", sh_clear.boundary_edges == 0, "0 boundary edges expected");
    check("still manifold", sh_clear.nonmanifold_edges == 0, "0 non-manifold edges expected");
    check("still sealed", sh_clear.sealed(), "air > wall");
    const bool clear_floor = sdf_inside(sh_clear, IN_FLOOR, "/tmp/clearance_floor.obj");
    check("a point in the floor still reads INSIDE", clear_floor, "unchanged by clearance");
    const bool clear_above = sdf_inside(sh_clear, ABOVE_TUNNEL, "/tmp/clearance_above.obj");
    check("open air above the bore reads OUTSIDE", !clear_above,
          clear_above ? "the sealed volume grew past the passage" : "bounded");

    // ---- DISCRIMINATING CONTROL: the same prism, NOT flagged -------------
    // Everything above is also true of an ordinary solid brush placed there — it
    // blocks the fill too. What separates them is the accounting: a solid is
    // wall, clearance is air. Without this, `clearance` could be ignored
    // entirely and the suite would still pass.
    Scene solid;
    build_scene(solid, 2);
    dc_level_shell::ShellMesh sh_solid;
    if (!dc_level_shell::build(solid.brushes, sh_solid)) {
        printf("FAIL — build() returned false with a solid tunnel brush\n");
        return 1;
    }
    const double solid_wall = sh_solid.solid_volume - sh_plain.solid_volume;
    snprintf(buf, sizeof(buf), "solid +%.1f wall vs clearance +%.1f", solid_wall, solid_delta);
    check("an UNFLAGGED prism counts as wall instead", solid_wall > TUNNEL_VOLUME * 0.9, buf);

    printf("\n%s (%d failure%s)\n", failures ? "FAILED" : "PASSED",
           failures, failures == 1 ? "" : "s");
    return failures ? 1 : 0;
}