#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[]; };
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
// =====================================================================
// 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 = evaluate_density(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) ----
float eps = voxel_size * 0.01;
vec3 n;
n.x = evaluate_density(pos.x + eps, pos.y, pos.z) - evaluate_density(pos.x - eps, pos.y, pos.z);
n.y = evaluate_density(pos.x, pos.y + eps, pos.z) - evaluate_density(pos.x, pos.y - eps, pos.z);
n.z = evaluate_density(pos.x, pos.y, pos.z + eps) - evaluate_density(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));
}