OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / libdc / include / dc_generator.h

117 lines · c ref:
#ifndef DC_GENERATOR_H
#define DC_GENERATOR_H

#include <cstdint>

#include <cmath>

// Abstract base class for density/SDF evaluation.
//
// Convention: positive = solid, negative = air.
// (Matches DCDensity's convention used throughout the DC pipeline.)
//
// Implementations provide at minimum evaluate(). The default gradient()
// uses Quilez's tetrahedron finite-difference method. Override for analytic
// gradients or GPU-backed evaluation.
class DCGenerator {
public:
    virtual ~DCGenerator() = default;

    // Required: evaluate density at world position.
    // lod_voxel_size: current LOD voxel size (for Nyquist octave clamping).
    // Returns: positive = solid, negative = air.
    virtual float evaluate(float x, float y, float z, float lod_voxel_size) = 0;

    // Optional: analytic gradient. Default: Quilez tetrahedron finite difference.
    // Writes unit normal into normal[3].
    virtual void gradient(float x, float y, float z, float lod_voxel_size, float normal[3]) {
        float h = lod_voxel_size * 0.25f;
        if (h < 0.001f) h = 0.001f;
        float d0 = evaluate(x + h, y - h, z - h, lod_voxel_size);
        float d1 = evaluate(x - h, y - h, z + h, lod_voxel_size);
        float d2 = evaluate(x - h, y + h, z - h, lod_voxel_size);
        float d3 = evaluate(x + h, y + h, z + h, lod_voxel_size);
        normal[0] = -(d0 - d1 - d2 + d3);
        normal[1] = -(-d0 - d1 + d2 + d3);
        normal[2] = -(-d0 + d1 - d2 + d3);
        float len = std::sqrt(normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]);
        if (len > 1e-8f) {
            normal[0] /= len;
            normal[1] /= len;
            normal[2] /= len;
        }
    }

    // MANY POINTS AT ONCE, and the default is exactly the loop it replaces.
    //
    // A generator whose per-point cost is dominated by resolving state that is
    // the same for every point in a grid can hoist that out here. The proc-nodes
    // bridge is one: it answered "what is Erosion Noise's Gain" by scanning a
    // vector of Godot Strings, per voxel, 35937 times per chunk.
    //
    // Overriding this is OPTIONAL and the default below is behaviour-identical,
    // so no existing generator changes. r_out receives p_count values;
    // p_xyz holds p_count * 3 floats, x,y,z interleaved.
    virtual void evaluate_batch(const float* p_xyz, int p_count,
                                float lod_voxel_size, float* r_out) {
        for (int i = 0; i < p_count; i++) {
            r_out[i] = evaluate(p_xyz[i * 3], p_xyz[i * 3 + 1], p_xyz[i * 3 + 2],
                                lod_voxel_size);
        }
    }

    // Optional: GPU compute shader source (GLSL). Return nullptr for CPU-only.
    virtual bool has_gpu_shader() const { return false; }
    virtual const char* gpu_shader_source() const { return nullptr; }
    // OPAQUE DATA for the custom shader. dc-terrain does not know or care
    // what these floats mean -- a heightfield, a mask, a lookup table. It
    // transports them to a storage buffer the density body may read, and
    // the generator that supplied them owns their meaning.
    //
    // The pointer must stay valid and STABLE: the GPU thread re-uploads only
    // when it changes, exactly as it recompiles only when the shader source
    // changes.
    virtual const float* gpu_custom_data() const { return nullptr; }
    // ELEMENTS behind gpu_custom_data(), not a side length. DCCompute uploads
    // exactly this many floats; the two are a PAIR and must move together —
    // moving one alone gives either a truncated buffer or an attempt to
    // allocate count² floats, which is 7.9 TB for a 1026 tile and a segfault.
    virtual int gpu_custom_data_count() const { return 0; }
    // A CONTENT VERSION, because the STABLE-pointer contract above cannot
    // express mutation.
    //
    // The GPU thread re-uploads when the POINTER changes. That is correct for a
    // buffer that is only ever replaced wholesale, and WRONG the moment a
    // generator can grow its data in place: a std::vector assigned to within
    // its existing capacity keeps the same data() address, so the new contents
    // never reach the GPU and every log line still reads healthy. A tile
    // fetched from a peer mid-session is exactly that case.
    //
    // Bump this whenever the bytes change, whatever the pointer does.
    virtual uint32_t gpu_custom_data_version() const { return 0; }

    // NOISE VOLUMES for a density body that SAMPLES noise rather than computing
    // it (GPU Gems 3 ch.1 fetches eight 16^3 RGBA volumes; that is why nine
    // octaves were affordable in 2007). Transported to sampler3D bindings the
    // host shader declares by name, under the same stable-pointer contract as
    // gpu_custom_data above.
    //
    // Returns an array of EIGHT pointers -- plain 0-3, then packed 4-7 -- each
    // to dim^3 RGBA HALF floats (the .vol files' native format, and what the
    // texture is created as). Deliberately not float32: promoting them changes
    // the precision of the hardware trilinear filtering, and a 25-unit domain
    // warp amplifies that into a visible density difference from anything else
    // sampling the same volumes. nullptr means the body samples no volumes, and
    // then neither shader declares the bindings.
    virtual const void* const* gpu_noise_volumes() const { return nullptr; }
    // Side length, unlike gpu_custom_data_count which is an element count.
    virtual int gpu_noise_dim() const { return 0; }
    virtual uint32_t gpu_noise_version() const { return 0; }

    // Optional: bounding box hint for grid allocation.
    // Writes axis-aligned min/max into min[3]/max[3]. Returns false if unbounded.
    virtual bool get_bounds(float min[3], float max[3]) const { return false; }
};

#endif // DC_GENERATOR_H