OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / libdc / include / dc_pocket.h

212 lines · c ref:
#ifndef DC_POCKET_H
#define DC_POCKET_H

// ---------------------------------------------------------------------------
// dc_pocket — subsystem 3 of level-mesh-architecture.md: a CARVED region as a
// discrete voxel entity.
//
// Why this module has to exist at all. The level is exact brush geometry, and
// DCBrushStore::csg4_view feeds the exact mesher only CLEAN brushes — anything
// an active carve touches is excluded (dc_brush_store.h:227). That exclusion is
// correct: a carved solid is non-convex, so it is no longer a brush and the
// winding clip cannot represent it. But until something meshed the excluded
// brushes, a carve did not carve, it DELETED. tests/test_pocket.cpp PART 1
// pins that behaviour so this claim stays checkable.
//
// The representation, straight from the decision in level-mesh-architecture.md:
//
//     pocket = DC mesh of ( union(component brushes) - union(active carves) )
//
// The BRUSHES stay authoritative; this mesh is a derived cache and may be thrown
// away and rebuilt at any time. Undo drops a carve and the component returns to
// the exact mesher unchanged — no plane re-fitting, so no error accumulation.
//
// Why voxels here and only here: a sphere carve is not a half-space, so the
// result leaves brush-land entirely. Mesh boolean is the wrong SHAPE of cost —
// O(global), 240 ms at 500K tris — while voxel DC is O(local). This is the one
// place in the level pipeline where DC genuinely earns its keep, and confining
// it to the carved pocket is what lets the other 99% of the level stay exact.
//
// Engine-free (libdc rule), so the gate runs from a bare main().
// ---------------------------------------------------------------------------

#include <cstdint>
#include <cmath>
#include <vector>
#include <algorithm>

#include "dc_brush_store.h"
#include "dc_edit.h"
#include "dc_generator.h"
#include "dc_mesher.h"

namespace dc_pocket {

struct PocketConfig {
    // Defaults to the level import's grid-aligned voxel (map_dc_terrain.gd sets
    // voxel_size 0.224 = 8 Quake units x world_scale) so a pocket lands on the
    // same lattice as everything else.
    float voxel_size = 0.224f;
    // Hard cap per axis. A pocket over a large brush at a fine voxel would
    // allocate (n+1)^3 samples; at 128 that is already ~2.1M evaluations. When
    // the cap binds, the voxel size is COARSENED to fit rather than the volume
    // being silently cropped — a cropped pocket would lose geometry, which is the
    // exact failure this module exists to prevent.
    int   max_grid = 128;
    // Voxels of margin around the component AABB. The surface of a brush sits ON
    // the AABB face, and DC needs a sample on both sides of the isosurface to
    // find it, so a pocket with no margin loses its outer skin.
    int   padding = 2;
};

struct PocketResult {
    DCMeshData mesh;
    float origin[3] = {0, 0, 0};
    float voxel_size = 0;
    int   grid_size = 0;
    bool  coarsened = false;    // max_grid bound; voxel_size was increased
};

// (union of the component's brushes) minus (all active carves).
//
// SIGN CONVENTIONS — the thing most likely to be got wrong here, so stated
// explicitly. Two different conventions are in play and both are deliberate:
//   * DCGenerator::evaluate returns DENSITY: positive = SOLID, negative = air.
//   * The shape SDFs inside DCEditList::apply are negative = INSIDE the shape.
// So a subtractive edit is min(density, shape_sdf) — inside the shape the sdf is
// negative, which drives density negative, which is air. That mixed convention
// is what the in-game terrain edit path already uses, so this reuses
// DCEditList::apply verbatim rather than re-deriving the CSG and risking a sign
// flip that would silently invert every carve.
class Generator : public DCGenerator {
public:
    Generator(const DCBrushStore& store, const std::vector<uint32_t>& component) {
        for (uint32_t id : component) {
            const DCBrush* b = store.get(id);
            if (b && !b->tombstoned) brushes_.push_back(b);
        }
        // ALL active carves, not just those touching this component. A carve
        // elsewhere cannot affect this volume anyway (its own quick-reject sees
        // to that), and filtering by `touched` would silently drop a carve whose
        // AABB test and whose actual reach disagree at the margin.
        for (const DCCarve& c : store.carves())
            if (!c.undone) carves_.push_back(c.shape);
    }

    float evaluate(float x, float y, float z, float /*lod_voxel_size*/) override {
        // Union of convex brushes, in POSITIVE = SOLID.
        // A convex brush is the intersection of its half-spaces, so
        //   f(p) = max_i (n_i·p - d_i)  is negative inside;
        // negate it to get a positive-inside density, and take the max over
        // brushes for the union.
        float density = -1e30f;
        for (const DCBrush* b : brushes_) {
            float f = -1e30f;
            for (const ConvexPlane& pl : b->planes) {
                const float dp = pl.nx * x + pl.ny * y + pl.nz * z - pl.d;
                if (dp > f) f = dp;
            }
            const float d = -f;
            if (d > density) density = d;
        }
        if (carves_.empty()) return density;
        return DCEditList::apply(carves_, x, y, z, density);
    }

    bool empty() const { return brushes_.empty(); }

private:
    std::vector<const DCBrush*> brushes_;
    std::vector<TerrainEdit>    carves_;
};

// Build one pocket. Returns false when the component has no live brush or no
// valid bounds — never on "the mesh came out empty", which is a legitimate
// result (a carve that removes a brush entirely leaves nothing to draw, and the
// caller needs to tell that apart from a failure).
// Mesh an EXPLICIT box. This is the one the level uses: dc_brush_split bounds a
// carve to an edit-sized box, and the pocket must sample exactly that box or the
// voxel surface and the exact pieces stop meeting at the same plane.
inline bool build_bounded(const DCBrushStore& store,
                          const std::vector<uint32_t>& component,
                          const PocketConfig& cfg,
                          const float lo_in[3], const float hi_in[3],
                          PocketResult& out) {
    float lo[3] = {lo_in[0], lo_in[1], lo_in[2]};
    float hi[3] = {hi_in[0], hi_in[1], hi_in[2]};
    if (hi[0] <= lo[0] || hi[1] <= lo[1] || hi[2] <= lo[2]) return false;

    float voxel = cfg.voxel_size > 1e-6f ? cfg.voxel_size : 0.224f;
    const float ext = std::max(std::max(hi[0] - lo[0], hi[1] - lo[1]), hi[2] - lo[2]);

    // Size the grid to the box, then coarsen if it would blow the cap.
    int grid = (int)std::ceil(ext / voxel) + 2 * cfg.padding + 1;
    out.coarsened = false;
    if (grid > cfg.max_grid) {
        const int usable = std::max(4, cfg.max_grid - 2 * cfg.padding - 1);
        voxel = ext / (float)usable;
        grid = cfg.max_grid;
        out.coarsened = true;
    }
    // Round to a power of two HERE, because dc_mesher does it internally anyway
    // (dc_mesher.cpp:44-47, the octree requires it). A grid computed as 65 is
    // silently sampled as 128 — 8x the voxels. Doing it up front means the size
    // this reports is the size actually sampled, instead of a number that
    // understates the cost by up to 8x.
    int p2 = 1;
    while (p2 < grid) p2 <<= 1;
    grid = p2;

    // Origin is the component AABB corner pulled back by the margin, so the
    // brush's own surface is strictly interior to the sampled volume.
    const float origin[3] = {
        lo[0] - voxel * (float)cfg.padding,
        lo[1] - voxel * (float)cfg.padding,
        lo[2] - voxel * (float)cfg.padding,
    };

    Generator gen(store, component);
    if (gen.empty()) return false;

    DCMesherConfig mc;
    // Sharp features are the whole point of this level pipeline; Laplacian
    // smoothing would round the very brush edges the exact mesher preserves on
    // the other side of the seam.
    mc.laplacian_smooth = false;
    mc.auto_smooth = true;

    out.mesh = dc_mesh_from_generator(gen, origin, voxel, grid, mc);
    out.origin[0] = origin[0];
    out.origin[1] = origin[1];
    out.origin[2] = origin[2];
    out.voxel_size = voxel;
    out.grid_size = grid;
    return true;
}

// Convenience: mesh the component's own AABB. Kept for the oracle and for any
// caller with no split box — the LEVEL does not use this, because a box derived
// from the brush is exactly the cost dc_brush_split exists to remove.
inline bool build(const DCBrushStore& store,
                  const std::vector<uint32_t>& component,
                  const PocketConfig& cfg,
                  PocketResult& out) {
    float lo[3] = { 1e30f,  1e30f,  1e30f};
    float hi[3] = {-1e30f, -1e30f, -1e30f};
    int live = 0;
    for (uint32_t id : component) {
        const DCBrush* b = store.get(id);
        if (!b || b->tombstoned || !b->bounds_valid) continue;
        lo[0] = std::min(lo[0], b->lo.x); hi[0] = std::max(hi[0], b->hi.x);
        lo[1] = std::min(lo[1], b->lo.y); hi[1] = std::max(hi[1], b->hi.y);
        lo[2] = std::min(lo[2], b->lo.z); hi[2] = std::max(hi[2], b->hi.z);
        live++;
    }
    if (live == 0) return false;
    return build_bounded(store, component, cfg, lo, hi, out);
}

}  // namespace dc_pocket

#endif // DC_POCKET_H