struct CullingUniforms {
view_projection: mat4x4<f32>,
camera_position: vec4<f32>,
lod_distances: vec4<f32>,
lod_density_scales: vec4<f32>,
total_instances: u32,
max_visible: u32,
time: f32,
_padding: f32,
}
struct GrassInstance {
position: vec3<f32>,
rotation: f32,
height: f32,
width: f32,
species_index: u32,
lod: u32,
base_color: vec4<f32>,
tip_color: vec4<f32>,
bend: vec2<f32>,
_padding: vec2<f32>,
}
struct DrawIndirect {
vertex_count: u32,
instance_count: atomic<u32>,
first_vertex: u32,
first_instance: u32,
}
@group(0) @binding(0) var<uniform> uniforms: CullingUniforms;
@group(0) @binding(1) var<storage, read> instances: array<GrassInstance>;
@group(0) @binding(2) var<storage, read_write> visible_indices: array<u32>;
@group(0) @binding(3) var<storage, read_write> draw_command: DrawIndirect;
fn compute_density_scale(distance: f32) -> f32 {
let d0 = uniforms.lod_distances.x;
let d1 = uniforms.lod_distances.y;
let d2 = uniforms.lod_distances.z;
let d3 = uniforms.lod_distances.w;
let s0 = uniforms.lod_density_scales.x;
let s1 = uniforms.lod_density_scales.y;
let s2 = uniforms.lod_density_scales.z;
let s3 = uniforms.lod_density_scales.w;
if distance < d0 {
return s0;
} else if distance < d1 {
let t = (distance - d0) / (d1 - d0);
return mix(s0, s1, t);
} else if distance < d2 {
let t = (distance - d1) / (d2 - d1);
return mix(s1, s2, t);
} else if distance < d3 {
let t = (distance - d2) / (d3 - d2);
return mix(s2, s3, t);
}
return 0.0;
}
fn density_cull(instance_index: u32, density_scale: f32) -> bool {
if density_scale >= 1.0 {
return false;
}
if density_scale <= 0.0 {
return true;
}
let hash = fract(sin(f32(instance_index) * 12.9898) * 43758.5453);
return hash > density_scale;
}
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
let instance_index = global_id.x;
if instance_index >= uniforms.total_instances {
return;
}
let instance = instances[instance_index];
if instance.height <= 0.0 {
return;
}
let blade_center = instance.position + vec3<f32>(0.0, instance.height * 0.5, 0.0);
let blade_radius = max(instance.height, instance.width);
let clip = uniforms.view_projection * vec4<f32>(blade_center, 1.0);
let w = clip.w;
if w <= 0.001 {
return;
}
let expand = blade_radius * 1.5;
let padded_w = w + expand;
if clip.x < -padded_w || clip.x > padded_w {
return;
}
if clip.y < -padded_w || clip.y > padded_w {
return;
}
if clip.z < -expand || clip.z > padded_w {
return;
}
let distance = length(blade_center - uniforms.camera_position.xyz);
let density_scale = compute_density_scale(distance);
if density_cull(instance_index, density_scale) {
return;
}
let slot = atomicAdd(&draw_command.instance_count, 1u);
if slot >= uniforms.max_visible {
atomicSub(&draw_command.instance_count, 1u);
return;
}
visible_indices[slot] = instance_index;
}
@compute @workgroup_size(1)
fn reset_draw_command() {
atomicStore(&draw_command.instance_count, 0u);
}