const BRICK_SIZE: u32 = 8u;
const BRICK_CORNER_COUNT: u32 = 9u;
struct ComputeUniforms {
brick_count: u32,
terrain_enabled: u32,
terrain_base_height: f32,
terrain_material_id: u32,
edit_count: u32,
smoothness_scale: f32,
terrain_seed: u32,
terrain_octaves: u32,
terrain_frequency: f32,
terrain_amplitude: f32,
terrain_gain: f32,
_pad0: u32,
_pad1: u32,
_pad2: u32,
_pad3: u32,
_pad4: u32,
}
struct GpuSdfEdit {
inverse_transform: mat4x4<f32>,
primitive_type: u32,
operation_type: u32,
material_id: u32,
smoothness: f32,
uniform_scale: f32,
param0: f32,
param1: f32,
param2: f32,
param3: f32,
_pad0: u32,
_pad1: u32,
_pad2: u32,
}
struct DirtyBrick {
world_origin: vec3<f32>,
voxel_size: f32,
atlas_slot: u32,
material_atlas_x: u32,
material_atlas_y: u32,
material_atlas_z: u32,
}
@group(0) @binding(0) var<uniform> uniforms: ComputeUniforms;
@group(0) @binding(1) var<storage, read> edits: array<GpuSdfEdit>;
@group(0) @binding(2) var<storage, read> dirty_bricks: array<DirtyBrick>;
@group(0) @binding(3) var brick_atlas: texture_storage_3d<r32float, write>;
@group(0) @binding(4) var material_atlas: texture_storage_3d<r32uint, write>;
@group(0) @binding(5) var<storage, read_write> surface_flags: array<atomic<u32>>;
fn sdf_sphere(point: vec3<f32>, radius: f32) -> f32 {
return length(point) - radius;
}
fn sdf_box(point: vec3<f32>, half_extents: vec3<f32>) -> f32 {
let q = abs(point) - half_extents;
return length(max(q, vec3<f32>(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0);
}
fn sdf_cylinder(point: vec3<f32>, radius: f32, half_height: f32) -> f32 {
let d_xz = length(point.xz) - radius;
let d_y = abs(point.y) - half_height;
return length(max(vec2<f32>(d_xz, d_y), vec2<f32>(0.0))) + min(max(d_xz, d_y), 0.0);
}
fn sdf_torus(point: vec3<f32>, major_radius: f32, minor_radius: f32) -> f32 {
let q = vec2<f32>(length(point.xz) - major_radius, point.y);
return length(q) - minor_radius;
}
fn sdf_capsule(point: vec3<f32>, radius: f32, half_height: f32) -> f32 {
let clamped_y = clamp(point.y, -half_height, half_height);
let closest = vec3<f32>(0.0, clamped_y, 0.0);
return length(point - closest) - radius;
}
fn sdf_plane(point: vec3<f32>, normal: vec3<f32>, offset: f32) -> f32 {
return dot(point, normal) - offset;
}
fn evaluate_primitive(edit: GpuSdfEdit, local_point: vec3<f32>) -> f32 {
var distance: f32;
switch edit.primitive_type {
case 0u: {
distance = sdf_sphere(local_point, edit.param0);
}
case 1u: {
distance = sdf_box(local_point, vec3<f32>(edit.param0, edit.param1, edit.param2));
}
case 2u: {
distance = sdf_cylinder(local_point, edit.param0, edit.param1);
}
case 3u: {
distance = sdf_torus(local_point, edit.param0, edit.param1);
}
case 4u: {
distance = sdf_capsule(local_point, edit.param0, edit.param1);
}
case 5u: {
distance = sdf_plane(local_point, vec3<f32>(edit.param0, edit.param1, edit.param2), edit.param3);
}
default: {
distance = 1000.0;
}
}
return distance * edit.uniform_scale;
}
fn smooth_min(a: f32, b: f32, k: f32) -> f32 {
if k <= 0.0 {
return min(a, b);
}
let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
return mix(b, a, h) - k * h * (1.0 - h);
}
fn apply_csg(distance_a: f32, distance_b: f32, operation_type: u32, smoothness: f32) -> f32 {
switch operation_type {
case 0u: {
return min(distance_a, distance_b);
}
case 1u: {
return max(distance_a, -distance_b);
}
case 2u: {
return max(distance_a, distance_b);
}
case 3u: {
return smooth_min(distance_a, distance_b, smoothness);
}
case 4u: {
return -smooth_min(-distance_a, distance_b, smoothness);
}
case 5u: {
return -smooth_min(-distance_a, -distance_b, smoothness);
}
default: {
return min(distance_a, distance_b);
}
}
}
fn apply_csg_with_material(
distance_a: f32,
material_a: u32,
distance_b: f32,
material_b: u32,
operation_type: u32,
smoothness: f32,
dither: f32,
) -> vec2<f32> {
var new_distance: f32;
var new_material_f: f32;
switch operation_type {
case 0u: {
new_distance = min(distance_a, distance_b);
new_material_f = select(f32(material_b), f32(material_a), distance_a < distance_b);
}
case 1u: {
new_distance = max(distance_a, -distance_b);
new_material_f = f32(material_a);
}
case 2u: {
new_distance = max(distance_a, distance_b);
new_material_f = select(f32(material_b), f32(material_a), distance_a > distance_b);
}
case 3u: {
let h = clamp(0.5 + 0.5 * (distance_b - distance_a) / smoothness, 0.0, 1.0);
new_distance = mix(distance_b, distance_a, h) - smoothness * h * (1.0 - h);
new_material_f = select(f32(material_b), f32(material_a), h > dither);
}
case 4u: {
let h = clamp(0.5 - 0.5 * (distance_a + distance_b) / smoothness, 0.0, 1.0);
new_distance = mix(distance_a, -distance_b, h) + smoothness * h * (1.0 - h);
new_material_f = f32(material_a);
}
case 5u: {
let h = clamp(0.5 - 0.5 * (distance_b - distance_a) / smoothness, 0.0, 1.0);
new_distance = mix(distance_b, distance_a, h) + smoothness * h * (1.0 - h);
new_material_f = select(f32(material_b), f32(material_a), h > dither);
}
default: {
new_distance = min(distance_a, distance_b);
new_material_f = select(f32(material_b), f32(material_a), distance_a < distance_b);
}
}
return vec2<f32>(new_distance, new_material_f);
}
fn position_hash(pos: vec3<f32>) -> f32 {
let x = sin(pos.x * 127.1 + pos.y * 311.7 + pos.z * 74.7) * 43758.5453;
return abs(fract(x));
}
fn terrain_hash(ix: i32, iz: i32, seed: u32) -> f32 {
var n = (ix * 1619) ^ (iz * 6971) ^ (i32(seed) * 1013);
n = n * n * n;
n = n ^ (n >> 13);
return f32(n & 0x7fffffff) / 2147483647.0;
}
fn noised_2d(px: f32, pz: f32, seed: u32) -> vec3<f32> {
let cell_x = i32(floor(px));
let cell_z = i32(floor(pz));
let wx = px - f32(cell_x);
let wz = pz - f32(cell_z);
let ux = wx * wx * wx * (wx * (wx * 6.0 - 15.0) + 10.0);
let uz = wz * wz * wz * (wz * (wz * 6.0 - 15.0) + 10.0);
let dux = 30.0 * wx * wx * (wx * (wx - 2.0) + 1.0);
let duz = 30.0 * wz * wz * (wz * (wz - 2.0) + 1.0);
let h00 = terrain_hash(cell_x, cell_z, seed);
let h10 = terrain_hash(cell_x + 1, cell_z, seed);
let h01 = terrain_hash(cell_x, cell_z + 1, seed);
let h11 = terrain_hash(cell_x + 1, cell_z + 1, seed);
let k0 = h00;
let k1 = h10 - h00;
let k2 = h01 - h00;
let k3 = h00 - h10 - h01 + h11;
let value = -1.0 + 2.0 * (k0 + k1 * ux + k2 * uz + k3 * ux * uz);
let dvdx = 2.0 * dux * (k1 + k3 * uz);
let dvdz = 2.0 * duz * (k2 + k3 * ux);
return vec3<f32>(value, dvdx, dvdz);
}
fn terrain_fbm(world_pos: vec3<f32>) -> f32 {
var px = world_pos.x * uniforms.terrain_frequency;
var pz = world_pos.z * uniforms.terrain_frequency;
var total = 0.0;
var amplitude = uniforms.terrain_amplitude;
var dx_sum = 0.0;
var dz_sum = 0.0;
for (var octave = 0u; octave < uniforms.terrain_octaves; octave += 1u) {
let n = noised_2d(px, pz, uniforms.terrain_seed);
dx_sum += n.y * 0.5;
dz_sum += n.z * 0.5;
let dampening = 1.0 / (1.0 + dx_sum * dx_sum + dz_sum * dz_sum);
total += amplitude * n.x * dampening;
let new_px = 1.6 * px - 1.2 * pz;
let new_pz = 1.2 * px + 1.6 * pz;
px = new_px;
pz = new_pz;
amplitude *= uniforms.terrain_gain;
}
return world_pos.y - uniforms.terrain_base_height - total;
}
fn atlas_base_for_slot(slot: u32, atlas_size: u32) -> vec3<u32> {
let bricks_per_dim = atlas_size / BRICK_CORNER_COUNT;
let bricks_per_slice = bricks_per_dim * bricks_per_dim;
let z = slot / bricks_per_slice;
let remainder = slot % bricks_per_slice;
let y = remainder / bricks_per_dim;
let x = remainder % bricks_per_dim;
return vec3<u32>(x * BRICK_CORNER_COUNT, y * BRICK_CORNER_COUNT, z * BRICK_CORNER_COUNT);
}
@compute @workgroup_size(128)
fn evaluate_corners(@builtin(global_invocation_id) global_id: vec3<u32>) {
let total_corners_per_brick = BRICK_CORNER_COUNT * BRICK_CORNER_COUNT * BRICK_CORNER_COUNT;
let brick_index = global_id.x / total_corners_per_brick;
let corner_index = global_id.x % total_corners_per_brick;
if brick_index >= uniforms.brick_count {
return;
}
let brick = dirty_bricks[brick_index];
let corner_z = corner_index / (BRICK_CORNER_COUNT * BRICK_CORNER_COUNT);
let remainder = corner_index % (BRICK_CORNER_COUNT * BRICK_CORNER_COUNT);
let corner_y = remainder / BRICK_CORNER_COUNT;
let corner_x = remainder % BRICK_CORNER_COUNT;
let world_pos = brick.world_origin + vec3<f32>(f32(corner_x), f32(corner_y), f32(corner_z)) * brick.voxel_size;
var distance: f32;
if uniforms.terrain_enabled != 0u {
distance = terrain_fbm(world_pos);
} else {
distance = 3.40282e+38;
}
for (var edit_index = 0u; edit_index < uniforms.edit_count; edit_index += 1u) {
let edit = edits[edit_index];
let local_point = (edit.inverse_transform * vec4<f32>(world_pos, 1.0)).xyz;
let edit_distance = evaluate_primitive(edit, local_point);
let scaled_smoothness = edit.smoothness * uniforms.smoothness_scale;
distance = apply_csg(distance, edit_distance, edit.operation_type, scaled_smoothness);
}
let atlas_base = atlas_base_for_slot(brick.atlas_slot, textureDimensions(brick_atlas).x);
let atlas_coord = vec3<i32>(atlas_base) + vec3<i32>(i32(corner_x), i32(corner_y), i32(corner_z));
textureStore(brick_atlas, atlas_coord, vec4<f32>(distance, 0.0, 0.0, 0.0));
if distance <= 0.0 {
atomicOr(&surface_flags[brick_index], 1u);
} else {
atomicOr(&surface_flags[brick_index], 2u);
}
}
@compute @workgroup_size(128)
fn evaluate_materials(@builtin(global_invocation_id) global_id: vec3<u32>) {
let total_voxels_per_brick = BRICK_SIZE * BRICK_SIZE * BRICK_SIZE;
let brick_index = global_id.x / total_voxels_per_brick;
let voxel_index = global_id.x % total_voxels_per_brick;
if brick_index >= uniforms.brick_count {
return;
}
let brick = dirty_bricks[brick_index];
let voxel_z = voxel_index / (BRICK_SIZE * BRICK_SIZE);
let remainder = voxel_index % (BRICK_SIZE * BRICK_SIZE);
let voxel_y = remainder / BRICK_SIZE;
let voxel_x = remainder % BRICK_SIZE;
let world_pos = brick.world_origin
+ vec3<f32>(f32(voxel_x) + 0.5, f32(voxel_y) + 0.5, f32(voxel_z) + 0.5) * brick.voxel_size;
let dither = position_hash(world_pos);
var distance: f32;
var material: u32;
if uniforms.terrain_enabled != 0u {
distance = terrain_fbm(world_pos);
material = uniforms.terrain_material_id;
} else {
distance = 3.40282e+38;
material = 0u;
}
for (var edit_index = 0u; edit_index < uniforms.edit_count; edit_index += 1u) {
let edit = edits[edit_index];
let local_point = (edit.inverse_transform * vec4<f32>(world_pos, 1.0)).xyz;
let edit_distance = evaluate_primitive(edit, local_point);
let scaled_smoothness = edit.smoothness * uniforms.smoothness_scale;
let result = apply_csg_with_material(distance, material, edit_distance, edit.material_id, edit.operation_type, scaled_smoothness, dither);
distance = result.x;
material = u32(result.y);
}
let material_coord = vec3<i32>(
i32(brick.material_atlas_x + voxel_x),
i32(brick.material_atlas_y + voxel_y),
i32(brick.material_atlas_z + voxel_z)
);
textureStore(material_atlas, material_coord, vec4<u32>(material, 0u, 0u, 0u));
}