#version 460 core const uint MAX_FRAG_TEX_UNITS = 32u; const uint MAX_NUM_LIGHTS = 100u; const float PI = 3.14159265358979; in VS_OUT { vec3 pos; vec3 tan_pos; vec2 uv; flat uint draw_id; mat3 TBN; } fs_in; out vec4 out_colour; struct point_light { vec4 position; vec4 colour; float intensity; float radius; }; struct cluster { vec4 min_point; vec4 max_point; uint count; uint light_indices[MAX_NUM_LIGHTS]; }; layout(std430, binding = 1) readonly buffer cluster_buf { cluster clusters[]; }; layout(std430, binding = 2) readonly buffer lights_buf { point_light point_lights[]; }; uniform mat4 view_mat; uniform float z_near; uniform float z_far; uniform uvec3 grid_size; uniform uvec2 screen_dimensions; uniform vec3 view_pos; // Camera world position uniform sampler2DArray packed_tex[MAX_FRAG_TEX_UNITS]; // z index = 0: Albedo, 1: Normal, 2: Metallic-Roughness vec3 get_normal(); float trowbridge_reitz(vec3 normal, vec3 halfway, float roughness); float schlick(float n_dot_v, float roughness); float smith(vec3 normal, vec3 view, vec3 light, float roughness); vec3 fresnel_schlick(float cos_theta, vec3 F0); void main() { vec3 albedo = texture(packed_tex[fs_in.draw_id % MAX_FRAG_TEX_UNITS], vec3(fs_in.uv, 0.0)).rgb; vec3 normal = get_normal(); vec3 met_rough = texture(packed_tex[fs_in.draw_id % MAX_FRAG_TEX_UNITS], vec3(fs_in.uv, 2.0)).rgb; float metallic = met_rough.b; float roughness = met_rough.g; vec3 tangent_frag_pos = fs_in.tan_pos; vec3 tangent_view_pos = fs_in.TBN * view_pos; vec3 view_dir = normalize(tangent_view_pos - tangent_frag_pos); // Normal incidence reflectance vec3 F0 = mix(vec3(0.04), albedo, metallic); // Find light cluster index // Position of this fragment in view space vec3 view_space_pos = vec3(view_mat * vec4(fs_in.pos, 1.0)); // Locating which cluster this fragment is part of uint z_tile = uint((log(abs(view_space_pos.z) / z_near) * grid_size.z) / log(z_far / z_near)); vec2 tile_size = screen_dimensions / grid_size.xy; uvec3 tile = uvec3(gl_FragCoord.xy / tile_size, z_tile); uint tile_idx = tile.x + (tile.y * grid_size.x) + (tile.z * grid_size.x * grid_size.y); // Shade fragment for each light uint n_lights = clusters[tile_idx].count; vec3 light_reflected = vec3(0.0, 0.0, 0.0); for (uint i = 0u; i < n_lights; ++i) { uint light_idx = clusters[tile_idx].light_indices[i]; point_light light = point_lights[light_idx]; vec3 tangent_light_pos = fs_in.TBN * vec3(light.position); // Per-light radiance vec3 light_dir = normalize(tangent_light_pos - tangent_frag_pos); vec3 halfway = normalize(view_dir + light_dir); float dist = length(tangent_light_pos - tangent_frag_pos); float attenuation = light.intensity / (dist * dist); vec3 radiance = vec3(light.colour) * attenuation; // Cook-Torrance BRDF float NDF = trowbridge_reitz(normal, halfway, roughness); float G = smith(normal, view_dir, light_dir, roughness); vec3 F = fresnel_schlick(clamp(dot(halfway, view_dir), 0.0, 1.0), F0); vec3 numerator = NDF * G * F; float denominator = 4.0 * max(dot(normal, view_dir), 0.0) * max(dot(normal, light_dir), 0.0) + 0.0001; // + 0.0001 to prevent divide by zero vec3 specular = numerator / denominator; // kS is equal to Fresnel vec3 kS = F; // for energy conservation, the diffuse and specular light can't // be above 1.0 (unless the surface emits light); to preserve this // relationship the diffuse component (kD) should equal 1.0 - kS. vec3 kD = vec3(1.0) - kS; // multiply kD by the inverse metalness such that only non-metals // have diffuse lighting, or a linear blend if partly metal (pure metals // have no diffuse light). kD *= 1.0 - metallic; // scale light by NdotL float n_dot_l = max(dot(normal, light_dir), 0.0); // add to outgoing radiance Lo light_reflected += (kD * albedo / PI + specular) * radiance * n_dot_l; // note that we already multiplied the BRDF by the Fresnel (kS) so we won't multiply by kS again } // Don't need to gamma-correct as PNG textures are already gamma corrected (?) // const float screen_gamma = 2.2; // light_collect = pow(light_collect, vec3(1.0 / screen_gamma)); vec3 ambient = vec3(0.03) * albedo; out_colour = vec4(ambient, 1.0) + vec4(light_reflected, 1.0); } vec3 get_normal() { vec3 normal_map_sample = texture(packed_tex[fs_in.draw_id % MAX_FRAG_TEX_UNITS], vec3(fs_in.uv, 1.0)).rgb; // Transform normal vector to range [-1, 1] return normalize(normal_map_sample * 2.0 - 1.0); // Tangent-space } vec3 cook_torrance(vec3 albedo) { return albedo; } // Normal distribution function (a.k.a. GGX) float trowbridge_reitz(vec3 normal, vec3 halfway, float roughness) { float a_squared = roughness * roughness; float n_dot_h = max(dot(normal, halfway), 0.0); // how closely the normal & halfway align float n_dot_h_squared = n_dot_h * n_dot_h; float nom = a_squared; float denom = (n_dot_h_squared * (a_squared - 1.0) + 1.0); denom = PI * denom * denom; return nom / denom; } // Geometry function (self-shadowing) float schlick(float n_dot_v, float roughness) { float nom = n_dot_v; float denom = n_dot_v * (1.0 - roughness) + roughness; return nom / denom; } // Geometry approximation using Schlick-GGX float smith(vec3 normal, vec3 view, vec3 light, float roughness) { float n_dot_v = max(dot(normal, view), 0.0); float n_dot_l = max(dot(normal, light), 0.0); float ggx_1 = schlick(n_dot_v, roughness); float ggx_2 = schlick(n_dot_l, roughness); return ggx_1 * ggx_2; } // Fresnel effect vec3 fresnel_schlick(float cos_theta, vec3 F0) { return F0 + (1.0 - F0) * pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0); }