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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[];
|
||||
|
||||
18
gl/ubo.h
18
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;
|
||||
|
||||
@@ -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 ¶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<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)
|
||||
{
|
||||
|
||||
@@ -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
151
shaders/cluster_cull.comp
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user