#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));
}