initial commit
All checks were successful
Build (Arch Linux) / build (push) Successful in 3m10s

This commit is contained in:
2025-04-16 01:58:29 +01:00
commit a8d8b9b9ab
116 changed files with 106633 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
#version 460 core
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
const uint MAX_NUM_LIGHTS = 100u;
struct cluster {
vec4 min_point;
vec4 max_point;
uint count;
uint light_indices[MAX_NUM_LIGHTS];
};
layout(std430, binding = 1) restrict buffer cluster_buf {
cluster clusters[];
};
uniform float z_near;
uniform float z_far;
uniform mat4 inverse_proj_mat;
uniform uvec3 grid_size;
uniform uvec2 screen_dimensions;
vec3 screen_to_view(vec2 screen_coord);
vec3 line_intersection_with_z_plane(vec3 start_point, vec3 end_point, float z_distance);
/*
context: glViewport is referred to as the "screen"
clusters are built based on a 2d screen-space grid and depth slices.
Later when shading, it is easy to figure what cluster a fragment is in based on
gl_FragCoord.xy and the fragment's z depth from camera
*/
void main() {
uint tile_idx =
gl_WorkGroupID.x + (gl_WorkGroupID.y * grid_size.x) + (gl_WorkGroupID.z * grid_size.x * grid_size.y);
vec2 tile_size = screen_dimensions / grid_size.xy;
// tile in screen-space
vec2 min_tile_screen = gl_WorkGroupID.xy * tile_size;
vec2 max_tile_screen = (gl_WorkGroupID.xy + 1) * tile_size;
// convert tile to view space sitting on the near plane
vec3 min_tile = screen_to_view(min_tile_screen);
vec3 max_tile = screen_to_view(max_tile_screen);
float plane_near = z_near * pow(z_far / z_near, gl_WorkGroupID.z / float(grid_size.z));
float plane_far = z_near * pow(z_far / z_near, (gl_WorkGroupID.z + 1) / float(grid_size.z));
// the line goes from the eye position in view space (0, 0, 0)
// through the min/max points of a tile to intersect with a given cluster's near-far planes
vec3 min_point_near = line_intersection_with_z_plane(vec3(0, 0, 0), min_tile, plane_near);
vec3 min_point_far = line_intersection_with_z_plane(vec3(0, 0, 0), min_tile, plane_far);
vec3 max_point_near = line_intersection_with_z_plane(vec3(0, 0, 0), max_tile, plane_near);
vec3 max_point_far = line_intersection_with_z_plane(vec3(0, 0, 0), max_tile, plane_far);
clusters[tile_idx].min_point = vec4(min(min_point_near, min_point_far), 0.0);
clusters[tile_idx].max_point = vec4(max(max_point_near, max_point_far), 0.0);
}
// Returns the intersection point of an infinite line and a
// plane perpendicular to the Z-axis
vec3 line_intersection_with_z_plane(vec3 start_point, vec3 end_point, float z_distance) {
vec3 direction = end_point - start_point;
vec3 normal = vec3(0.0, 0.0, -1.0); // plane normal
// skip check if the line is parallel to the plane.
float t = (z_distance - dot(normal, start_point)) / dot(normal, direction);
return start_point + t * direction; // the parametric form of the line equation
}
vec3 screen_to_view(vec2 screen_coord) {
// normalize screen_coord to [-1, 1] and
// set the NDC depth of the coordinate to be on the near plane. This is -1 by
// default in OpenGL
vec4 ndc = vec4(screen_coord / screen_dimensions * 2.0 - 1.0, -1.0, 1.0);
vec4 view_coord = inverse_proj_mat * ndc;
view_coord /= view_coord.w;
return view_coord.xyz;
}

View File

@@ -0,0 +1,73 @@
#version 460 core
const uint LOCAL_SIZE = 128u;
layout(local_size_x = LOCAL_SIZE, local_size_y = 1, local_size_z = 1) in;
struct point_light {
vec4 position;
vec4 colour;
float intensity;
float radius;
};
const uint MAX_NUM_LIGHTS = 100u;
struct cluster {
vec4 min_point;
vec4 max_point;
uint count;
uint light_indices[MAX_NUM_LIGHTS];
};
layout(std430, binding = 1) restrict buffer cluster_buf {
cluster clusters[];
};
layout(std430, binding = 2) restrict buffer lights_buf {
point_light point_lights[];
};
uniform mat4 view_mat;
bool test_sphere_aabb(uint i, cluster c);
// each invocation of main() is a thread processing a cluster
void main() {
uint n_lights = point_lights.length();
uint cluster_idx = gl_WorkGroupID.x * LOCAL_SIZE + gl_LocalInvocationID.x;
cluster c = clusters[cluster_idx];
// we need to reset count because culling runs every frame.
// otherwise it would accumulate.
c.count = 0;
for (uint i = 0; i < n_lights; ++i) {
if (test_sphere_aabb(i, c) && c.count < 100) {
c.light_indices[c.count] = i;
c.count++;
}
}
clusters[cluster_idx] = c;
}
bool sphere_aabb_intersection(vec3 center, float radius, vec3 aabb_min, vec3 aabb_max) {
// closest point on the AABB to the sphere center
vec3 closest_point = clamp(center, aabb_min, aabb_max);
// squared distance between the sphere center and closest point
float distance_squared = dot(closest_point - center, closest_point - center);
return distance_squared <= radius * radius;
}
// this just unpacks data for sphere_aabb_intersection
bool test_sphere_aabb(uint i, cluster c) {
vec3 center = vec3(view_mat * point_lights[i].position);
float radius = point_lights[i].radius;
vec3 aabb_min = c.min_point.xyz;
vec3 aabb_max = c.max_point.xyz;
return sphere_aabb_intersection(center, radius, aabb_min, aabb_max);
}

View File

@@ -0,0 +1,40 @@
#version 460 core
layout(location = 0) in vec3 pos;
layout(location = 1) in vec3 norm;
layout(location = 2) in vec3 tangent;
layout(location = 3) in vec2 uv;
layout(location = 4) in uint draw_id;
layout(location = 5) in mat4 model_mat; // also occupies location 5, 6, 7 & 8
layout(location = 9) in mat3 normal_mat;
out VS_OUT {
vec3 pos;
vec3 tan_pos;
vec2 uv;
flat uint draw_id;
mat3 TBN;
}
vs_out;
// Uniforms
uniform mat4 view_proj_mat; // View-projection matrix
void main() {
// Tangent-space matrix
vec3 T = normalize(normal_mat * tangent);
vec3 N = normalize(normal_mat * norm);
T = normalize(T - dot(T, N) * N); // Re-orthogonalisation
vec3 B = cross(N, T);
mat3 TBN = transpose(mat3(T, B, N));
vec4 world_pos = model_mat * vec4(pos, 1.0);
vs_out.pos = vec3(world_pos) / world_pos.w; // World-space position
vs_out.tan_pos = TBN * vs_out.pos; // Tangent-space position
vs_out.uv = uv; // Texture coordinates
vs_out.draw_id = draw_id; // MDI draw ID
vs_out.TBN = TBN; // World-space -> tangent-space matrix
gl_Position = view_proj_mat * world_pos;
}

179
assets/shaders/pbr.frag Normal file
View File

@@ -0,0 +1,179 @@
#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);
}