17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 03:59:19 +02: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

View File

@@ -1,6 +1,7 @@
#include "stdafx.h"
#include "cluster_grid.h"
#include <algorithm>
#include <cstring>
gl::cluster_grid::cluster_grid()
@@ -17,6 +18,17 @@ gl::cluster_grid::cluster_grid()
m_params_buffer.update(m_params);
}
gl::cluster_grid::~cluster_grid()
{
for (auto &slot : m_readback)
{
// fences first, they belong to the copies
slot.copied.reset();
if (slot.buffer != 0)
glDeleteBuffers(1, &slot.buffer);
}
}
void gl::cluster_grid::bind()
{
m_aabbs.bind_base();
@@ -64,3 +76,53 @@ void gl::cluster_grid::build(program &build_program)
memory_barrier(GL_SHADER_STORAGE_BARRIER_BIT);
m_boxes_stale = false;
}
void gl::cluster_grid::cull(program &cull_program)
{
collect_stats();
bind();
// the counters are the allocator of the index list and the count of dropped lights: every pass starts them anew
m_counters.update(cluster_counters{});
cull_program.dispatch((CLUSTER_COUNT + CLUSTER_CULL_GROUP_SIZE - 1) / CLUSTER_CULL_GROUP_SIZE);
// the clusters are read by the shading, and the counters by the copy below and then by the next pass writing them
memory_barrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
// a copy of the counters for the cpu to read when the gpu gets there; if it is far behind, this frame is not reported
if (m_readback_used < READBACK_SLOTS)
{
auto &slot{m_readback[(m_readback_first + m_readback_used) % READBACK_SLOTS]};
if (slot.buffer == 0)
{
glCreateBuffers(1, &slot.buffer);
glNamedBufferStorage(slot.buffer, sizeof(cluster_counters), nullptr, GL_CLIENT_STORAGE_BIT);
}
glCopyNamedBufferSubData(m_counters, slot.buffer, 0, 0, sizeof(cluster_counters));
slot.copied = std::make_unique<fence>();
++m_readback_used;
}
}
void gl::cluster_grid::collect_stats()
{
// oldest first, as long as the gpu has finished the copies
while (m_readback_used > 0)
{
auto &slot{m_readback[m_readback_first]};
if (!slot.copied->is_signalled())
break;
cluster_counters counters{};
glGetNamedBufferSubData(slot.buffer, 0, sizeof(counters), &counters);
slot.copied.reset();
cull_stats stats{};
stats.index_count = counters.index_count;
stats.overflow_count = counters.overflow_count;
stats.overflow_peak = std::max(counters.overflow_count, m_stats ? m_stats->overflow_peak : 0U);
m_stats = stats;
m_readback_first = (m_readback_first + 1) % READBACK_SLOTS;
--m_readback_used;
}
}

View File

@@ -1,7 +1,12 @@
#pragma once
#include <array>
#include <memory>
#include <optional>
#include <glm/glm.hpp>
#include "fence.h"
#include "ssbo.h"
#include "ubo.h"
#include "shader.h"
@@ -19,7 +24,21 @@ namespace gl
class cluster_grid
{
public:
// what the culling pass reports about a frame
struct cull_stats
{
// entries of the shared index list in use
uint32_t index_count = 0;
// (cluster, light) pairs dropped because the cluster was full
uint32_t overflow_count = 0;
// highest overflow_count seen since the grid was created
uint32_t overflow_peak = 0;
};
cluster_grid();
~cluster_grid();
cluster_grid(const cluster_grid &) = delete;
cluster_grid &operator=(const cluster_grid &) = delete;
void bind();
@@ -29,6 +48,11 @@ namespace gl
// 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);
// assigns the lights of light_ssbo to the clusters with the program made from cluster_cull.comp: zeroes the
// counters, fills the (offset, count) of every cluster and the index list, and leaves them ready to be
// read by the shading and the debug view which follow. the boxes have to be current (update(), build()) and the
// grid valid. also queues the counters for reading back, see stats()
void cull(program &cull_program);
// false if the last update found no grid possible for the pass (orthographic projection)
bool valid() const
@@ -39,6 +63,12 @@ namespace gl
{
return m_params;
}
// counters of the most recent culling pass whose result has reached the cpu, which takes a few frames
// (read back without waiting for the gpu); empty until there is one
std::optional<cull_stats> stats() const
{
return m_stats;
}
private:
// cluster_aabb per cluster
@@ -53,5 +83,19 @@ namespace gl
cluster_ubs m_params{};
bool m_boxes_stale = true;
// counters copied for reading back: a queue of buffers, each with the fence of the copy into it
static constexpr size_t READBACK_SLOTS = 4;
struct readback_slot
{
GLuint buffer = 0;
std::unique_ptr<fence> copied;
};
void collect_stats();
std::array<readback_slot, READBACK_SLOTS> m_readback;
// oldest slot in use and the number of slots in use
size_t m_readback_first = 0;
size_t m_readback_used = 0;
std::optional<cull_stats> m_stats;
};
} // namespace gl

View File

@@ -22,6 +22,7 @@ void gl::glsl_common_setup()
"#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_CULL_GROUP_SIZE " + std::to_string(CLUSTER_CULL_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" +
@@ -59,8 +60,11 @@ void gl::glsl_common_setup()
layout(std430, binding = LIGHT_SSBO_BINDING) readonly buffer light_ssbo
{
vec3 ambient;
// lights stored in lights[], what the cluster culling goes through
uint lights_total;
vec3 fog_color;
// lights the shading loop goes through
uint lights_count;
light_s lights[];

View File

@@ -156,22 +156,26 @@ namespace gl
static_assert(offsetof(light_element, headlight_projection) == 64, "bad layout of light_element");
static_assert(offsetof(light_element, headlight_weights) == 128, "bad layout of light_element");
// capacity of light_data.lights; slot 0 is the sun
const size_t MAX_LIGHTS = 8;
// capacity of light_data.lights; slot 0 is the sun. a buffer size, not a limit of the shaders: they take the
// number of lights from the header
const size_t MAX_LIGHTS = 256;
// contents of light_ssbo: global lighting header followed by the light array
struct light_data
{
packed_vec3 ambient;
UBS_PAD(4);
// number of lights stored in lights[] (the sun included): what the light clusters are culled from
uint32_t lights_total;
packed_vec3 fog_color;
// number of lights (the sun included) the shading loop over lights[] goes through
uint32_t lights_count;
light_element lights[MAX_LIGHTS];
};
static_assert(offsetof(light_data, ambient) == 0, "bad layout of light_data");
static_assert(offsetof(light_data, lights_total) == 12, "bad layout of light_data");
static_assert(offsetof(light_data, fog_color) == 16, "bad layout of light_data");
static_assert(offsetof(light_data, lights_count) == 28, "bad layout of light_data");
static_assert(offsetof(light_data, lights) == 32, "bad layout of light_data");
@@ -186,8 +190,9 @@ namespace gl
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
// work group size of cluster_build.comp and cluster_cull.comp, which handle one cluster per invocation
const GLuint CLUSTER_BUILD_GROUP_SIZE = 64;
const GLuint CLUSTER_CULL_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
@@ -247,8 +252,9 @@ namespace gl
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
// layout std430: cluster_counter_ssbo, zeroed before every culling pass. index_count is the allocator of the shared
// index list, overflow_count the number of (cluster, light) pairs dropped because the cluster already held
// MAX_LIGHTS_PER_CLUSTER lights
struct cluster_counters
{
uint32_t index_count;

View File

@@ -127,16 +127,6 @@ bool opengl46_renderer::Init(GLFWwindow *Window)
Global.DayLight.is_directional = true;
m_sunlight.id = opengl46_renderer::sunlight;
// create dynamic light pool
for (int idx = 0; idx < Global.DynamicLightCount; ++idx)
{
opengl46_light light;
light.id = 1 + idx;
light.is_directional = false;
m_lights.emplace_back(light);
}
// preload some common textures
WriteLog("Loading common gfx data...");
m_glaretexture = Fetch_Texture("fx/lightglare");
@@ -173,6 +163,10 @@ bool opengl46_renderer::Init(GLFWwindow *Window)
gl::shader cluster_build("cluster_build.comp");
m_cluster_build_shader = std::make_unique<gl::program>(std::vector<std::reference_wrapper<const gl::shader>>{cluster_build});
}
{
gl::shader cluster_cull("cluster_cull.comp");
m_cluster_cull_shader = std::make_unique<gl::program>(std::vector<std::reference_wrapper<const gl::shader>>{cluster_cull});
}
m_cluster_heatmap_shader = make_shader("quad.vert", "cluster_heatmap.frag");
m_invalid_material = Fetch_Material("invalid");
}
@@ -661,6 +655,8 @@ void opengl46_renderer::SwapBuffers()
m_debugtimestext += "gpu frame total: " + to_string((double)(m_gllasttime / 1000ULL) / 1000.0, 3) + " ms\n";
if (auto const clusterstime = m_clusterstimer.result())
m_debugtimestext += " clusters: " + to_string(static_cast<double>(*clusterstime) / 1000000.0, 3) + " ms\n";
if (auto const culltime = m_cullstimer.result())
m_debugtimestext += " cull: " + to_string(static_cast<double>(*culltime) / 1000000.0, 3) + " ms\n";
m_debugtimestext += "uilayer: " + to_string( Timer::subsystem.gfx_gui.average(), 2 ) + " ms\n";
if( DebugModeFlag )
m_debugtimestext += m_textures.info();
@@ -696,7 +692,15 @@ void opengl46_renderer::SwapBuffers()
+ " =" + to_string( m_colorpass.draw_stats.shapes + shadowstats.shapes, 7 ) + "\n"
+ " traction: " + to_string( m_colorpass.draw_stats.traction, 7 ) + "\n"
+ " lines: " + to_string( m_colorpass.draw_stats.lines, 7 ) + "\n"
+ "particles: " + to_string( m_colorpass.draw_stats.particles, 7 );
+ "particles: " + to_string( m_colorpass.draw_stats.particles, 7 )
+ "\nlights: " + to_string( static_cast<int>( m_lights_lit ), 7 )
+ " (" + std::to_string( m_lights_dropped ) + " over capacity)";
if( auto const cullstats = ( m_viewports.empty() ? std::nullopt : m_viewports.front()->clusters->stats() ) ) {
m_debugstatstext +=
"\ncluster lists: " + std::to_string( cullstats->index_count )
+ ", overflow " + std::to_string( cullstats->overflow_count )
+ " (peak " + std::to_string( cullstats->overflow_peak ) + ")";
}
}
void opengl46_renderer::draw_debug_ui()
@@ -728,6 +732,12 @@ void opengl46_renderer::draw_debug_ui()
auto const &params{clusters->params()};
ImGui::Text("grid %u x %u x %u, %s", gl::CLUSTER_GRID_X, gl::CLUSTER_GRID_Y, gl::CLUSTER_GRID_Z, params.valid ? "built" : "none (orthographic view)");
ImGui::Text("near %.3f m, far %.1f m", params.znear, params.zfar);
ImGui::Text("lights %u (sun excluded), %u over capacity of %u", m_lights_lit, m_lights_dropped, static_cast<unsigned>(gl::MAX_LIGHTS - 1));
if (auto const cullstats = clusters->stats())
{
ImGui::Text("index list %u of %u", cullstats->index_count, gl::CLUSTER_COUNT * gl::MAX_LIGHTS_PER_CLUSTER);
ImGui::Text("overflow: %u lights dropped from full clusters (limit %u each), peak %u", cullstats->overflow_count, gl::MAX_LIGHTS_PER_CLUSTER, cullstats->overflow_peak);
}
}
}
ImGui::End();
@@ -918,6 +928,7 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode)
Update_Clusters( vp );
Update_Lights( simulation::Lights );
Cull_Lights( vp );
glDebug("render environment");
@@ -1896,7 +1907,14 @@ void opengl46_renderer::setup_sunlight_intensity( float const Factor ) {
m_sunlight.apply_intensity( Factor );
light_data.lights[ 0 ].intensity = m_sunlight.factor * (1.0f - Global.Overcast * 0.33f);
light_data.ambient = m_sunlight.ambient * m_sunlight.factor;
light_ssbo->update( light_data );
upload_light_header();
}
// the lighting values which change from object to object within a pass (the sun in tunnels, the ambient light in a cab):
// the header of the light buffer and the sun in its first slot. the headlights are written once per pass by Update_Lights()
void opengl46_renderer::upload_light_header()
{
light_ssbo->update( &light_data, 0, offsetof( gl::light_data, lights ) + sizeof( gl::light_element ) );
}
bool opengl46_renderer::Render(world_environment *Environment)
@@ -3862,7 +3880,7 @@ bool opengl46_renderer::Render_cab(TDynamicObject const *Dynamic, float const Li
if( Lightlevel > 0.f ) {
// crude way to light the cabin, until we have something more complete in place
light_data.ambient += packed_vec3( ( Dynamic->InteriorLight * Lightlevel ) * std::clamp( 1.25f - (float)luminance, 0.f, 1.f ) );
light_ssbo->update( light_data );
upload_light_header();
}
// render
@@ -5592,13 +5610,14 @@ void opengl46_renderer::Preview_Color(TAnimModel *Instance, float const Squaredi
auto const &modelview{OpenGLMatrices.data(GL_MODELVIEW)};
light_data.ambient = m_sunlight.ambient;
light_data.fog_color = glm::vec3{1.f};
light_data.lights_total = 1;
light_data.lights_count = 1;
light_data.lights[0].type = gl::light_element::DIR;
light_data.lights[0].dir = modelview * glm::vec4(m_sunlight.direction, 0.0f);
light_data.lights[0].color = m_sunlight.diffuse;
light_data.lights[0].ambient = 0.0f;
light_data.lights[0].intensity = 1.0f;
light_ssbo->update(light_data);
upload_light_header();
model_ubs.fog_density = 0.f;
model_ubs.future = glm::mat4();
model_ubs.emission = 0.f;
@@ -5903,16 +5922,15 @@ void opengl46_renderer::Update_Lights(light_array &Lights)
);
glm::mat4 mv = OpenGLMatrices.data( GL_MODELVIEW );
// fill vehicle headlights data
auto renderlight = m_lights.begin();
// fill vehicle headlights data, for every light which is on
// the light is worked out in a scratch object, nothing of it is kept for the next one
opengl46_light renderlight;
renderlight.is_directional = false;
size_t light_i = 1;
m_lights_dropped = 0;
for (auto const &scenelight : Lights.data)
{
if (renderlight == m_lights.end()) {
// we ran out of lights to assign
break;
}
if (scenelight.intensity == 0.f) {
// all lights past this one are bound to be off
break;
@@ -5923,9 +5941,14 @@ void opengl46_renderer::Update_Lights(light_array &Lights)
// but there could still be weaker lights which are closer, so keep looking
continue;
}
if (light_i >= gl::MAX_LIGHTS) {
// the buffer is full, and the lights come in the order of importance
++m_lights_dropped;
continue;
}
// if the light passed tests so far, it's good enough
renderlight->position = lightoffset;
renderlight->direction = scenelight.direction;
renderlight.position = lightoffset;
renderlight.direction = scenelight.direction;
auto luminance = static_cast<float>(Global.fLuminance);
// adjust luminance level based on vehicle's location, e.g. tunnels
@@ -5934,20 +5957,20 @@ void opengl46_renderer::Update_Lights(light_array &Lights)
{
luminance *= environment;
}
renderlight->diffuse = glm::vec4{glm::max(glm::vec3{colors::none}, scenelight.color - glm::vec3{luminance}), renderlight->diffuse[3]};
renderlight->ambient = glm::vec4{glm::max(glm::vec3{colors::none}, scenelight.color * glm::vec3{scenelight.intensity} - glm::vec3{luminance}), renderlight->ambient[3]};
renderlight.diffuse = glm::vec4{glm::max(glm::vec3{colors::none}, scenelight.color - glm::vec3{luminance}), renderlight.diffuse[3]};
renderlight.ambient = glm::vec4{glm::max(glm::vec3{colors::none}, scenelight.color * glm::vec3{scenelight.intensity} - glm::vec3{luminance}), renderlight.ambient[3]};
renderlight->apply_intensity( ( scenelight.count * 0.5 ) * ( scenelight.owner->DimHeadlights ? 0.5 : 1.0 ) );
renderlight->apply_angle();
renderlight.apply_intensity( ( scenelight.count * 0.5 ) * ( scenelight.owner->DimHeadlights ? 0.5 : 1.0 ) );
renderlight.apply_angle();
gl::light_element *light = &light_data.lights[light_i];
light->pos = mv * glm::vec4(renderlight->position, 1.0f);
light->dir = mv * glm::vec4(renderlight->direction, 0.0f);
light->pos = mv * glm::vec4(renderlight.position, 1.0f);
light->dir = mv * glm::vec4(renderlight.direction, 0.0f);
light->type = gl::light_element::HEADLIGHTS;
light->in_cutoff = headlight_config.in_cutoff;
light->out_cutoff = headlight_config.out_cutoff;
light->color = renderlight->diffuse * renderlight->factor;
light->color = renderlight.diffuse * renderlight.factor;
light->linear = headlight_config.falloff_linear / 10.0f;
light->quadratic = headlight_config.falloff_quadratic / 100.0f;
light->ambient = headlight_config.ambient;
@@ -6003,9 +6026,12 @@ void opengl46_renderer::Update_Lights(light_array &Lights)
light->headlight_weights = { scenelight.state, 0.f };
++light_i;
++renderlight;
}
light_data.lights_count = light_i;
// every light is there for the clusters to be culled from, while the shading loop over the lights keeps to the number
// of them it always went through
light_data.lights_total = static_cast<uint32_t>(light_i);
m_lights_lit = static_cast<uint32_t>(light_i - 1);
light_data.lights_count = 1 + std::min<uint32_t>(static_cast<uint32_t>(light_i - 1), static_cast<uint32_t>(std::max(Global.DynamicLightCount, 0)));
// fill sunlight data
light_data.ambient = m_sunlight.ambient * m_sunlight.factor;// *simulation::Environment.light_intensity();
light_data.lights[0].type = gl::light_element::DIR;
@@ -6026,7 +6052,8 @@ void opengl46_renderer::Update_Lights(light_array &Lights)
}
// ship config data to the gpu
model_ubo->update(model_ubs);
light_ssbo->update(light_data);
// the header and the lights in use, the rest of the buffer is not read
light_ssbo->update(&light_data, 0, offsetof(gl::light_data, lights) + sizeof(gl::light_element) * light_i);
}
void opengl46_renderer::Update_Clusters(viewport_config &vp)
@@ -6044,6 +6071,20 @@ void opengl46_renderer::Update_Clusters(viewport_config &vp)
}
}
void opengl46_renderer::Cull_Lights(viewport_config &vp)
{
glDebug("Cull_Lights");
auto &grid{*vp.clusters};
// without a grid (orthographic view) there is nothing to cull the lights into
if (!grid.valid())
return;
m_cullstimer.begin();
grid.cull(*m_cluster_cull_shader);
m_cullstimer.end();
}
void opengl46_renderer::Render_cluster_heatmap(viewport_config &vp, gl::framebuffer *Target, glm::ivec2 const &Targetsize)
{
glDebug("Render_cluster_heatmap");
@@ -6159,7 +6200,6 @@ bool opengl46_renderer::Init_caps()
m_shadowbuffersize = std::min(m_shadowbuffersize, texturesize);
WriteLog("shadows map size capped at " + std::to_string(m_shadowbuffersize) + "p");
}
Global.DynamicLightCount = std::min(Global.DynamicLightCount, (int)gl::MAX_LIGHTS - 1); // reserve 1 slot for sun
if (Global.iMultisampling)
{

View File

@@ -272,8 +272,6 @@ class opengl46_renderer : public gfx_renderer {
viewport_config *m_current_viewport = nullptr;
typedef std::vector<opengl46_light> opengllight_array;
// methods
std::unique_ptr<gl::program> make_shader(std::string v, std::string f);
bool Init_caps();
@@ -286,6 +284,8 @@ class opengl46_renderer : public gfx_renderer {
void setup_env_map(gl::cubemap *tex);
void setup_environment_light(TEnvironmentType const Environment = e_flat);
void setup_sunlight_intensity( float const Factor = 1.f);
// sends the header of the light buffer and the sun to the gpu
void upload_light_header();
// runs jobs needed to generate graphics for specified render pass
void Render_pass(viewport_config &vp, rendermode const Mode);
// creates dynamic environment cubemap
@@ -344,6 +344,8 @@ class opengl46_renderer : public gfx_renderer {
void Update_Lights(light_array &Lights);
// brings the light cluster grid of the viewport up to date with the projection of the color pass being drawn
void Update_Clusters(viewport_config &vp);
// assigns the lights set up by Update_Lights() to the clusters of the viewport; run after it, before the scene is drawn
void Cull_Lights(viewport_config &vp);
// debug view: tints the finished image of the viewport with the cluster of every pixel. Target is the framebuffer
// holding the image (null for the default one), Targetsize its size
void Render_cluster_heatmap(viewport_config &vp, gl::framebuffer *Target, glm::ivec2 const &Targetsize);
@@ -374,7 +376,6 @@ class opengl46_renderer : public gfx_renderer {
texture_manager m_textures;
opengl46_terrain_materials m_terrainmaterials{m_materials, m_textures};
opengl46_light m_sunlight;
opengllight_array m_lights;
/*
float m_sunandviewangle; // cached dot product of sunlight and camera vectors
*/
@@ -451,6 +452,11 @@ class opengl46_renderer : public gfx_renderer {
GLuint64 m_gllasttime = 0;
// gpu time of the light cluster compute passes; the debug panel shows it once a pass measures it
gl::gpu_timer m_clusterstimer;
// gpu time of assigning the lights to the clusters, done every frame
gl::gpu_timer m_cullstimer;
// headlights placed in the light buffer by the last Update_Lights(), and those which are on but did not fit in it
uint32_t m_lights_lit = 0;
uint32_t m_lights_dropped = 0;
double m_precipitationrotation;
@@ -517,6 +523,7 @@ class opengl46_renderer : public gfx_renderer {
std::unique_ptr<gl::program> m_hiddenarea_shader;
std::unique_ptr<gl::program> m_copy_shader;
std::unique_ptr<gl::program> m_cluster_build_shader;
std::unique_ptr<gl::program> m_cluster_cull_shader;
std::unique_ptr<gl::program> m_cluster_heatmap_shader;
std::unique_ptr<gl::vao> m_empty_vao;

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);
}