17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-31 12:19:06 +01:00

Build view-space light cluster grid in a compute pass

This commit is contained in:
2026-10-15 14:16:55 +02:00
parent c0f45184b8
commit f85cf5fe52
11 changed files with 488 additions and 0 deletions

66
gl/cluster_grid.cpp Normal file
View File

@@ -0,0 +1,66 @@
#include "stdafx.h"
#include "cluster_grid.h"
#include <cstring>
gl::cluster_grid::cluster_grid()
: m_aabbs(sizeof(cluster_aabb) * CLUSTER_COUNT, CLUSTER_AABB_SSBO_BINDING)
, m_ranges(sizeof(cluster_range) * CLUSTER_COUNT, CLUSTER_RANGE_SSBO_BINDING)
, m_indices(sizeof(uint32_t) * CLUSTER_COUNT * MAX_LIGHTS_PER_CLUSTER, CLUSTER_INDEX_SSBO_BINDING)
, m_counters(sizeof(cluster_counters), CLUSTER_COUNTER_SSBO_BINDING)
, m_params_buffer(sizeof(cluster_ubs), CLUSTER_UBO_BINDING)
{
// no light is assigned to any cluster until the culling pass runs
std::vector<uint8_t> const empty(sizeof(cluster_range) * CLUSTER_COUNT, 0);
m_ranges.update(empty.data(), 0, static_cast<GLsizeiptr>(empty.size()));
m_counters.update(cluster_counters{});
m_params_buffer.update(m_params);
}
void gl::cluster_grid::bind()
{
m_aabbs.bind_base();
m_ranges.bind_base();
m_indices.bind_base();
m_counters.bind_base();
m_params_buffer.bind_uniform();
}
bool gl::cluster_grid::update(const glm::mat4 &projection, float znear, float zfar, glm::vec2 viewport_size)
{
cluster_ubs params = m_params;
params.viewport_width = viewport_size.x;
params.viewport_height = viewport_size.y;
// clip space w is a function of the position in a perspective projection, and a constant in an orthographic one
bool const perspective{glm::length(glm::vec3{projection[0][3], projection[1][3], projection[2][3]}) > 1e-4f};
bool const usable{perspective && znear > 0.f && zfar > znear};
params.valid = usable ? 1 : 0;
if (usable && (projection != m_params.projection || znear != m_params.znear || zfar != m_params.zfar))
{
params.projection = projection;
params.znear = znear;
params.zfar = zfar;
params.slice_scale = static_cast<float>(CLUSTER_GRID_Z) / std::log(zfar / znear);
params.slice_bias = -std::log(znear) * params.slice_scale;
m_boxes_stale = true;
}
if (std::memcmp(&params, &m_params, sizeof(params)) != 0)
{
m_params = params;
m_params_buffer.update(m_params);
}
return usable && m_boxes_stale;
}
void gl::cluster_grid::build(program &build_program)
{
bind();
build_program.dispatch((CLUSTER_COUNT + CLUSTER_BUILD_GROUP_SIZE - 1) / CLUSTER_BUILD_GROUP_SIZE);
// the boxes are read by compute passes which follow, from the same shader storage buffer
memory_barrier(GL_SHADER_STORAGE_BARRIER_BIT);
m_boxes_stale = false;
}

57
gl/cluster_grid.h Normal file
View File

@@ -0,0 +1,57 @@
#pragma once
#include <glm/glm.hpp>
#include "ssbo.h"
#include "ubo.h"
#include "shader.h"
namespace gl
{
// GPU resources of the light cluster grid of one viewport. the grid cuts the view frustum of a pass into
// CLUSTER_GRID_X x CLUSTER_GRID_Y screen tiles and CLUSTER_GRID_Z slices of depth, spaced exponentially
// between znear and zfar (layout and binding points: ubo.h). depth is the clip space w of a point, which is
// also 1.0 / gl_FragCoord.w of its fragment and, unlike the view space z, is right for tilted screens.
// the buffers have a fixed size, so the grid survives changes of the resolution, fov and draw range: those
// only make its parameters and boxes be recomputed.
// bindings of the buffers are context state and the buffers are per viewport, so bind() has to be done in
// the context and before every pass which uses the grid
class cluster_grid
{
public:
cluster_grid();
void bind();
// refreshes the parameters for a pass: its projection, depth range and the size of the render target.
// returns true if the boxes of the clusters no longer match them and build() has to run
bool update(const glm::mat4 &projection, float znear, float zfar, glm::vec2 viewport_size);
// recomputes the boxes of the clusters in view space with the program made from cluster_build.comp.
// binds the grid, and leaves the boxes ready to be read by the compute passes which follow
void build(program &build_program);
// false if the last update found no grid possible for the pass (orthographic projection)
bool valid() const
{
return m_params.valid != 0;
}
const cluster_ubs &params() const
{
return m_params;
}
private:
// cluster_aabb per cluster
ssbo m_aabbs;
// cluster_range per cluster
ssbo m_ranges;
// light indices of all clusters, allocated from m_counters.index_count
ssbo m_indices;
// cluster_counters
ssbo m_counters;
ubo m_params_buffer;
cluster_ubs m_params{};
bool m_boxes_stale = true;
};
} // namespace gl

View File

@@ -16,6 +16,17 @@ void gl::glsl_common_setup()
"#define MAX_PARAMS " + std::to_string(MAX_PARAMS) + "U\n" +
"#define MAX_INSTANCES_PER_BATCH " + std::to_string(MAX_INSTANCES_PER_BATCH) + "U\n" +
"#define LIGHT_SSBO_BINDING " + std::to_string(LIGHT_SSBO_BINDING) + "\n" +
"#define CLUSTER_GRID_X " + std::to_string(CLUSTER_GRID_X) + "\n" +
"#define CLUSTER_GRID_Y " + std::to_string(CLUSTER_GRID_Y) + "\n" +
"#define CLUSTER_GRID_Z " + std::to_string(CLUSTER_GRID_Z) + "\n" +
"#define CLUSTER_COUNT " + std::to_string(CLUSTER_COUNT) + "\n" +
"#define MAX_LIGHTS_PER_CLUSTER " + std::to_string(MAX_LIGHTS_PER_CLUSTER) + "\n" +
"#define CLUSTER_BUILD_GROUP_SIZE " + std::to_string(CLUSTER_BUILD_GROUP_SIZE) + "\n" +
"#define CLUSTER_UBO_BINDING " + std::to_string(CLUSTER_UBO_BINDING) + "\n" +
"#define CLUSTER_AABB_SSBO_BINDING " + std::to_string(CLUSTER_AABB_SSBO_BINDING) + "\n" +
"#define CLUSTER_RANGE_SSBO_BINDING " + std::to_string(CLUSTER_RANGE_SSBO_BINDING) + "\n" +
"#define CLUSTER_INDEX_SSBO_BINDING " + std::to_string(CLUSTER_INDEX_SSBO_BINDING) + "\n" +
"#define CLUSTER_COUNTER_SSBO_BINDING " + std::to_string(CLUSTER_COUNTER_SSBO_BINDING) + "\n" +
R"STRING(
const uint LIGHT_SPOT = 0U;
const uint LIGHT_POINT = 1U;
@@ -56,6 +67,60 @@ void gl::glsl_common_setup()
};
#endif
// light clusters, see gl/cluster_grid.h
struct cluster_aabb_s
{
vec4 lower;
vec4 upper;
};
struct cluster_range_s
{
uint offset;
uint count;
};
#if defined(STAGE_FRAGMENT) || defined(STAGE_COMPUTE)
// parameters of the grid for the pass being drawn
layout(std140, binding = CLUSTER_UBO_BINDING) uniform cluster_ubo
{
mat4 cluster_projection;
float cluster_znear;
float cluster_zfar;
float cluster_slice_scale;
float cluster_slice_bias;
vec2 cluster_viewport_size;
uint cluster_valid;
};
// the compute passes fill the cluster buffers, shading only reads them
#ifdef STAGE_COMPUTE
#define CLUSTER_SSBO_ACCESS
layout(std430, binding = CLUSTER_AABB_SSBO_BINDING) buffer cluster_aabb_ssbo
{
cluster_aabb_s cluster_aabbs[];
};
layout(std430, binding = CLUSTER_COUNTER_SSBO_BINDING) buffer cluster_counter_ssbo
{
uint cluster_index_count;
uint cluster_overflow_count;
};
#else
#define CLUSTER_SSBO_ACCESS readonly
#endif
layout(std430, binding = CLUSTER_RANGE_SSBO_BINDING) CLUSTER_SSBO_ACCESS buffer cluster_range_ssbo
{
cluster_range_s cluster_ranges[];
};
layout(std430, binding = CLUSTER_INDEX_SSBO_BINDING) CLUSTER_SSBO_ACCESS buffer cluster_index_ssbo
{
uint cluster_light_indices[];
};
#endif
layout (std140) uniform model_ubo
{
mat4 modelview;

View File

@@ -177,5 +177,87 @@ namespace gl
static_assert(offsetof(light_data, lights) == 32, "bad layout of light_data");
static_assert(sizeof(light_data) == 32 + sizeof(light_element) * MAX_LIGHTS, "bad size of light_data");
// light clusters (see gl/cluster_grid.h): the view frustum is cut into CLUSTER_GRID_X x CLUSTER_GRID_Y screen
// tiles and CLUSTER_GRID_Z depth slices, spaced exponentially between the near and far plane of the pass.
// cluster index = x + CLUSTER_GRID_X * (y + CLUSTER_GRID_Y * z), with x and y counted from the lower left corner
const GLuint CLUSTER_GRID_X = 16;
const GLuint CLUSTER_GRID_Y = 9;
const GLuint CLUSTER_GRID_Z = 24;
const GLuint CLUSTER_COUNT = CLUSTER_GRID_X * CLUSTER_GRID_Y * CLUSTER_GRID_Z;
// capacity of the light list of a single cluster
const GLuint MAX_LIGHTS_PER_CLUSTER = 64;
// work group size of cluster_build.comp, which handles one cluster per invocation
const GLuint CLUSTER_BUILD_GROUP_SIZE = 64;
// binding points of the cluster buffers: layout(binding = ...) in glsl_common.
// the uniform block numbers go together with scene (0), model (1) and instance (3) blocks
const GLuint CLUSTER_UBO_BINDING = 2;
const GLuint CLUSTER_AABB_SSBO_BINDING = 1;
const GLuint CLUSTER_RANGE_SSBO_BINDING = 2;
const GLuint CLUSTER_INDEX_SSBO_BINDING = 3;
const GLuint CLUSTER_COUNTER_SSBO_BINDING = 4;
// layout std140: grid parameters for the pass it was built for (GLSL: cluster_ubo)
struct cluster_ubs
{
glm::mat4 projection;
float znear;
float zfar;
// depth slice of a point with clip space w: floor(log(w) * slice_scale + slice_bias), znear..zfar covers all slices
float slice_scale;
float slice_bias;
// size of the render target the tiles are laid over, in pixels
float viewport_width;
float viewport_height;
// 0 if there is no grid for the pass (orthographic projection)
uint32_t valid;
UBS_PAD(4);
};
static_assert(sizeof(cluster_ubs) == 96, "bad size of cluster_ubs");
static_assert(offsetof(cluster_ubs, projection) == 0, "bad layout of cluster_ubs");
static_assert(offsetof(cluster_ubs, znear) == 64, "bad layout of cluster_ubs");
static_assert(offsetof(cluster_ubs, zfar) == 68, "bad layout of cluster_ubs");
static_assert(offsetof(cluster_ubs, slice_scale) == 72, "bad layout of cluster_ubs");
static_assert(offsetof(cluster_ubs, slice_bias) == 76, "bad layout of cluster_ubs");
static_assert(offsetof(cluster_ubs, viewport_width) == 80, "bad layout of cluster_ubs");
static_assert(offsetof(cluster_ubs, viewport_height) == 84, "bad layout of cluster_ubs");
static_assert(offsetof(cluster_ubs, valid) == 88, "bad layout of cluster_ubs");
// layout std430: bounding box of a cluster in view space, element of cluster_aabb_ssbo (GLSL: cluster_aabb_s), w unused
struct cluster_aabb
{
glm::vec4 lower;
glm::vec4 upper;
};
static_assert(sizeof(cluster_aabb) == 32, "bad size of cluster_aabb");
static_assert(offsetof(cluster_aabb, lower) == 0, "bad layout of cluster_aabb");
static_assert(offsetof(cluster_aabb, upper) == 16, "bad layout of cluster_aabb");
// layout std430: lights of a cluster, element of cluster_range_ssbo (GLSL: cluster_range_s). the lights are
// light_data.lights indices stored in the cluster_index_ssbo list from offset on
struct cluster_range
{
uint32_t offset;
uint32_t count;
};
static_assert(sizeof(cluster_range) == 8, "bad size of cluster_range");
static_assert(offsetof(cluster_range, offset) == 0, "bad layout of cluster_range");
static_assert(offsetof(cluster_range, count) == 4, "bad layout of cluster_range");
// layout std430: cluster_counter_ssbo. index_count is the allocator of the shared index list, overflow_count
// the number of lights dropped from clusters which were full
struct cluster_counters
{
uint32_t index_count;
uint32_t overflow_count;
};
static_assert(sizeof(cluster_counters) == 8, "bad size of cluster_counters");
static_assert(offsetof(cluster_counters, index_count) == 0, "bad layout of cluster_counters");
static_assert(offsetof(cluster_counters, overflow_count) == 4, "bad layout of cluster_counters");
#pragma pack(pop)
}