From 6aa6e505e83a528474bb1defea88278681f44f75 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 15 Oct 2026 15:21:21 +0200 Subject: [PATCH] Cull lights into clusters in a compute pass --- gl/cluster_grid.cpp | 62 ++++++++++ gl/cluster_grid.h | 44 +++++++ gl/glsl_common.cpp | 4 + gl/ubo.h | 18 ++- rendering/opengl46/opengl46renderer.cpp | 106 +++++++++++------ rendering/opengl46/opengl46renderer.h | 13 +- shaders/cluster_cull.comp | 151 ++++++++++++++++++++++++ shaders/cluster_heatmap.frag | 18 ++- 8 files changed, 368 insertions(+), 48 deletions(-) create mode 100644 shaders/cluster_cull.comp diff --git a/gl/cluster_grid.cpp b/gl/cluster_grid.cpp index 7a6119f6..fa2edfd1 100644 --- a/gl/cluster_grid.cpp +++ b/gl/cluster_grid.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include "cluster_grid.h" +#include #include 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(); + ++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; + } +} diff --git a/gl/cluster_grid.h b/gl/cluster_grid.h index 9763226f..3af9f9ad 100644 --- a/gl/cluster_grid.h +++ b/gl/cluster_grid.h @@ -1,7 +1,12 @@ #pragma once +#include +#include +#include + #include +#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 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 copied; + }; + void collect_stats(); + std::array 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 m_stats; }; } // namespace gl diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index 5f419b80..a52050e1 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -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[]; diff --git a/gl/ubo.h b/gl/ubo.h index 36bca73d..38c30b2b 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -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; diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 4c8d4590..59a1596b 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -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(std::vector>{cluster_build}); } + { + gl::shader cluster_cull("cluster_cull.comp"); + m_cluster_cull_shader = std::make_unique(std::vector>{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(*clusterstime) / 1000000.0, 3) + " ms\n"; + if (auto const culltime = m_cullstimer.result()) + m_debugtimestext += " cull: " + to_string(static_cast(*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( 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 ¶ms{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(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(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(light_i); + m_lights_lit = static_cast(light_i - 1); + light_data.lights_count = 1 + std::min(static_cast(light_i - 1), static_cast(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) { diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index faeea237..e6ad2065 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -272,8 +272,6 @@ class opengl46_renderer : public gfx_renderer { viewport_config *m_current_viewport = nullptr; - typedef std::vector opengllight_array; - // methods std::unique_ptr 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 m_hiddenarea_shader; std::unique_ptr m_copy_shader; std::unique_ptr m_cluster_build_shader; + std::unique_ptr m_cluster_cull_shader; std::unique_ptr m_cluster_heatmap_shader; std::unique_ptr m_empty_vao; diff --git a/shaders/cluster_cull.comp b/shaders/cluster_cull.comp new file mode 100644 index 00000000..692d2400 --- /dev/null +++ b/shaders/cluster_cull.comp @@ -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 + +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); +} diff --git a/shaders/cluster_heatmap.frag b/shaders/cluster_heatmap.frag index d3448afb..49bf7c20 100644 --- a/shaders/cluster_heatmap.frag +++ b/shaders/cluster_heatmap.frag @@ -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); }