Newer
Older
ForwardPlusRenderer / shaders / LightComputeShader.comp
#version 450

//CONSTANTS
const int MAX_LIGHTS = 4098;
#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);
	}
}