OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / shaders / edge_intersect.glsl

508 lines · glsl ref:
#version 450

// ============================================================================
// Stage 4+5 FUSED: Edge Intersection + Surface Normals
//
// Template shader: the SDF body is injected at the GENERATOR_SDF_BODY marker
// by DCWorker at shader composition time.
//
// For each active edge (compacted via Phase E), find the zero-crossing
// position via binary search, then compute the gradient (normal) at that
// point via central differences — all in a single pass.
//
// Previously these were two separate dispatches (edge_intersect + surface_normals).
// Fusing them eliminates 1 dispatch + 1 barrier and reuses SDF cache locality
// from the binary search for the gradient evaluations.
//
// Input:
//   density[33^3]         — density samples (for initial bracket)
//   edge_flags[32^3x3]    — 1 if sign change
//   edge_scan[32^3x3]     — exclusive prefix sum of edge_flags
//   Params UBO            — density evaluation parameters
//   voronoi_data[]        — voronoi cell data
//   perm_tables[]         — noise permutation tables
//   csg_primitives[]      — CSG primitive list
//   active_edge_indices[] — compacted active edge list (Phase E)
//   counts[]              — counts[0] = active edge count
//
// Output:
//   edge_data[active_count] — vec3 position + vec3 normal + uint edge_idx + pad
//
// Dispatch: ceil(EDGE_COUNT / 256), 1, 1
// ============================================================================

#define THREADS 256
#define BINARY_SEARCH_ITERS 8

layout(local_size_x = THREADS, local_size_y = 1, local_size_z = 1) in;

layout(set = 0, binding = 0, std430) readonly  buffer DensityBuf  { float density[];    };
layout(set = 0, binding = 1, std430) readonly  buffer EdgeFlags   { uint  edge_flags[]; };
layout(set = 0, binding = 2, std430) readonly  buffer EdgeScan    { uint  edge_scan[];  };
layout(set = 0, binding = 3, std430) writeonly buffer EdgeData    { float edge_data[];  };

// Density evaluation parameters (same layout as density_fill.glsl)
layout(set = 0, binding = 4, std140) uniform Params {
    int   grid_stride;
    float voxel_scale;
    float origin_x;
    float origin_y;
    float origin_z;
    float primary_frequency;
    float primary_strength;
    float tunnel_frequency;
    float tunnel_strength;
    float tunnel_radius;
    float ridge_frequency;
    float ridge_strength;
    float warp_frequency;
    float warp_strength;
    float detail_frequency;
    float detail_strength;
    float ground_bias;
    float solid_floor_depth;
    float sky_height;
    float flat_center_radius;
    float floor_strength;
    float floor_height_min;
    float floor_height_max;
    float room_height;
    float terrain_size;
    int primary_octaves;
    int tunnel_octaves;
    int ridge_octaves;
    int warp_octaves;
    int detail_octaves;
    int compute_normals;
    int seed;
    int voronoi_cell_count;
    int use_noise;
    int spherical;
    float planet_radius;
    float planet_center_x;
    float planet_center_y;
    float planet_center_z;
    float island_radius;
    int flags;            // bit1 = sample settled-edit brick (matches GpuParams)
};

layout(set = 0, binding = 5, std430) readonly buffer VoronoiBuf {
    int   voronoi_count;
    float voronoi_data[];
};

layout(set = 0, binding = 6, std430) readonly buffer PermBuf {
    int perm_tables[];
};

// ---- CSG primitives ----
// Edit buffer — MUST match the density-stage layout in dc_compute.cpp (GpuEdit /
// EditBuf). Previously this stage declared a divergent CSGPrimitive struct over
// the same binding-7 bytes, so the mesh stage read edits with scrambled field
// offsets (box center from half-extents, op/group from position floats) — the
// zero-crossing binary search and normals were placed on a garbage surface while
// the density grid was carved correctly, producing per-chunk steps, dark seams
// and mis-snapped cut floors. Same struct + same apply_gpu_edits math now.
struct GpuEdit {
    uint  edit_type;    // 0=sphere_sub,1=sphere_add,2=box_sub,3=box_add,4=mesh_sub,5=mesh_add,6=sphere_hard_sub,7=sphere_hard_add
    float pos_x, pos_y, pos_z;
    float half_x, half_y, half_z;
    float radius;
    float strength;
    uint  _pad0, _pad1, _pad2;
};

layout(set = 0, binding = 7, std430) readonly buffer EditBuf {
    uint edit_count;
    uint _edit_pad0, _edit_pad1, _edit_pad2;
    GpuEdit gpu_edits[];
};

// Convex-brush half-space planes (binding 11) — MUST match dc_compute.cpp
// binding 12. A CONVEX edit (etype 8/9) reads [_pad1, _pad1+_pad2) of
// vec4(nx,ny,nz,d); inside the brush = (n·p <= d).
layout(set = 0, binding = 11, std430) readonly buffer ConvexPlaneBuf {
    vec4 convex_planes[];
};

// Phase E: Compacted active edge index list
layout(set = 0, binding = 8, std430) readonly buffer ActiveEdgesBuf { uint active_edge_indices[]; };
layout(set = 0, binding = 9, std430) readonly buffer CountBuf { uint counts[]; };

// Settled-edit SDF delta brick (chunk-local, density-stride grid). Trilinearly
// sampled inside evaluate_density (noise_sdf.glsl) so the fractional binary-
// search + normal evals see the SAME baked surface as the density-grid
// endpoints. A zero buffer is an exact no-op (d += 0). Binding 10 was free.
layout(set = 0, binding = 10, std430) readonly buffer BrickBuf { float brick_delta[]; };

// ---------------------------------------------------------------------------
// EXTERNAL HEIGHTFIELD (hyper-games #50 section 5.3)
//
// A baked terrain-diffusion block, elevation in METRES, row-major with
// row = world Z and column = world X. Bound in BOTH shaders the proc-nodes
// density body is injected into, because that body references custom_data_height_m
// and a shader missing it does not compile at all.
//
// The block's GEOMETRY (size, origin, pitch) is NOT here. It is baked into
// the generator's emitted GLSL as literals, so this needs no Params field and
// therefore cannot desynchronise the std140 layout between the two shaders and
// the C++ struct -- which is the one failure in this area that is silent.
// The generator recompiles whenever a property changes, so a new block is a
// recompile, not a uniform update.
//
// Unbound, this is a one-float placeholder and the generator emits no
// reference to it.
layout(set = 0, binding = 13, std430) readonly buffer CustomDataBuf { float custom_data[]; };

// GENERATOR_SAMPLER_BINDINGS
// Bindings 14..21 (eight 16^3 noise volumes) are injected HERE, and only when
// the generator body actually samples them -- see _build_edge_intersect_glsl.


layout(push_constant) uniform PushConstants {
    vec3  cell_origin;
    float voxel_size;
    ivec3 grid_dims;       // (32, 32, 32)
    uint  total_elements;
    uint  mesh_quality_flags;
    uint  _pc_pad0;
    uint  _pc_pad1;
    uint  _pc_pad2;
};

// =====================================================================
// SDF body — injected by DCWorker from generator GLSL
// Must define: float evaluate_density(float x, float y, float z)
// =====================================================================
// GENERATOR_SDF_BODY
// =====================================================================
// FIELD SAMPLING — edits and the settled brick are applied HERE, by the
// TEMPLATE, for every generator body.
//
// THEY USED TO BE THE BODY'S JOB, and only one body ever did it. There are two
// templates and they had OPPOSITE contracts: the density stage
// (dc_shader_source.h, _build_custom_shader = HEADER + body + MAIN) applies the
// edits in MAIN, so its body must NOT; this stage called evaluate_density()
// directly and applied none, so its body MUST. The built-in generator satisfied
// both only by existing twice -- DENSITY_GLSL_BODY without the apply,
// generators/noise_sdf.glsl with it.
//
// A CUSTOM generator has ONE body for BOTH templates and cannot satisfy both.
// DCTerrainGeneratorProcNodes applies edits in neither (dc_gpu_thread.h:258 and
// :321 hand the SAME `glsl` string to set_custom_shader and set_custom_sdf), so
// on map_dc_erosion the density grid was carved while this stage's crossing
// search and normals rode the UNCARVED field -- the failure class the EditBuf
// note above already documents from a previous incident.
//
// Emitting the tail from the proc-nodes generator instead would have been
// WRONG: the density stage would then apply twice, and apply_gpu_edits is
// ADDITIVE (`d += e.strength * t * t`), not idempotent. The carve would go
// twice as deep.
//
// So the contract is now ONE contract, and it is the density stage's:
//     a generator body returns the RAW field; the template applies the edits.
// Gate: tests/test_edit_contract.cpp drives THIS main() with a trivial
// analytic body and asserts the crossing lands on the carved surface.
// =====================================================================
// Edit application — IDENTICAL to the density-stage apply_gpu_edits in
// dc_compute.cpp so the mesh stage carves the exact same surface the density
// grid was carved with (no per-chunk divergence). Box + sphere only; mesh-SDF
// edits (etype>=4) need the BVH bindings the mesh stage doesn't have, so they
// are skipped here.
float apply_gpu_edits(float d, float wx, float wy, float wz) {
    for (uint i = 0u; i < edit_count; i++) {
        GpuEdit e = gpu_edits[i];
        uint etype = e.edit_type;

        // Bounding sphere early reject
        float edx = wx - e.pos_x;
        float edy = wy - e.pos_y;
        float edz = wz - e.pos_z;
        float dist_sq = edx*edx + edy*edy + edz*edz;
        if (dist_sq > e.radius * e.radius) continue;

        if (etype <= 1u) {
            // Sphere: smooth quadratic falloff
            float r_sq = e.radius * e.radius;
            float t = 1.0 - dist_sq / r_sq;
            d += e.strength * t * t;
        } else if (etype <= 3u) {
            // Box: SDF-based CSG
            float dx = abs(wx - e.pos_x) - e.half_x;
            float dy = abs(wy - e.pos_y) - e.half_y;
            float dz = abs(wz - e.pos_z) - e.half_z;
            float outside = sqrt(
                max(dx, 0.0) * max(dx, 0.0) +
                max(dy, 0.0) * max(dy, 0.0) +
                max(dz, 0.0) * max(dz, 0.0));
            float inside = min(max(max(dx, dy), dz), 0.0);
            float box_sdf = outside + inside;
            if (e.strength < 0.0) {
                d = min(d, box_sdf);
            } else {
                d = max(d, -box_sdf);
            }
        } else if (etype == 6u || etype == 7u) {
            // Hard sphere CSG: SDF = dist - radius (neg inside, pos outside —
            // same convention as box_sdf). Union/difference by strength sign.
            float sphere_sdf = sqrt(dist_sq) - e.radius;
            if (e.strength < 0.0) {
                d = min(d, sphere_sdf);
            } else {
                d = max(d, -sphere_sdf);
            }
        } else if (etype == 8u || etype == 9u) {
            // Convex brush = intersection of half-spaces: f = max_i(n_i·p - d_i)
            // (neg inside, pos outside). Planes in [_pad1, _pad1+_pad2). IDENTICAL
            // to the density-stage convex branch in dc_compute.cpp.
            uint poff = e._pad1;
            uint pcnt = e._pad2;
            float f = -1e30;
            for (uint pi = 0u; pi < pcnt; pi++) {
                vec4 pl = convex_planes[poff + pi];
                f = max(f, pl.x * wx + pl.y * wy + pl.z * wz - pl.w);
            }
            if (e.strength < 0.0) {
                d = min(d, f);
            } else {
                d = max(d, -f);
            }
        }
        // etype 4/5 (mesh-SDF edits) skipped: BVH bindings unavailable here.
    }
    return d;
}

// =====================================================================
// Settled-edit SDF delta brick — trilinear sample of brick_delta[] (binding
// 10, declared in edge_intersect.glsl). Chunk-local grid at the density
// stride (grid_dims + 1). MUST match the density-stage apply_brick_delta in
// dc_compute.cpp so the fractional mesh-stage samples agree with the grid
// endpoints (same divergence class as the two evaluate_density copies). A
// zero buffer returns 0 exactly, so this is a no-op until a bake fills it.
// =====================================================================
float apply_brick_delta(float wx, float wy, float wz) {
    int stride = grid_dims.x + 1;
    vec3 lv = (vec3(wx, wy, wz) - cell_origin) / voxel_size;
    lv = clamp(lv, vec3(0.0), vec3(float(stride - 1) - 1e-4));
    ivec3 i0 = ivec3(floor(lv));
    vec3 f = lv - vec3(i0);
    int sx = stride, sxy = stride * stride;
    int b = i0.z * sxy + i0.y * sx + i0.x;
    float c00 = mix(brick_delta[b],            brick_delta[b + 1],            f.x);
    float c10 = mix(brick_delta[b + sx],       brick_delta[b + sx + 1],       f.x);
    float c01 = mix(brick_delta[b + sxy],      brick_delta[b + sxy + 1],      f.x);
    float c11 = mix(brick_delta[b + sxy + sx], brick_delta[b + sxy + sx + 1], f.x);
    return mix(mix(c00, c10, f.y), mix(c01, c11, f.y), f.z);
}

float dc_sample_field(float x, float y, float z) {
    float d = evaluate_density(x, y, z);
    // Order and gating match what noise_sdf.glsl did inline: the settled brick
    // already has the edits folded in, so exactly one of the two applies.
    if ((flags & 2) != 0) {
        d += apply_brick_delta(x, y, z);
    } else if (edit_count > 0u) {
        d = apply_gpu_edits(d, x, y, z);
    }
    return d;
}


// =====================================================================
// Main: binary search + gradient in single pass
// =====================================================================

void main() {
    uint tid = gl_GlobalInvocationID.x;
    if (tid >= counts[0]) return;  // active edge count from prepare_dispatch
    uint gid = active_edge_indices[tid];  // original edge grid index

    int voxels = grid_dims.x;  // 32
    int density_dim = voxels + 1;  // 33

    // Decode: gid = (z*V*V + y*V + x) * 3 + dir
    uint voxel_idx = gid / 3u;
    uint dir = gid % 3u;
    int z = int(voxel_idx) / (voxels * voxels);
    int rem = int(voxel_idx) % (voxels * voxels);
    int y = rem / voxels;
    int x = rem % voxels;

    // Endpoint world positions
    vec3 p0 = cell_origin + vec3(float(x), float(y), float(z)) * voxel_size;
    vec3 p1 = p0;
    if (dir == 0u) p1.x += voxel_size;
    else if (dir == 1u) p1.y += voxel_size;
    else p1.z += voxel_size;

    // Endpoint densities from the grid
    int d_idx0 = z * density_dim * density_dim + y * density_dim + x;
    float d0 = density[d_idx0];

    // Binary search: find the zero crossing between p0 (d0) and p1 (d1)
    float lo = 0.0;
    float hi = 1.0;
    float d_lo = d0;
    int d_idx1;
    if (dir == 0u) d_idx1 = d_idx0 + 1;
    else if (dir == 1u) d_idx1 = d_idx0 + density_dim;
    else d_idx1 = d_idx0 + density_dim * density_dim;
    float d_hi = density[d_idx1];

    for (int i = 0; i < BINARY_SEARCH_ITERS; i++) {
        float mid = (lo + hi) * 0.5;
        vec3 p_mid = mix(p0, p1, mid);
        float d_mid = dc_sample_field(p_mid.x, p_mid.y, p_mid.z);

        if ((d_lo >= 0.0) == (d_mid >= 0.0)) {
            lo = mid;
            d_lo = d_mid;
        } else {
            hi = mid;
            d_hi = d_mid;
        }
    }

    float t = (lo + hi) * 0.5;
    t = clamp(t, 0.001, 0.999);
    vec3 pos = mix(p0, p1, t);

    // ---- Surface normal via central differences (fused from Stage 5) ----
    //
    // HALF A VOXEL, NOT A HUNDREDTH, AND THIS IS THE FILE THAT MATTERS.
    //
    // The same fix was written into surface_normals.glsl, which _compile_all
    // does not load: the two stages were fused into this one (see the header),
    // so that file is retired and editing it changed nothing that runs. Naming
    // the artifact a check examined is hyper-games #44's rule; this is the same
    // failure applied to a fix instead of a check.
    //
    // WHY A HUNDREDTH OF A VOXEL CANNOT WORK. evaluate_density reaches the
    // surface by subtracting two numbers that are both about 2.63 near the
    // ground -- the field's height and y_mapped, each (metres + 10900) * 2.4e-4
    // -- and only then multiplies the difference by 4200. Float32 has about
    // 2.4e-7 of resolution at 2.63, so a 1 cm horizontal probe on gentle ground
    // asks for a difference far below one ULP.
    //
    // MEASURED, tools/oracles/density_precision.py, float32, the shipping
    // expression, voxel 1 m, against a float64 control:
    //
    //   slope   eps=0.01 voxel        eps=0.5 voxel
    //   0.003   n.x EXACTLY 0.0       4.005e-03   (0.06 deg err)
    //   0.010   n.x EXACTLY 0.0       1.001e-02   (0.00)
    //   0.030   n.x EXACTLY 0.0       2.904e-02   (0.05)
    //   0.100   2.003e-03  (10x the   1.001e-01   (0.01)
    //           true 2.0e-04)
    //   0.300   4.16 deg error        3.004e-01   (0.03)
    //
    // A zero n.x hands the QEF a plane that says the ground is level when it is
    // not. Above the collapse the survivor is QUANTISED: n.x moves in steps of
    // ~2e-3 against an n.y of 0.02, so the normal can only point in directions
    // about 5.7 degrees apart, and a smooth hillside gets a dithered normal
    // field -- the scaled, dimpled look, with isolated wrong-facing triangles
    // where rounding tips one over.
    //
    // It is WORSE ON SMOOTH GROUND, which is why it shows on the baked
    // heightfield map and not on the erosion map: erosion puts more than 5.7
    // degrees of variation between neighbouring cells everywhere, so the
    // quantisation is masked. A Catmull-Rom heightfield over 30 m texels does
    // not.
    //
    // Half a voxel costs nothing DC could have represented -- it resolves at
    // one voxel by construction -- and brush faces are unaffected: the exact
    // analytic plane normal replaces this one below for convex hits, where it
    // survives only as a sign reference.
    float eps = voxel_size * 0.5;
    vec3 n;
    n.x = dc_sample_field(pos.x + eps, pos.y, pos.z) - dc_sample_field(pos.x - eps, pos.y, pos.z);
    n.y = dc_sample_field(pos.x, pos.y + eps, pos.z) - dc_sample_field(pos.x, pos.y - eps, pos.z);
    n.z = dc_sample_field(pos.x, pos.y, pos.z + eps) - dc_sample_field(pos.x, pos.y, pos.z - eps);

    float len = length(n);
    if (len > 1e-8) {
        n /= len;
    } else {
        n = vec3(0.0, 1.0, 0.0);  // fallback
    }

    // --- EXACT convex plane normal (feeds the qef_solve plane-snap) ----------
    // If `pos` lies on a convex brush face, replace the grid/noise-contaminated
    // central-difference normal with the brush's EXACT analytic plane normal
    // (argmax_i nᵢ·p − dᵢ). This gives the mesh stage clean, identical per-face
    // normals so the qef_solve clustering (0.999 tight) reconstructs the exact
    // planes for the plane-snap. ONLY convex crossings (etype 8/9) are touched —
    // organic/box/sphere normals stay central-diff (noisy → never cluster tight).
    uint hit_brush = 0u;         // which convex brush this crossing lies on (0 = none)
    uint hit_mat = 0u;           // that brush's material_id (rides GpuEdit._pad0)
    vec3 cdn = n;                // central-diff normal, kept for sign reference
    vec4 hit_pl = vec4(0.0);     // exact active brush face plane (nx,ny,nz,d)
    float min_absf = voxel_size * 1.8;   // crossings within ~2 voxels of a brush face
                                         // (0.5→1.0→1.8) — the override sets the EXACT
                                         // ray-plane crossing for these cells, so even
                                         // the edge cells the scalar brick rounds ~1-2
                                         // voxels off get exact Hermite → the snap lands
                                         // on the exact plane/crease → sawtooth (and the
                                         // geometric material-bleed at boundary tris)
                                         // clears. Safe: qef_solve's safety net gates the
                                         // vertex MOVE (tiny, since crossings are now
                                         // exact), not |f|; kept just under its 2.0 limit.
    for (uint ei = 0u; ei < edit_count; ei++) {
        GpuEdit ce = gpu_edits[ei];
        if (ce.edit_type != 8u && ce.edit_type != 9u) continue;
        vec3 dcv = pos - vec3(ce.pos_x, ce.pos_y, ce.pos_z);
        if (dot(dcv, dcv) > ce.radius * ce.radius) continue;
        uint poff = ce._pad1;
        uint pcnt = ce._pad2;
        float best = -1e30;
        vec4 best_pl = vec4(0.0);
        for (uint pi = 0u; pi < pcnt; pi++) {
            vec4 pl = convex_planes[poff + pi];
            float dp = pl.x * pos.x + pl.y * pos.y + pl.z * pos.z - pl.w;
            if (dp > best) { best = dp; best_pl = pl; }   // argmax = active convex face
        }
        // Pick the brush whose surface is CLOSEST to this crossing (smallest |f|) —
        // the actual ACTIVE surface in a dense/overlapping brush region.
        if (abs(best) < min_absf) {
            min_absf = abs(best);
            hit_pl = best_pl;
            hit_brush = ei + 1u;
            hit_mat = ce._pad0;              // material_id for the shader (route ②)
        }
    }
    // STEP 1 — EXACT ANALYTIC HERMITE (the Cepero path). If this crossing lies on
    // a convex brush face, discard the binary-searched position (which rides the
    // blended/noisy union density field) and the central-diff normal, and replace
    // them with the EXACT ray-vs-plane intersection along this edge + the exact
    // plane normal. Feeding exact Hermite to the QEF lets Dual Contouring
    // reproduce flat faces + razor edges/corners WITHOUT any downstream snap
    // (verified: test_hermite_qef.cpp). Guard: skip if the edge is ~parallel to
    // the face (no clean crossing) and keep the sampled fallback.
    if (hit_brush != 0u) {
        vec3 edir = p1 - p0;
        float denom = dot(hit_pl.xyz, edir);
        if (abs(denom) > 1e-8) {
            float te = clamp((hit_pl.w - dot(hit_pl.xyz, p0)) / denom, 0.0, 1.0);
            pos = p0 + te * edir;                          // exact crossing position
        }
        n = (dot(hit_pl.xyz, cdn) < 0.0) ? -hit_pl.xyz : hit_pl.xyz;  // exact normal
    }
    // ------------------------------------------------------------------------

    // Write to compacted output: position + normal + edge index
    uint dest = edge_scan[gid];
    uint base = dest * 8u;  // 8 floats per edge

    edge_data[base + 0u] = pos.x;
    edge_data[base + 1u] = pos.y;
    edge_data[base + 2u] = pos.z;
    edge_data[base + 3u] = n.x;
    edge_data[base + 4u] = n.y;
    edge_data[base + 5u] = n.z;
    edge_data[base + 6u] = uintBitsToFloat(gid);  // edge index for QEF
    // Pack brush id (high bits: hard-cell flag + per-brush confinement) with the
    // material_id (low 12 bits) into one slot — no extra buffer. qef_solve unpacks:
    // hit_brush = v>>12, material = v&0xFFF. Organic (hit_brush 0, mat 0) packs to 0.
    edge_data[base + 7u] = uintBitsToFloat((hit_brush << 12u) | (hit_mat & 0xFFFu));
}