#version 450
// COMPUTE SHADER - Frustum Culling
// This compute shader performs frustum culling on the GPU.
// It checks each instance's bounding sphere against the view-projection frustum.
// Instances that are visible are written to the visible indices buffer.
// Each workgroup processes 256 instances in parallel
layout(local_size_x = 256) in;
// Per-instance data from the physics buffer
struct InstanceData {
mat4 model; // Model matrix (also contains position in column 3)
vec4 color; // Color and emissive
vec4 mat_props; // Material properties
vec4 velocity; // Physics velocity
vec4 physic_props; // Physics properties
vec4 angular_velocity; // Angular velocity
};
// INPUT BUFFER - Instance data (read from)
// Read-only buffer with all instance transforms
layout(set = 0, binding = 0) readonly buffer Instances {
InstanceData data[];
} inst_in;
// OUTPUT BUFFERS - Results (written to)
// Write-only buffer storing indices of visible instances
layout(set = 0, binding = 1) writeonly buffer VisibleIndices {
uint data[];
} indices_out;
// Indirect buffer for draw command - only visible instances are drawn
layout(set = 0, binding = 2) buffer IndirectBuffer {
uint indexCount; // Number of indices (not used by draw call)
uint instanceCount; // Number of VISIBLE instances (updated by this shader)
uint firstIndex; // First index to draw
int vertexOffset; // Vertex buffer offset
uint firstInstance; // Base instance ID for draw
} indirect_out;
// PUSH CONSTANTS - Per-dispatch parameters
layout(push_constant) uniform CullParams {
mat4 view_proj; // Combined view-projection matrix
uint batch_offset; // Starting instance ID for this batch
uint batch_count; // Total instances to cull in this dispatch
uint visible_list_offset; // Offset into visible list to write
} pc;
// MAIN ENTRY POINT
void main() {
// Get global thread ID (0 to batch_count-1)
uint local_id = gl_GlobalInvocationID.x;
// Early exit if beyond the batch count
if (local_id >= pc.batch_count) return;
// Calculate instance index in the buffer
uint global_id = local_id + pc.batch_offset;
// Extract object center position from the model matrix (column 3, rows 0-2)
vec3 center = inst_in.data[global_id].model[3].xyz;
// Calculate the bounding sphere radius of this object
// The radius is the half of the largest axis of the model matrix
float sx = length(inst_in.data[global_id].model[0].xyz);
float sy = length(inst_in.data[global_id].model[1].xyz);
float sz = length(inst_in.data[global_id].model[2].xyz);
float radius = max(sx, max(sy, sz)) * 1.5;
// Transform the center to clip space using view-projection matrix
vec4 clip = pc.view_proj * vec4(center, 1.0);
// ============================================================================
// FRUSTUM CULLING TEST
// ============================================================================
// First check: is the object in front of the camera?
bool visible = clip.w >= -radius;
// Second check: is the bounding sphere inside the NDC frustum?
if (visible) {
// Prevent div-by-zero or inversion when exactly at clip plane
float w_safe = max(abs(clip.w), 0.001);
// Convert to normalized device coordinates (-1 to 1)
vec3 ndc = clip.xyz / w_safe;
// Add a margin based on the object radius (larger objects need more margin)
float ndc_margin = radius / w_safe;
// Test against the 6 frustum planes
// Allow object to extend slightly beyond frustum (ndc_margin)
visible = ndc.x >= -(1.0 + ndc_margin) && ndc.x <= (1.0 + ndc_margin)
&& ndc.y >= -(1.0 + ndc_margin) && ndc.y <= (1.0 + ndc_margin)
&& ndc.z >= -ndc_margin && ndc.z <= (1.0 + ndc_margin);
}
// WRITE VISIBLE INSTANCE
// If visible, reserve a slot and write the instance index
if (visible) {
// Atomically increment the instance count to get a unique slot
uint slot = atomicAdd(indirect_out.instanceCount, 1);
// Write this instance's index to the visible list
indices_out.data[slot + pc.visible_list_offset] = global_id;
}
}