17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-01 06:52:45 +01:00

Cull lights into clusters in a compute pass

This commit is contained in:
2026-10-15 15:21:21 +02:00
parent f85cf5fe52
commit 6aa6e505e8
8 changed files with 368 additions and 48 deletions

151
shaders/cluster_cull.comp Normal file
View File

@@ -0,0 +1,151 @@
// assigns the lights to the light clusters, see gl/cluster_grid.h
// one invocation per cluster: tests every light of light_ssbo (the sun, slot 0, is handled apart from the clusters)
// against the bounding box of the cluster in view space, and writes the indices of the lights which reach it.
// run every frame after the lights were updated, for the grid of the pass about to be drawn
#include <common>
layout(local_size_x = CLUSTER_CULL_GROUP_SIZE) in;
// a light is taken to end where its brightest possible contribution drops under this, a fraction of
// what the display can tell apart. no light reaches further than MAX_RANGE, as with the 1 km filter on the cpu side
const float MIN_CONTRIBUTION = 1.0 / 1024.0;
const float MAX_RANGE = 1000.0;
// the boxes and the light data are floats: planes of the headlight projection let this much (in meters) of a box through
const float PLANE_MARGIN = 0.05;
// brightest value the light adds to a surface it hits, as the shading computes it
float light_peak(light_s light)
{
float peak = max(light.color.r, max(light.color.g, light.color.b)) * light.intensity;
if (light.type == LIGHT_HEADLIGHTS)
// weights are the state of the individual lamps and the beam texture, which is not brighter than 1, picks from them
return peak * max(light.headlight_weights.r, max(light.headlight_weights.g, light.headlight_weights.b));
return peak * (1.0 + light.ambient);
}
// distance from the light at which its contribution falls under MIN_CONTRIBUTION, 0 for a light which has none
float light_range(light_s light)
{
float peak = light_peak(light);
if (!(peak > MIN_CONTRIBUTION))
return 0.0;
// attenuation which the contribution can drop to without being lost: 1 / k
float k = peak / MIN_CONTRIBUTION;
if (light.type == LIGHT_HEADLIGHTS)
{
// 1 + linear * d + quadratic * d^2 <= k, the positive root written the way which has no cancellation for a small quadratic
float a = light.quadratic;
float b = light.linear;
float c = 1.0 - k;
float denominator = b + sqrt(b * b - 4.0 * a * c);
return (denominator > 1e-12) ? min(-2.0 * c / denominator, MAX_RANGE) : MAX_RANGE;
}
// point and spot lights fall off with the square of the distance
return min(sqrt(k), MAX_RANGE);
}
bool sphere_touches_box(vec3 center, float radius, vec3 lower, vec3 upper)
{
vec3 offset = center - clamp(center, lower, upper);
return dot(offset, offset) <= radius * radius;
}
// whether any part of the box, given by its center and half extents, is on the positive side of the plane
bool box_reaches_plane(vec4 plane, vec3 center, vec3 extent)
{
return dot(plane.xyz, center) + plane.w + dot(abs(plane.xyz), extent) + PLANE_MARGIN * length(plane.xyz) >= 0.0;
}
// planes bounding the beam of a headlight: the projection takes a view space position to the texture coordinates
// of the beam, and the beam is where those fall in 0..1 and the position is in front of the projector. clip z of that
// projection is a constant (reverse z, no far plane), so only the four sides and the near plane are real planes
bool box_in_headlight_beam(mat4 projection, vec3 center, vec3 extent)
{
vec4 row_x = vec4(projection[0][0], projection[1][0], projection[2][0], projection[3][0]);
vec4 row_y = vec4(projection[0][1], projection[1][1], projection[2][1], projection[3][1]);
vec4 row_z = vec4(projection[0][2], projection[1][2], projection[2][2], projection[3][2]);
vec4 row_w = vec4(projection[0][3], projection[1][3], projection[2][3], projection[3][3]);
return box_reaches_plane(row_x, center, extent)
&& box_reaches_plane(row_w - row_x, center, extent)
&& box_reaches_plane(row_y, center, extent)
&& box_reaches_plane(row_w - row_y, center, extent)
&& box_reaches_plane(row_z, center, extent)
&& box_reaches_plane(row_w - row_z, center, extent);
}
// whether the sphere touches the infinite cone with the apex at the origin, the unit axis and the cosine of the half angle
bool sphere_touches_cone(vec3 center, float radius, vec3 axis, float cosine)
{
float sine = max(sqrt(max(1.0 - cosine * cosine, 0.0)), 1e-4);
float along = dot(center, axis);
float across = sqrt(max(dot(center, center) - along * along, 0.0));
// distance to the boundary ray if the closest point of the cone is on it, and to the apex otherwise
float distance_to_cone = (along * cosine + across * sine > 0.0) ? across * cosine - along * sine : length(center);
return distance_to_cone <= radius;
}
bool light_reaches_box(light_s light, float range, vec3 lower, vec3 upper)
{
vec3 center = 0.5 * (lower + upper);
vec3 extent = 0.5 * (upper - lower);
if (light.type == LIGHT_DIR)
return true;
if (!sphere_touches_box(light.pos, range, lower, upper))
return false;
if (light.type == LIGHT_HEADLIGHTS)
return box_in_headlight_beam(light.headlight_projection, center, extent);
if (light.type == LIGHT_SPOT)
// the cone is tested against the sphere around the box, which lets some of the corners through
return sphere_touches_cone(center - light.pos, length(extent), normalize(light.dir), light.out_cutoff);
return true;
}
void main()
{
uint index = gl_GlobalInvocationID.x;
if (index >= uint(CLUSTER_COUNT))
return;
vec3 lower = cluster_aabbs[index].lower.xyz;
vec3 upper = cluster_aabbs[index].upper.xyz;
// the lights are in the order of their importance, so when a cluster is full the least important are the ones left out
uint found[MAX_LIGHTS_PER_CLUSTER];
uint found_count = 0U;
uint dropped = 0U;
uint lights_end = min(lights_total, uint(lights.length()));
for (uint i = 1U; i < lights_end; ++i)
{
float range = light_range(lights[i]);
if (range <= 0.0 && lights[i].type != LIGHT_DIR)
continue;
if (!light_reaches_box(lights[i], range, lower, upper))
continue;
if (found_count < uint(MAX_LIGHTS_PER_CLUSTER))
found[found_count++] = i;
else
++dropped;
}
uint offset = 0U;
if (found_count > 0U)
{
offset = atomicAdd(cluster_index_count, found_count);
for (uint k = 0U; k < found_count; ++k)
cluster_light_indices[offset + k] = found[k];
}
if (dropped > 0U)
atomicAdd(cluster_overflow_count, dropped);
cluster_ranges[index] = cluster_range_s(offset, found_count);
}

View File

@@ -1,5 +1,6 @@
// debug view of the light clusters: tints every pixel of the scene with the colour of its depth slice,
// and darkens every other screen tile to show the grid. drawn over the resolved image, see gl/cluster_grid.h
// debug view of the light clusters: tints every pixel of the scene with the colour of the number of lights which
// were assigned to its cluster (see gl/cluster_grid.h), pixels of clusters without lights stay as they are.
// drawn over the resolved image
in vec2 f_coords;
@@ -12,7 +13,7 @@ layout(location = 0) out vec4 out_color;
const float TINT = 0.55;
// blue (near) through green to red (far)
// blue (few) through green to red (many)
vec3 ramp(float t)
{
return clamp(vec3(1.5) - abs(4.0 * t - vec3(3.0, 2.0, 1.0)), 0.0, 1.0);
@@ -30,9 +31,14 @@ void main()
int slice = clamp(int(floor(log(w) * cluster_slice_scale + cluster_slice_bias)), 0, CLUSTER_GRID_Z - 1);
ivec2 tile = clamp(ivec2(floor(f_coords * vec2(CLUSTER_GRID_X, CLUSTER_GRID_Y))), ivec2(0), ivec2(CLUSTER_GRID_X - 1, CLUSTER_GRID_Y - 1));
vec3 color = ramp(float(slice) / float(CLUSTER_GRID_Z - 1));
if (((tile.x + tile.y) & 1) != 0)
color *= 0.8;
uint count = cluster_ranges[tile.x + CLUSTER_GRID_X * (tile.y + CLUSTER_GRID_Y * slice)].count;
if (count == 0U)
discard;
// a full cluster (lights beyond its capacity are dropped) is white; the rest goes by the logarithm of the count
vec3 color = (count >= uint(MAX_LIGHTS_PER_CLUSTER))
? vec3(1.0)
: ramp(log2(1.0 + float(count)) / log2(1.0 + float(MAX_LIGHTS_PER_CLUSTER)));
out_color = vec4(color, TINT);
}