#version 450 //CONSTANTS const int MAX_LIGHTS = 8192; #define MAX_LIGHTS_IN_TILE 1024 const vec2 ndc_begin = vec2(-1.0, -1.0); //STRUCTS struct Frustum { vec4 planes[6]; vec3 vertices[8]; }; struct LightsInTile { uint indices[MAX_LIGHTS_IN_TILE]; uint total; }; struct PointLight { vec3 color; float luminance; vec3 position; }; //SHARED VARIABLES shared Frustum tile_frustum; shared uint current_light_count; shared float tile_depth_low; shared float tile_depth_high; //SHADER BUFERS layout(push_constant) uniform ViewportConstants { vec4 discarded1; ivec2 viewport_size; ivec2 tile_size; uint horizontal_tile_count; uint vertical_tile_count; uint discarded2; uint discarded3; } viewport_constants; layout(std140, set = 0, binding = 0) uniform CameraUBO { mat4 projection; mat4 view; vec3 position; }camera; layout(std140, set = 1, binding =0) buffer PointLights { PointLight lights[MAX_LIGHTS]; }; layout(std430, set = 1, binding = 1) buffer writeonly LightsPerTileSSBO { LightsInTile lights_in_tile[]; }; layout(set = 2, binding = 0) uniform sampler2D depth_sampler; //NON-CONST VARIABLES ivec2 tile_subdivision = ivec2(viewport_constants.horizontal_tile_count, viewport_constants.vertical_tile_count); //SHADER FUNCTIONS Frustum CreateTileFrustum(ivec2 tile) { //Inverted projection-view matrix. Ideally should be precalculated on the CPU side. mat4 inverted_pv = inverse(camera.projection * camera.view); //Size of tiled scaled down to normalised device coordinates. vec2 tile_size_NDC = 2.0 * vec2(viewport_constants.tile_size) / viewport_constants.viewport_size; //Get the vertices for the given tile, used to construct the planes of the viewing frustum. vec2 tile_vertices[4]; tile_vertices[0] = ndc_begin + tile * tile_size_NDC; tile_vertices[1] = vec2(tile_vertices[0].x + tile_size_NDC.x, tile_vertices[0].y); tile_vertices[2] = tile_vertices[0] + tile_size_NDC; tile_vertices[3] = vec2(tile_vertices[0].x, tile_vertices[0].y + tile_size_NDC.y); Frustum frustum; vec4 plane = vec4(1); for (int i = 0; i < 4; i++) { plane = inverted_pv * vec4(tile_vertices[i], tile_depth_low, 1.0); frustum.vertices[i] = plane.xyz / plane.w; plane = inverted_pv * vec4(tile_vertices[i], tile_depth_high, 1.0); frustum.vertices[i + 4] = plane.xyz / plane.w; } vec3 normal_to_plane = vec3(1); //0-3 surrounding planes for (int i = 0; i < 4; i++) { normal_to_plane = cross(frustum.vertices[i] - camera.position, frustum.vertices[i + 1] - camera.position); normal_to_plane = normalize(normal_to_plane); frustum.planes[i] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[i])); } //last two are near/far plane normal_to_plane = cross(frustum.vertices[1] - frustum.vertices[0], frustum.vertices[3] - frustum.vertices[0]); normal_to_plane = normalize(normal_to_plane); frustum.planes[4] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[0])); normal_to_plane = cross(frustum.vertices[7] - frustum.vertices[4], frustum.vertices[5] - frustum.vertices[4]); normal_to_plane = normalize(normal_to_plane); frustum.planes[5] = vec4(normal_to_plane, - dot(normal_to_plane, frustum.vertices[4])); return frustum; } bool LightToFrustumCollision(PointLight light, Frustum frustum) { bool sphere_to_plane = true; for (int i = 0; i < 6; i++) { if (dot(light.position, frustum.planes[i].xyz) + frustum.planes[i].w < - light.luminance ) { sphere_to_plane = false; break; } } //Sphere to plane test failed, early return to avoid further checks. if (!sphere_to_plane) return false; //Moving onto boundiing volume test, as a sphere to plane test is not always accurate. vec3 min_bounds = light.position - vec3(light.luminance); vec3 max_bounds = light.position + vec3(light.luminance); //X CHECKS int current_vertex = 0; for(int i=0; i<8;++i) { if(frustum.vertices[i].x > max_bounds.x) ++current_vertex; if(current_vertex == 8) return false; } current_vertex=0; for(int i=0;i<8;++i) { if(frustum.vertices[i].x < min_bounds.x) ++current_vertex; if(current_vertex == 8) return false; } //Y CHECKS current_vertex=0; for(int i=0;i<8;++i) { if(frustum.vertices[i].y > max_bounds.y)++current_vertex; if(current_vertex == 8) return false; } current_vertex=0; for(int i=0; i<8;++i) { if(frustum.vertices[i].y < min_bounds.y) ++current_vertex; if(current_vertex == 8) return false; } //Z CHECKS current_vertex=0; for(int i=0;i<8;++i) { if(frustum.vertices[i].z > max_bounds.z)++current_vertex; if(current_vertex == 8) return false; } current_vertex=0; for(int i=0;i<8;++i) { if(frustum.vertices[i].z < min_bounds.z)++current_vertex; if(current_vertex == 8) return false; } //Sphere to plane test failed, box test failed, light is in tile. return true; } void main() { //Extract the current tile based on the tile coordinates. ivec2 tile_coords = ivec2(gl_WorkGroupID.xy); uint tile_index = tile_coords.y * tile_subdivision.x + tile_coords.x; if (gl_LocalInvocationIndex == 0) { //First invocation, must sample depth buffer to reduce overhead of checking the entire depth range. tile_depth_low = 1.0f; tile_depth_high = 0.0f; for (int y = 0; y < viewport_constants.tile_size.y; y++) { for (int x = 0; x < viewport_constants.tile_size.x; x++) { vec2 z_buffer_location = (vec2(viewport_constants.tile_size) * tile_coords + vec2(x, y) ) / viewport_constants.viewport_size; float z_buffer_sample = texture(depth_sampler, z_buffer_location).x; tile_depth_low = min(tile_depth_low, z_buffer_sample); tile_depth_high = max(tile_depth_high, z_buffer_sample); } } //Suggestion by http://www.lighthouse3d.com/tutorials/view-frustum-culling/ if (tile_depth_low >= tile_depth_high) { tile_depth_low = tile_depth_high; } tile_frustum = CreateTileFrustum(tile_coords); current_light_count = 0; } barrier(); for (uint i = gl_LocalInvocationIndex; i < MAX_LIGHTS && current_light_count < MAX_LIGHTS_IN_TILE; i += gl_WorkGroupSize.x) { if (LightToFrustumCollision(lights[i], tile_frustum)) { //The tile is only affacted by this light if it collides and is within the allowed maximum of lights per tile. uint current_light_index = atomicAdd(current_light_count, 1); if (current_light_index >= MAX_LIGHTS_IN_TILE)break; lights_in_tile[tile_index].indices[current_light_index] = i; } } barrier(); if (gl_LocalInvocationIndex == 0) { lights_in_tile[tile_index].total = min(MAX_LIGHTS_IN_TILE, current_light_count); } }