diff --git a/CMakeLists.txt b/CMakeLists.txt index e69b44bf..258c7929 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -259,6 +259,7 @@ set(SOURCES "gl/pbo.cpp" "gl/query.cpp" "gl/ssbo.cpp" +"gl/cluster_grid.cpp" "imgui/imgui.cpp" "imgui/imgui_demo.cpp" diff --git a/gl/cluster_grid.cpp b/gl/cluster_grid.cpp new file mode 100644 index 00000000..7a6119f6 --- /dev/null +++ b/gl/cluster_grid.cpp @@ -0,0 +1,66 @@ +#include "stdafx.h" +#include "cluster_grid.h" + +#include + +gl::cluster_grid::cluster_grid() + : m_aabbs(sizeof(cluster_aabb) * CLUSTER_COUNT, CLUSTER_AABB_SSBO_BINDING) + , m_ranges(sizeof(cluster_range) * CLUSTER_COUNT, CLUSTER_RANGE_SSBO_BINDING) + , m_indices(sizeof(uint32_t) * CLUSTER_COUNT * MAX_LIGHTS_PER_CLUSTER, CLUSTER_INDEX_SSBO_BINDING) + , m_counters(sizeof(cluster_counters), CLUSTER_COUNTER_SSBO_BINDING) + , m_params_buffer(sizeof(cluster_ubs), CLUSTER_UBO_BINDING) +{ + // no light is assigned to any cluster until the culling pass runs + std::vector const empty(sizeof(cluster_range) * CLUSTER_COUNT, 0); + m_ranges.update(empty.data(), 0, static_cast(empty.size())); + m_counters.update(cluster_counters{}); + m_params_buffer.update(m_params); +} + +void gl::cluster_grid::bind() +{ + m_aabbs.bind_base(); + m_ranges.bind_base(); + m_indices.bind_base(); + m_counters.bind_base(); + m_params_buffer.bind_uniform(); +} + +bool gl::cluster_grid::update(const glm::mat4 &projection, float znear, float zfar, glm::vec2 viewport_size) +{ + cluster_ubs params = m_params; + params.viewport_width = viewport_size.x; + params.viewport_height = viewport_size.y; + + // clip space w is a function of the position in a perspective projection, and a constant in an orthographic one + bool const perspective{glm::length(glm::vec3{projection[0][3], projection[1][3], projection[2][3]}) > 1e-4f}; + bool const usable{perspective && znear > 0.f && zfar > znear}; + params.valid = usable ? 1 : 0; + + if (usable && (projection != m_params.projection || znear != m_params.znear || zfar != m_params.zfar)) + { + params.projection = projection; + params.znear = znear; + params.zfar = zfar; + params.slice_scale = static_cast(CLUSTER_GRID_Z) / std::log(zfar / znear); + params.slice_bias = -std::log(znear) * params.slice_scale; + m_boxes_stale = true; + } + + if (std::memcmp(¶ms, &m_params, sizeof(params)) != 0) + { + m_params = params; + m_params_buffer.update(m_params); + } + + return usable && m_boxes_stale; +} + +void gl::cluster_grid::build(program &build_program) +{ + bind(); + build_program.dispatch((CLUSTER_COUNT + CLUSTER_BUILD_GROUP_SIZE - 1) / CLUSTER_BUILD_GROUP_SIZE); + // the boxes are read by compute passes which follow, from the same shader storage buffer + memory_barrier(GL_SHADER_STORAGE_BARRIER_BIT); + m_boxes_stale = false; +} diff --git a/gl/cluster_grid.h b/gl/cluster_grid.h new file mode 100644 index 00000000..9763226f --- /dev/null +++ b/gl/cluster_grid.h @@ -0,0 +1,57 @@ +#pragma once + +#include + +#include "ssbo.h" +#include "ubo.h" +#include "shader.h" + +namespace gl +{ + // GPU resources of the light cluster grid of one viewport. the grid cuts the view frustum of a pass into + // CLUSTER_GRID_X x CLUSTER_GRID_Y screen tiles and CLUSTER_GRID_Z slices of depth, spaced exponentially + // between znear and zfar (layout and binding points: ubo.h). depth is the clip space w of a point, which is + // also 1.0 / gl_FragCoord.w of its fragment and, unlike the view space z, is right for tilted screens. + // the buffers have a fixed size, so the grid survives changes of the resolution, fov and draw range: those + // only make its parameters and boxes be recomputed. + // bindings of the buffers are context state and the buffers are per viewport, so bind() has to be done in + // the context and before every pass which uses the grid + class cluster_grid + { + public: + cluster_grid(); + + void bind(); + + // refreshes the parameters for a pass: its projection, depth range and the size of the render target. + // returns true if the boxes of the clusters no longer match them and build() has to run + bool update(const glm::mat4 &projection, float znear, float zfar, glm::vec2 viewport_size); + // recomputes the boxes of the clusters in view space with the program made from cluster_build.comp. + // binds the grid, and leaves the boxes ready to be read by the compute passes which follow + void build(program &build_program); + + // false if the last update found no grid possible for the pass (orthographic projection) + bool valid() const + { + return m_params.valid != 0; + } + const cluster_ubs ¶ms() const + { + return m_params; + } + + private: + // cluster_aabb per cluster + ssbo m_aabbs; + // cluster_range per cluster + ssbo m_ranges; + // light indices of all clusters, allocated from m_counters.index_count + ssbo m_indices; + // cluster_counters + ssbo m_counters; + ubo m_params_buffer; + + cluster_ubs m_params{}; + bool m_boxes_stale = true; + }; +} // namespace gl diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index 00656939..5f419b80 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -16,6 +16,17 @@ void gl::glsl_common_setup() "#define MAX_PARAMS " + std::to_string(MAX_PARAMS) + "U\n" + "#define MAX_INSTANCES_PER_BATCH " + std::to_string(MAX_INSTANCES_PER_BATCH) + "U\n" + "#define LIGHT_SSBO_BINDING " + std::to_string(LIGHT_SSBO_BINDING) + "\n" + + "#define CLUSTER_GRID_X " + std::to_string(CLUSTER_GRID_X) + "\n" + + "#define CLUSTER_GRID_Y " + std::to_string(CLUSTER_GRID_Y) + "\n" + + "#define CLUSTER_GRID_Z " + std::to_string(CLUSTER_GRID_Z) + "\n" + + "#define CLUSTER_COUNT " + std::to_string(CLUSTER_COUNT) + "\n" + + "#define MAX_LIGHTS_PER_CLUSTER " + std::to_string(MAX_LIGHTS_PER_CLUSTER) + "\n" + + "#define CLUSTER_BUILD_GROUP_SIZE " + std::to_string(CLUSTER_BUILD_GROUP_SIZE) + "\n" + + "#define CLUSTER_UBO_BINDING " + std::to_string(CLUSTER_UBO_BINDING) + "\n" + + "#define CLUSTER_AABB_SSBO_BINDING " + std::to_string(CLUSTER_AABB_SSBO_BINDING) + "\n" + + "#define CLUSTER_RANGE_SSBO_BINDING " + std::to_string(CLUSTER_RANGE_SSBO_BINDING) + "\n" + + "#define CLUSTER_INDEX_SSBO_BINDING " + std::to_string(CLUSTER_INDEX_SSBO_BINDING) + "\n" + + "#define CLUSTER_COUNTER_SSBO_BINDING " + std::to_string(CLUSTER_COUNTER_SSBO_BINDING) + "\n" + R"STRING( const uint LIGHT_SPOT = 0U; const uint LIGHT_POINT = 1U; @@ -56,6 +67,60 @@ void gl::glsl_common_setup() }; #endif + // light clusters, see gl/cluster_grid.h + struct cluster_aabb_s + { + vec4 lower; + vec4 upper; + }; + + struct cluster_range_s + { + uint offset; + uint count; + }; + + #if defined(STAGE_FRAGMENT) || defined(STAGE_COMPUTE) + // parameters of the grid for the pass being drawn + layout(std140, binding = CLUSTER_UBO_BINDING) uniform cluster_ubo + { + mat4 cluster_projection; + float cluster_znear; + float cluster_zfar; + float cluster_slice_scale; + float cluster_slice_bias; + vec2 cluster_viewport_size; + uint cluster_valid; + }; + + // the compute passes fill the cluster buffers, shading only reads them + #ifdef STAGE_COMPUTE + #define CLUSTER_SSBO_ACCESS + layout(std430, binding = CLUSTER_AABB_SSBO_BINDING) buffer cluster_aabb_ssbo + { + cluster_aabb_s cluster_aabbs[]; + }; + + layout(std430, binding = CLUSTER_COUNTER_SSBO_BINDING) buffer cluster_counter_ssbo + { + uint cluster_index_count; + uint cluster_overflow_count; + }; + #else + #define CLUSTER_SSBO_ACCESS readonly + #endif + + layout(std430, binding = CLUSTER_RANGE_SSBO_BINDING) CLUSTER_SSBO_ACCESS buffer cluster_range_ssbo + { + cluster_range_s cluster_ranges[]; + }; + + layout(std430, binding = CLUSTER_INDEX_SSBO_BINDING) CLUSTER_SSBO_ACCESS buffer cluster_index_ssbo + { + uint cluster_light_indices[]; + }; + #endif + layout (std140) uniform model_ubo { mat4 modelview; diff --git a/gl/ubo.h b/gl/ubo.h index d41d0c0b..36bca73d 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -177,5 +177,87 @@ namespace gl static_assert(offsetof(light_data, lights) == 32, "bad layout of light_data"); static_assert(sizeof(light_data) == 32 + sizeof(light_element) * MAX_LIGHTS, "bad size of light_data"); + // light clusters (see gl/cluster_grid.h): the view frustum is cut into CLUSTER_GRID_X x CLUSTER_GRID_Y screen + // tiles and CLUSTER_GRID_Z depth slices, spaced exponentially between the near and far plane of the pass. + // cluster index = x + CLUSTER_GRID_X * (y + CLUSTER_GRID_Y * z), with x and y counted from the lower left corner + const GLuint CLUSTER_GRID_X = 16; + const GLuint CLUSTER_GRID_Y = 9; + const GLuint CLUSTER_GRID_Z = 24; + const GLuint CLUSTER_COUNT = CLUSTER_GRID_X * CLUSTER_GRID_Y * CLUSTER_GRID_Z; + // capacity of the light list of a single cluster + const GLuint MAX_LIGHTS_PER_CLUSTER = 64; + // work group size of cluster_build.comp, which handles one cluster per invocation + const GLuint CLUSTER_BUILD_GROUP_SIZE = 64; + + // binding points of the cluster buffers: layout(binding = ...) in glsl_common. + // the uniform block numbers go together with scene (0), model (1) and instance (3) blocks + const GLuint CLUSTER_UBO_BINDING = 2; + const GLuint CLUSTER_AABB_SSBO_BINDING = 1; + const GLuint CLUSTER_RANGE_SSBO_BINDING = 2; + const GLuint CLUSTER_INDEX_SSBO_BINDING = 3; + const GLuint CLUSTER_COUNTER_SSBO_BINDING = 4; + + // layout std140: grid parameters for the pass it was built for (GLSL: cluster_ubo) + struct cluster_ubs + { + glm::mat4 projection; + float znear; + float zfar; + // depth slice of a point with clip space w: floor(log(w) * slice_scale + slice_bias), znear..zfar covers all slices + float slice_scale; + float slice_bias; + // size of the render target the tiles are laid over, in pixels + float viewport_width; + float viewport_height; + // 0 if there is no grid for the pass (orthographic projection) + uint32_t valid; + UBS_PAD(4); + }; + + static_assert(sizeof(cluster_ubs) == 96, "bad size of cluster_ubs"); + static_assert(offsetof(cluster_ubs, projection) == 0, "bad layout of cluster_ubs"); + static_assert(offsetof(cluster_ubs, znear) == 64, "bad layout of cluster_ubs"); + static_assert(offsetof(cluster_ubs, zfar) == 68, "bad layout of cluster_ubs"); + static_assert(offsetof(cluster_ubs, slice_scale) == 72, "bad layout of cluster_ubs"); + static_assert(offsetof(cluster_ubs, slice_bias) == 76, "bad layout of cluster_ubs"); + static_assert(offsetof(cluster_ubs, viewport_width) == 80, "bad layout of cluster_ubs"); + static_assert(offsetof(cluster_ubs, viewport_height) == 84, "bad layout of cluster_ubs"); + static_assert(offsetof(cluster_ubs, valid) == 88, "bad layout of cluster_ubs"); + + // layout std430: bounding box of a cluster in view space, element of cluster_aabb_ssbo (GLSL: cluster_aabb_s), w unused + struct cluster_aabb + { + glm::vec4 lower; + glm::vec4 upper; + }; + + static_assert(sizeof(cluster_aabb) == 32, "bad size of cluster_aabb"); + static_assert(offsetof(cluster_aabb, lower) == 0, "bad layout of cluster_aabb"); + static_assert(offsetof(cluster_aabb, upper) == 16, "bad layout of cluster_aabb"); + + // layout std430: lights of a cluster, element of cluster_range_ssbo (GLSL: cluster_range_s). the lights are + // light_data.lights indices stored in the cluster_index_ssbo list from offset on + struct cluster_range + { + uint32_t offset; + uint32_t count; + }; + + static_assert(sizeof(cluster_range) == 8, "bad size of cluster_range"); + static_assert(offsetof(cluster_range, offset) == 0, "bad layout of cluster_range"); + static_assert(offsetof(cluster_range, count) == 4, "bad layout of cluster_range"); + + // layout std430: cluster_counter_ssbo. index_count is the allocator of the shared index list, overflow_count + // the number of lights dropped from clusters which were full + struct cluster_counters + { + uint32_t index_count; + uint32_t overflow_count; + }; + + static_assert(sizeof(cluster_counters) == 8, "bad size of cluster_counters"); + static_assert(offsetof(cluster_counters, index_count) == 0, "bad layout of cluster_counters"); + static_assert(offsetof(cluster_counters, overflow_count) == 4, "bad layout of cluster_counters"); + #pragma pack(pop) } diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 1402a221..4c8d4590 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -169,6 +169,11 @@ bool opengl46_renderer::Init(GLFWwindow *Window) m_celestial_shader = make_shader("celestial.vert", "celestial.frag"); m_hiddenarea_shader = make_shader("hiddenarea.vert", "hiddenarea.frag"); m_copy_shader = make_shader("quad.vert", "copy.frag"); + { + gl::shader cluster_build("cluster_build.comp"); + m_cluster_build_shader = std::make_unique(std::vector>{cluster_build}); + } + m_cluster_heatmap_shader = make_shader("quad.vert", "cluster_heatmap.frag"); m_invalid_material = Fetch_Material("invalid"); } catch (gl::shader_exception const &e) @@ -456,6 +461,8 @@ bool opengl46_renderer::init_viewport(viewport_config &vp) scene_ubo->bind_uniform(); light_ssbo->bind_base(); + vp.clusters = std::make_unique(); + if (!Global.gfx_skippipeline) { vp.msaa_rbc = std::make_unique(); @@ -713,6 +720,18 @@ void opengl46_renderer::draw_debug_ui() } ImGui::End(); + if (ImGui::Begin("Light clusters")) + { + ImGui::Checkbox("heatmap", &Global.gfx_lights_clusterheatmap); + if (auto const &clusters{m_viewports.front()->clusters}) + { + 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::End(); + ImGui::SetNextWindowSize(ImVec2S(400, 400)); if (ImGui::Begin("Pickbuffer") && m_pick_tex) { ImGui::Image((ImTextureID)(intptr_t)(m_pick_tex->id), ImGui::GetContentRegionAvail(), ImVec2(0, 1.0), ImVec2(1.0, 0)); @@ -897,6 +916,7 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) future = glm::translate(mv, -glm::vec3(vehicle->get_future_movement())) * glm::inverse(mv); } + Update_Clusters( vp ); Update_Lights( simulation::Lights ); glDebug("render environment"); @@ -1049,6 +1069,9 @@ if (!Global.gfx_skippipeline) m_pfx_tonemapping->apply( *vp.main2_tex, target ); } + if (Global.gfx_lights_clusterheatmap && vp.clusters->valid()) + Render_cluster_heatmap(vp, target, target_size); + if (vr) { if (vp.proj_type == viewport_config::vr_left) vr->submit(vr_interface::eye_left, vp.backbuffer_tex.get()); @@ -1587,6 +1610,8 @@ void opengl46_renderer::setup_pass(viewport_config &Viewport, renderpass_config auto const znear = ( Znear > 1.f ? Znear : Znear > 0.f ? Znear * zfar : 0.1f * Global.ZoomFactor); if (EditorModeFlag && Viewport.main) zfar = editor_track_view_range(zfar, true); + Config.znear = znear; + Config.zfar = zfar; camera.projection() = perspective_projection(Viewport.projection, znear, zfar, frustumtest_proj); editor_ortho_projection(Viewport, zfar, camera.projection(), frustumtest_proj); @@ -6004,6 +6029,78 @@ void opengl46_renderer::Update_Lights(light_array &Lights) light_ssbo->update(light_data); } +void opengl46_renderer::Update_Clusters(viewport_config &vp) +{ + glDebug("Update_Clusters"); + + auto &grid{*vp.clusters}; + grid.bind(); + // the boxes of the clusters depend only on the projection and the depth range, so they are rebuilt only when those change + if (grid.update(OpenGLMatrices.data(GL_PROJECTION), m_renderpass.znear, m_renderpass.zfar, glm::vec2{static_cast(vp.width), static_cast(vp.height)})) + { + m_clusterstimer.begin(); + grid.build(*m_cluster_build_shader); + m_clusterstimer.end(); + } +} + +void opengl46_renderer::Render_cluster_heatmap(viewport_config &vp, gl::framebuffer *Target, glm::ivec2 const &Targetsize) +{ + glDebug("Render_cluster_heatmap"); + + if (!vp.msaa_fb) + return; + + if (!vp.clusters_depth_fb) + { + auto texture{std::make_unique()}; + texture->alloc_rendertarget(Global.gfx_format_depth, GL_DEPTH_COMPONENT, vp.width, vp.height, 1, 1, GL_CLAMP_TO_EDGE); + // render target textures of depth are made for the shadow sampler; here the values themselves are read + glTextureParameteri(texture->id, GL_TEXTURE_COMPARE_MODE, GL_NONE); + glTextureParameteri(texture->id, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTextureParameteri(texture->id, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + auto framebuffer{std::make_unique()}; + framebuffer->attach(*texture, GL_DEPTH_ATTACHMENT); + // depth only + framebuffer->setup_drawing(0); + if (!framebuffer->is_complete()) + { + ErrorLog("cluster heatmap framebuffer setup failed"); + Global.gfx_lights_clusterheatmap = false; + return; + } + vp.clusters_depth_tex = std::move(texture); + vp.clusters_depth_fb = std::move(framebuffer); + } + + // the depth of the scene is still in the msaa buffer, the post processing worked on its colour + vp.msaa_fb->blit_to(vp.clusters_depth_fb.get(), vp.width, vp.height, GL_DEPTH_BUFFER_BIT, GL_DEPTH_ATTACHMENT); + + if (Target) + Target->bind(); + else + gl::framebuffer::unbind(); + glViewport(0, 0, Targetsize.x, Targetsize.y); + + vp.clusters->bind(); + m_cluster_heatmap_shader->bind(); + m_empty_vao->bind(); + vp.clusters_depth_tex->bind(0); + + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); + auto const blending{glIsEnabled(GL_BLEND) == GL_TRUE}; + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + if (!blending) + glDisable(GL_BLEND); + vp.clusters_depth_tex->unbind(0); +} + bool opengl46_renderer::Init_caps() { std::string gl_renderer((char *)glGetString(GL_RENDERER)); diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index af3c13e1..faeea237 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/. #include "vr/vr_interface.h" #include "gl/ubo.h" #include "gl/ssbo.h" +#include "gl/cluster_grid.h" #include "gl/framebuffer.h" #include "gl/renderbuffer.h" #include "gl/postfx.h" @@ -206,6 +207,10 @@ class opengl46_renderer : public gfx_renderer { opengl_camera viewport_camera; rendermode draw_mode{rendermode::none}; float draw_range{0.0f}; + // depth range of the projection of a color pass; the far plane is only the end of the range which is drawn, + // as the projection matrix itself has none + float znear{0.0f}; + float zfar{0.0f}; debug_stats draw_stats; }; @@ -256,6 +261,12 @@ class opengl46_renderer : public gfx_renderer { std::unique_ptr backbuffer_fb; std::unique_ptr backbuffer_tex; + // light cluster grid of the viewport + std::unique_ptr clusters; + // depth of the finished scene resolved from the msaa buffer, for the cluster heatmap; made when it is first needed + std::unique_ptr clusters_depth_fb; + std::unique_ptr clusters_depth_tex; + bool initialized = false; }; @@ -331,6 +342,11 @@ class opengl46_renderer : public gfx_renderer { void Render_Alpha(TSubModel *Submodel); void Render_Alpha_text(TSubModel *Submodel); // characters of a text display 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); + // 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); glm::vec3 pick_color(std::size_t const Index); std::size_t pick_index(glm::ivec3 const &Color); @@ -500,6 +516,8 @@ class opengl46_renderer : public gfx_renderer { std::unique_ptr m_celestial_shader; std::unique_ptr m_hiddenarea_shader; std::unique_ptr m_copy_shader; + std::unique_ptr m_cluster_build_shader; + std::unique_ptr m_cluster_heatmap_shader; std::unique_ptr m_empty_vao; diff --git a/shaders/cluster_build.comp b/shaders/cluster_build.comp new file mode 100644 index 00000000..37efd677 --- /dev/null +++ b/shaders/cluster_build.comp @@ -0,0 +1,57 @@ +// bounding boxes of the light clusters in view space, see gl/cluster_grid.h +// one invocation per cluster, run when the projection or the depth range of the pass changes + +#include + +layout(local_size_x = CLUSTER_BUILD_GROUP_SIZE) in; + +// clip space w at the lower boundary of slice k: the inverse of the slice formula used when shading +float slice_depth(uint k) +{ + return exp((float(k) - cluster_slice_bias) / cluster_slice_scale); +} + +void main() +{ + uint index = gl_GlobalInvocationID.x; + if (index >= uint(CLUSTER_COUNT)) + return; + + uvec3 cell = uvec3( + index % uint(CLUSTER_GRID_X), + (index / uint(CLUSTER_GRID_X)) % uint(CLUSTER_GRID_Y), + index / uint(CLUSTER_GRID_X * CLUSTER_GRID_Y)); + + // clip x, y and w are affine functions of the view space position, while clip z is constant (reverse z with + // the far plane at infinity), which makes the projection matrix singular. the position is recovered from + // those three alone, so off-axis and rotated screens of custom viewports and vr eyes work too + mat3 linear = mat3( + vec3(cluster_projection[0][0], cluster_projection[0][1], cluster_projection[0][3]), + vec3(cluster_projection[1][0], cluster_projection[1][1], cluster_projection[1][3]), + vec3(cluster_projection[2][0], cluster_projection[2][1], cluster_projection[2][3])); + vec3 translation = vec3(cluster_projection[3][0], cluster_projection[3][1], cluster_projection[3][3]); + mat3 unproject = inverse(linear); + + // the tile in clip space xy, the slice in clip space w + vec2 tiles = vec2(CLUSTER_GRID_X, CLUSTER_GRID_Y); + vec2 ndc_min = vec2(cell.xy) / tiles * 2.0 - 1.0; + vec2 ndc_max = vec2(cell.xy + 1U) / tiles * 2.0 - 1.0; + float w_near = slice_depth(cell.z); + float w_far = slice_depth(cell.z + 1U); + + vec3 lower = vec3(3.402823e38); + vec3 upper = vec3(-3.402823e38); + for (int corner = 0; corner < 8; ++corner) + { + vec2 ndc = vec2( + (corner & 1) != 0 ? ndc_max.x : ndc_min.x, + (corner & 2) != 0 ? ndc_max.y : ndc_min.y); + float w = (corner & 4) != 0 ? w_far : w_near; + vec3 position = unproject * (vec3(ndc * w, w) - translation); + lower = min(lower, position); + upper = max(upper, position); + } + + cluster_aabbs[index].lower = vec4(lower, 0.0); + cluster_aabbs[index].upper = vec4(upper, 0.0); +} diff --git a/shaders/cluster_heatmap.frag b/shaders/cluster_heatmap.frag new file mode 100644 index 00000000..d3448afb --- /dev/null +++ b/shaders/cluster_heatmap.frag @@ -0,0 +1,38 @@ +// 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 + +in vec2 f_coords; + +#include + +#texture (depth_tex, 0, R) +uniform sampler2D depth_tex; + +layout(location = 0) out vec4 out_color; + +const float TINT = 0.55; + +// blue (near) through green to red (far) +vec3 ramp(float t) +{ + return clamp(vec3(1.5) - abs(4.0 * t - vec3(3.0, 2.0, 1.0)), 0.0, 1.0); +} + +void main() +{ + // reverse z: 0 is the far plane at infinity, i.e. the sky, which has no cluster + float depth = texelFetch(depth_tex, ivec2(f_coords * vec2(textureSize(depth_tex, 0))), 0).r; + if (depth <= 0.0) + discard; + + // depth is znear / w + float w = cluster_znear / depth; + 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; + + out_color = vec4(color, TINT); +} diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index fe099aa2..e81c70f0 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1421,6 +1421,11 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token Parser.getTokens(1); Parser >> gfx_envmap_enabled; } + else if (Token == "gfx.lights.clusterheatmap") + { + Parser.getTokens(1); + Parser >> gfx_lights_clusterheatmap; + } else if (Token == "gfx.postfx.motionblur.enabled") { Parser.getTokens(1); @@ -1754,6 +1759,7 @@ global_settings::export_as_text( std::ostream &Output ) const { // TODO: export fpslimit export_as_text( Output, "randomseed", Global.random_seed ); export_as_text( Output, "gfx.envmap.enabled", gfx_envmap_enabled ); + export_as_text( Output, "gfx.lights.clusterheatmap", gfx_lights_clusterheatmap ); export_as_text( Output, "gfx.postfx.motionblur.enabled", gfx_postfx_motionblur_enabled ); export_as_text( Output, "gfx.postfx.motionblur.shutter", gfx_postfx_motionblur_shutter ); // TODO: export gfx_postfx_motionblur_format diff --git a/utilities/Globals.h b/utilities/Globals.h index 9d095799..933a4206 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -283,6 +283,7 @@ struct global_settings { int gfx_framebuffer_height = -1; bool gfx_shadowmap_enabled = true; bool gfx_envmap_enabled = true; + bool gfx_lights_clusterheatmap = false; // debug view of the light clusters bool gfx_postfx_motionblur_enabled = true; float gfx_postfx_motionblur_shutter = 0.01f; GLenum gfx_postfx_motionblur_format = GL_RG16F;