From 8c00b0bc3d524792218343196e5cfc420a541bf9 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Mon, 19 Oct 2026 12:02:01 +0200 Subject: [PATCH] Fit the cascades of the sun shadows to the depth of the view --- CMakeLists.txt | 1 + gl/glsl_common.cpp | 4 + gl/ubo.h | 34 ++- rendering/opengl46/opengl46renderer.cpp | 291 ++++++++++++++++++++++- rendering/opengl46/opengl46renderer.h | 34 +++ rendering/opengl46/opengl46shadowfit.cpp | 171 +++++++++++++ rendering/opengl46/opengl46shadowfit.h | 94 ++++++++ shaders/fog_inject.comp | 23 +- shaders/light_common.glsl | 15 ++ shaders/shadow_fit_bounds.comp | 126 ++++++++++ shaders/shadow_fit_depth.comp | 65 +++++ utilities/Globals.cpp | 13 + utilities/Globals.h | 7 + 13 files changed, 857 insertions(+), 21 deletions(-) create mode 100644 rendering/opengl46/opengl46shadowfit.cpp create mode 100644 rendering/opengl46/opengl46shadowfit.h create mode 100644 shaders/shadow_fit_bounds.comp create mode 100644 shaders/shadow_fit_depth.comp diff --git a/CMakeLists.txt b/CMakeLists.txt index 44aead35..0b8c94e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -340,6 +340,7 @@ if (WITH_OPENGL_MODERN) "rendering/opengl46/opengl46localshadows.cpp" "rendering/opengl46/opengl46instancecull.cpp" "rendering/opengl46/opengl46hiz.cpp" + "rendering/opengl46/opengl46shadowfit.cpp" "rendering/opengl46/opengl46precipitation.cpp" "rendering/opengl46/opengl46particles.cpp" "rendering/opengl46/opengl46terrain.cpp" diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index 22a7bafd..b58d48b7 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -27,6 +27,8 @@ void gl::glsl_common_setup() "#define INSTANCE_OFFSETS_GROUP_SIZE " + std::to_string(INSTANCE_OFFSETS_GROUP_SIZE) + "\n" + "#define INSTANCE_COMMAND_GROUP_SIZE " + std::to_string(INSTANCE_COMMAND_GROUP_SIZE) + "\n" + "#define HIZ_GROUP_SIZE " + std::to_string(HIZ_GROUP_SIZE) + "\n" + + "#define SHADOW_FIT_GROUP_SIZE " + std::to_string(SHADOW_FIT_GROUP_SIZE) + "\n" + + "#define SHADOW_FIT_SSBO_BINDING " + std::to_string(SHADOW_FIT_SSBO_BINDING) + "\n" + "#define INSTANCE_COUNTER_HEADER " + std::to_string(INSTANCE_COUNTER_HEADER) + "\n" + "#define INSTANCE_JOB_SLEEPERS " + std::to_string(INSTANCE_JOB_SLEEPERS) + "U\n" + "#define LIGHT_SSBO_BINDING " + std::to_string(LIGHT_SSBO_BINDING) + "\n" + @@ -264,6 +266,8 @@ void gl::glsl_common_setup() // contact shadows, see gl::scene_ubs vec4 contact_shadow_params; vec4 contact_shadow_lamps; + // cascades of the sun shadows fitted to the depth of the view, see gl::scene_ubs + vec4 shadow_fit; }; #ifdef STAGE_VERTEX diff --git a/gl/ubo.h b/gl/ubo.h index 163b691b..1b171cac 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -101,9 +101,12 @@ namespace gl glm::vec4 contact_shadow_params; // x: how many lamps and headlights a pixel traces the rays to (0: only the sun), yzw unused glm::vec4 contact_shadow_lamps; + // x: 1 when the cascades of the sun shadows are fitted to the depth of the view (gfx.shadow.sdsm, opengl46_shadow_fit): + // a point outside the map of its cascade goes to the next cascade whose map holds it. yzw unused + glm::vec4 shadow_fit; }; - static_assert(sizeof(scene_ubs) == 432 + 80 * MAX_HEADLIGHT_SHADOWS, "bad size of ubs"); + static_assert(sizeof(scene_ubs) == 448 + 80 * MAX_HEADLIGHT_SHADOWS, "bad size of ubs"); const size_t MAX_PARAMS = 3; @@ -487,6 +490,12 @@ namespace gl // work group of the passes which make the depth pyramid of the occlusion test (hiz_build.comp), 8x8 const GLuint HIZ_GROUP_SIZE = 8; + // the cascades of the sun shadows fitted to the depth of the main view (opengl46_shadow_fit, shadow_fit_*.comp): work group + // of its passes, 16x16 pixels, and the binding point of its buffer. the point is one of the instance culling, which binds + // its buffers before every use; the fit binds its own before its passes + const GLuint SHADOW_FIT_GROUP_SIZE = 16; + const GLuint SHADOW_FIT_SSBO_BINDING = 9; + // layout std430: a bucket of a scene cell queued in the pass (GLSL: instance_cull_job) struct instance_cull_job { @@ -539,5 +548,28 @@ namespace gl static_assert(sizeof(instance_draw_command) == 32, "bad size of instance_draw_command"); + // layout std430: what the passes of the cascades fitted to the depth of the view leave for the cpu (GLSL: shadow_fit_ssbo). + // the cpu writes it before the passes: the number of the measurement, no pixels, the nearest distance infinite, the bounds + // empty. the passes take the extremes with atomic operations on the bits: positive floats order as uints, and the bounds, + // which can be negative, are stored in an order preserving form (shadow_fit_order() in shadow_fit_bounds.comp) + struct shadow_fit_data + { + uint32_t measurement; // number given by the cpu, to find the camera and the light it was made with + uint32_t samples; // pixels of the view which aren't the sky + float depthmin; // nearest and farthest distance of those from the camera, metres + float depthmax; + float splits[4]; // distances the cascades go from and to (MAX_CASCADES + 1 of them), from the two above + uint32_t counts[4]; // pixels taken into the bounds of each cascade (MAX_CASCADES of them) + // per cascade the lowest x, y, z, unused, then the highest x, y, z, unused, of the pixels it takes, in metres in the + // space of the light (its rotation only), relative to the camera + uint32_t bounds[MAX_CASCADES * 8]; + }; + + static_assert(MAX_CASCADES + 1 <= 4, "shadow_fit_data holds the splits of at most 3 cascades"); + static_assert(offsetof(shadow_fit_data, splits) == 16, "bad layout of shadow_fit_data"); + static_assert(offsetof(shadow_fit_data, counts) == 32, "bad layout of shadow_fit_data"); + static_assert(offsetof(shadow_fit_data, bounds) == 48, "bad layout of shadow_fit_data"); + static_assert(sizeof(shadow_fit_data) == 48 + 32 * MAX_CASCADES, "bad size of shadow_fit_data"); + #pragma pack(pop) } diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 0dbc4942..8ee523a7 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -239,6 +239,7 @@ bool opengl46_renderer::Init(GLFWwindow *Window) shadowcull.init(&m_instancecull); m_reflectioncull.init(&m_instancecull); m_hiz.init(); + m_shadowfit.init(); m_exposure.init(); m_ao.init(); m_localshadows.init(); @@ -843,6 +844,8 @@ void opengl46_renderer::SwapBuffers() } } m_debugtimestext += m_localshadows.info_times(); + if( Global.gfx_shadow_sdsm ) + m_debugtimestext += m_shadowfit.info_times(); m_debugtimestext += "uilayer: " + to_string( Timer::subsystem.gfx_gui.average(), 2 ) + " ms\n"; if( DebugModeFlag ) m_debugtimestext += m_textures.info(); @@ -852,6 +855,36 @@ void opengl46_renderer::SwapBuffers() shadowstats += shadowpass.draw_stats; } + // the cascades of the sun shadows: where they end and the size of a texel of their maps, fixed or fitted to the depth + std::string shadowcascadestext; + if( Global.gfx_shadowmap_enabled && ( m_shadowbuffersize > 0 ) ) { + auto const &fit { m_shadowfitstate }; + shadowcascadestext = " sun shadow cascades to"; + for( std::size_t cascade = 0; cascade < gl::MAX_CASCADES; ++cascade ) { + shadowcascadestext += " " + to_string( fit.splits[ cascade + 1 ], 1 ); + } + shadowcascadestext += " m, texels"; + for( std::size_t cascade = 0; cascade < gl::MAX_CASCADES; ++cascade ) { + shadowcascadestext += " " + to_string( 100.f * fit.drawn[ cascade ] / m_shadowbuffersize, 1 ); + } + shadowcascadestext += " cm"; + if( Global.gfx_shadow_sdsm ) { + if( fit.fitted ) { + shadowcascadestext += " (whole slices"; + for( std::size_t cascade = 0; cascade < gl::MAX_CASCADES; ++cascade ) { + shadowcascadestext += " " + to_string( 100.f * fit.slice[ cascade ] / m_shadowbuffersize, 1 ); + } + shadowcascadestext += + "), fitted to pixels " + to_string( fit.depthmin, 2 ) + "-" + to_string( fit.depthmax, 1 ) + " m " + + std::to_string( fit.age ) + " frames ago, view turned " + to_string( fit.turn, 1 ) + " deg since"; + } + else { + shadowcascadestext += ", fixed: " + std::string{ fit.reason != nullptr ? fit.reason : "" }; + } + } + shadowcascadestext += "\n"; + } + m_debugstatstext = "triangles: " + to_string( static_cast(m_geometry.primitives_count()), 7 ) + "\n" + "vehicles: " + to_string( m_colorpass.draw_stats.dynamics, 7 ) + " +" + to_string( shadowstats.dynamics, 7 ) @@ -877,6 +910,7 @@ void opengl46_renderer::SwapBuffers() ", rays " + to_string( Global.gfx_contactshadows_length, 2 ) + " m within " + to_string( Global.gfx_contactshadows_range, 0 ) + " m\n" : ", off in this view\n" ) : std::string{} ) + + shadowcascadestext + " instance buffer: " + std::to_string( m_instancestream->used() / static_cast( sizeof( glm::mat4 ) ) ) + " matrices, " + std::to_string( m_instancestream->size() / 1024 ) + " KiB\n" + " model blocks: " + std::to_string( model_ubo->updates() ) @@ -982,6 +1016,27 @@ void opengl46_renderer::draw_debug_ui() } ImGui::End(); + if (ImGui::Begin("Sun shadows")) + { + ImGui::Checkbox("cascades fitted to the depth of the view (gfx.shadow.sdsm)", &Global.gfx_shadow_sdsm); + ImGui::SliderFloat("splits, 0 equal lengths .. 1 equal ratios (gfx.shadow.sdsm.split)", &Global.gfx_shadow_sdsm_split, 0.f, 1.f, "%.2f"); + auto const &fit{m_shadowfitstate}; + if (Global.gfx_shadow_sdsm) + { + if (fit.fitted) + ImGui::Text("fitted to pixels %.2f - %.1f m, measured %u frames ago, view turned %.1f deg since", fit.depthmin, fit.depthmax, fit.age, fit.turn); + else + ImGui::Text("fixed cascades: %s", fit.reason != nullptr ? fit.reason : ""); + } + if (m_shadowbuffersize > 0) + { + for (std::size_t cascade = 0; cascade < gl::MAX_CASCADES; ++cascade) + ImGui::Text("cascade %d: to %.1f m, map %.1f m across (whole slice %.1f m), texel %.1f cm", static_cast(cascade + 1), fit.splits[cascade + 1], + fit.drawn[cascade], fit.slice[cascade], 100.f * fit.drawn[cascade] / m_shadowbuffersize); + } + } + ImGui::End(); + if (ImGui::Begin("Weather")) { m_weather.draw_debug_ui(); @@ -1453,6 +1508,20 @@ if (!Global.gfx_skippipeline) } } + // the depth of the finished view, which the cascades of the sun shadows of the next frames are fitted to + // (gfx.shadow.sdsm). the main framebuffer holds it resolved for the motion blur or the ambient occlusion + if (vp.main && Global.gfx_shadow_sdsm && (Shadow_Fit_Unavailable(vp) == nullptr)) + { + auto const resolved{(vp.main_texd != nullptr) && (Global.gfx_postfx_motionblur_enabled || (aomode != 0))}; + opengl46_shadow_fit::view const view{ + m_colorpass.pass_camera.position(), + m_colorpass.pass_camera.projection(), + glm::mat3{m_colorpass.pass_camera.modelview()}, + Shadow_Light_Rotation(), + Shadow_Fixed_Splits().back()}; + m_shadowfit.measure(resolved ? vp.main_texd->id : 0, *vp.msaa_fb, vp.width, vp.height, view, m_framestamp); + } + gpupart("exposure, bloom"); // the exposure follows the main view; the other viewports use it too if (Global.gfx_postfx_autoexposure && vp.main) @@ -1559,17 +1628,13 @@ if (!Global.gfx_skippipeline) setup_drawing( false ); glViewport(0, 0, m_shadowbuffersize, m_shadowbuffersize); - float csmstageboundaries[] = {0.0f, - Global.shadowtune.range / 32, - Global.shadowtune.range / 8, - Global.shadowtune.range}; - if( Global.shadowtune.map_size > 2048 ) { - // increase coverage if our shadow map is large enough to produce decent results - csmstageboundaries[1] *= Global.shadowtune.map_size / 2048; - csmstageboundaries[2] *= Global.shadowtune.map_size / 2048; - csmstageboundaries[3] *= Global.shadowtune.map_size / 2048; - } - + auto csmstageboundaries { Shadow_Fixed_Splits() }; + // the cascades fitted to the depth of the view (gfx.shadow.sdsm) go between the distances of its pixels, each map + // covering the area of the pixels its cascade takes + std::array>, gl::MAX_CASCADES> fitareas; + auto const fitted { Shadow_Fit( vp, csmstageboundaries, fitareas ) }; + scene_ubs.shadow_fit = glm::vec4{ fitted ? 1.f : 0.f, 0.f, 0.f, 0.f }; + m_shadowfitstate.splits = csmstageboundaries; for( auto idx = 0; idx < m_shadowpass.size(); ++idx ) { @@ -1581,8 +1646,12 @@ if (!Global.gfx_skippipeline) m_shadow_fb->attach( *m_shadow_tex, GL_DEPTH_ATTACHMENT, idx ); m_shadow_fb->clear( GL_DEPTH_BUFFER_BIT ); + m_shadowfitarea = ( fitareas[ idx ] ? &( *fitareas[ idx ] ) : nullptr ); setup_pass( vp, m_renderpass, rendermode::shadows, csmstageboundaries[ idx ], csmstageboundaries[ idx + 1 ] ); + m_shadowfitarea = nullptr; m_shadowpass[ idx ] = m_renderpass; // store pass config for reference in other stages + m_shadowfitstate.slice[ idx ] = m_shadowslicesize; + m_shadowfitstate.drawn[ idx ] = m_shadowdrawnsize; setup_matrices(); scene_ubs.projection = OpenGLMatrices.data( GL_PROJECTION ); @@ -2090,12 +2159,53 @@ void opengl46_renderer::setup_pass(viewport_config &Viewport, renderpass_config } pass_camera /= bounding_viewports_count; + // ...a cascade fitted to the depth of the view (gfx.shadow.sdsm) covers the area of the pixels it takes, within the + // slice of the view. the areas are compared across the light, the depth along it stays the one of the slice... + auto quantizationstep{0.f}; + { + auto const lightrotation{Shadow_Light_Rotation()}; + glm::vec3 slicemin{std::numeric_limits::max()}, slicemax{std::numeric_limits::lowest()}; + for (auto const &point : frustumchunkshapepoints) + { + auto const lightpoint{lightrotation * glm::vec3{point}}; + slicemin = glm::min(slicemin, lightpoint); + slicemax = glm::max(slicemax, lightpoint); + } + m_shadowslicesize = std::max(slicemax.x - slicemin.x, slicemax.y - slicemin.y); + if (m_shadowfitarea != nullptr) + { + glm::vec3 areamin{std::numeric_limits::max()}, areamax{std::numeric_limits::lowest()}; + for (auto const &point : *m_shadowfitarea) + { + auto const lightpoint{lightrotation * point}; + areamin = glm::min(areamin, lightpoint); + areamax = glm::max(areamax, lightpoint); + } + auto const fitmin{glm::vec2{glm::max(glm::vec2{slicemin}, glm::vec2{areamin})}}; + auto const fitmax{glm::vec2{glm::min(glm::vec2{slicemax}, glm::vec2{areamax})}}; + if ((fitmin.x < fitmax.x) && (fitmin.y < fitmax.y)) + { + // the size changes in steps of a power of two of about 1/16 of it, so that the texels of the map keep + // their size until the area grows or shrinks by a step + auto const size{std::max(fitmax.x - fitmin.x, fitmax.y - fitmin.y)}; + quantizationstep = std::exp2(std::ceil(std::log2(std::max(size, 1.f) / 16.f))); + frustumchunkshapepoints.clear(); + auto const inverserotation{glm::transpose(lightrotation)}; + for (int corner = 0; corner < 8; ++corner) + { + auto const lightpoint{glm::vec3{(corner & 1) ? fitmax.x : fitmin.x, (corner & 2) ? fitmax.y : fitmin.y, (corner & 4) ? slicemax.z : slicemin.z}}; + frustumchunkshapepoints.emplace_back(inverserotation * lightpoint, 1.f); + } + } + } + } + // ...modelview matrix: determine the centre of frustum chunk in world space... glm::vec3 frustumchunkmin, frustumchunkmax; bounding_box(frustumchunkmin, frustumchunkmax, std::begin(frustumchunkshapepoints), std::end(frustumchunkshapepoints)); auto const frustumchunkcentre = (frustumchunkmin + frustumchunkmax) * 0.5f; // ...cap the vertical angle to keep shadows from getting too long... - auto const lightvector = glm::normalize(glm::vec3{m_sunlight.direction.x, std::min(m_sunlight.direction.y, Global.gfx_shadow_angle_min), m_sunlight.direction.z}); + auto const lightvector = Shadow_Light_Vector(); // ...place the light source at the calculated centre and setup world space light view matrix... camera.position() = pass_camera + glm::dvec3{frustumchunkcentre}; viewmatrix *= glm::lookAt(camera.position(), camera.position() + glm::dvec3{lightvector}, glm::dvec3{0.f, 1.f, 0.f}); @@ -2108,9 +2218,11 @@ void opengl46_renderer::setup_pass(viewport_config &Viewport, renderpass_config bounding_box(frustumchunkmin, frustumchunkmax, std::begin(frustumchunkshapepoints), std::end(frustumchunkshapepoints)); // quantize the frustum points and add some padding, to reduce shadow shimmer on scale changes auto const frustumchunkdepth { zfar - ( Znear > 1.f ? Znear : 0.f ) }; - auto const quantizationstep{std::min(frustumchunkdepth, 50.f)}; + if (quantizationstep <= 0.f) + quantizationstep = std::min(frustumchunkdepth, 50.f); frustumchunkmin = quantizationstep * glm::floor(frustumchunkmin * (1.f / quantizationstep)); frustumchunkmax = quantizationstep * glm::ceil(frustumchunkmax * (1.f / quantizationstep)); + m_shadowdrawnsize = std::max(frustumchunkmax.x - frustumchunkmin.x, frustumchunkmax.y - frustumchunkmin.y); // ...use the dimensions to set up light projection boundaries... // NOTE: since we only have one cascade map stage, we extend the chunk forward/back to catch areas normally covered by other stages camera.projection() = ortho_projection(frustumchunkmin.x, frustumchunkmax.x, frustumchunkmin.y, frustumchunkmax.y, frustumchunkmin.z - 500.f, frustumchunkmax.z + 500.f); @@ -7606,6 +7718,7 @@ void opengl46_renderer::Preview_Color(TAnimModel *Instance, float const Squaredi // everything is in the first cascade scene_ubs.cascade_end[idx] = std::numeric_limits::max(); } + scene_ubs.shadow_fit = glm::vec4{0.f}; scene_ubo->update(scene_ubs); scene_ubo->bind_uniform(); // the sun is the only light, and there's no fog @@ -8550,6 +8663,158 @@ bool opengl46_renderer::Resolved_Depth_Target(viewport_config &vp) return true; } +glm::vec3 opengl46_renderer::Shadow_Light_Vector() const +{ + // cap the vertical angle to keep shadows from getting too long + return glm::normalize(glm::vec3{m_sunlight.direction.x, std::min(m_sunlight.direction.y, Global.gfx_shadow_angle_min), m_sunlight.direction.z}); +} + +glm::mat3 opengl46_renderer::Shadow_Light_Rotation() const +{ + // the rotation of the view of the shadow pass (setup_pass()) + return glm::mat3{glm::lookAt(glm::vec3{0.f}, Shadow_Light_Vector(), glm::vec3{0.f, 1.f, 0.f})}; +} + +std::array opengl46_renderer::Shadow_Fixed_Splits() const +{ + static_assert(gl::MAX_CASCADES == 3, "the fixed cascades are made for 3 of them"); + std::array splits{0.0f, Global.shadowtune.range / 32, Global.shadowtune.range / 8, Global.shadowtune.range}; + if (Global.shadowtune.map_size > 2048) + { + // increase coverage if our shadow map is large enough to produce decent results + for (std::size_t idx = 1; idx < splits.size(); ++idx) + splits[idx] *= Global.shadowtune.map_size / 2048; + } + return splits; +} + +char const *opengl46_renderer::Shadow_Fit_Unavailable(viewport_config const &Viewport) const +{ + if (false == Global.gfx_shadowmap_enabled) + return "no shadow maps"; + if (Global.bWireFrame) + return "wireframe"; + // the shadows follow the camera, the view shows another one + if (DebugCameraFlag) + return "debug camera"; + if (Global.gfx_skippipeline || (Viewport.msaa_fb == nullptr)) + return "no post processing"; + if (EditorModeFlag && Global.EditorOrtho) + return "orthographic view"; + auto const shadowviews{std::count_if(m_viewports.begin(), m_viewports.end(), [](auto const &View) { return View->shadow; })}; + if ((false == Viewport.shadow) || (shadowviews != 1)) + return "the shadows cover more than one view"; + return nullptr; +} + +bool opengl46_renderer::Shadow_Fit(viewport_config const &Viewport, std::array &Splits, + std::array>, gl::MAX_CASCADES> &Areas) +{ + // a measurement older than this many frames, made with the camera this many metres away or the view turned this many + // degrees from the one now, isn't used + constexpr unsigned int AGE_MAX{6}; + constexpr float SHIFT_MAX{25.f}; + constexpr float TURN_MAX{20.f}; + // metres an area grows by on each side: the vehicles move in the frames since the measurement too + constexpr float MARGIN{0.25f}; + + auto &state{m_shadowfitstate}; + state = shadow_fit_state{}; + if (false == Global.gfx_shadow_sdsm) + return false; + if ((state.reason = Shadow_Fit_Unavailable(Viewport)) != nullptr) + return false; + auto const *latest{m_shadowfit.latest()}; + if (latest == nullptr) + { + state.reason = "no measurement yet"; + return false; + } + auto const &data{latest->data}; + auto const &source{latest->source}; + state.age = m_framestamp - latest->frame; + state.depthmin = data.depthmin; + state.depthmax = data.depthmax; + if (state.age > AGE_MAX) + { + state.reason = "the measurement is too old"; + return false; + } + if (data.samples == 0) + { + state.reason = "only the sky in the view"; + return false; + } + if (source.range != Shadow_Fixed_Splits().back()) + { + state.reason = "the range of the shadows changed"; + return false; + } + // the shadows of this frame are made for its colour pass + auto const &camera{m_colorpass.pass_camera}; + auto const shift{glm::vec3{source.camera - camera.position()}}; + if (glm::length(shift) > SHIFT_MAX) + { + state.reason = "the camera jumped"; + return false; + } + auto const turnmatrix{glm::mat3{camera.modelview()} * glm::transpose(source.rotation)}; + auto const turn{std::acos(std::clamp((turnmatrix[0][0] + turnmatrix[1][1] + turnmatrix[2][2] - 1.f) * 0.5f, -1.f, 1.f))}; + state.turn = glm::degrees(turn); + if (state.turn > TURN_MAX) + { + state.reason = "the view turned too fast"; + return false; + } + auto const &projection{camera.projection()}; + if ((std::abs(projection[0][0] - source.projection[0][0]) > 0.02f * std::abs(projection[0][0])) + || (std::abs(projection[1][1] - source.projection[1][1]) > 0.02f * std::abs(projection[1][1]))) + { + state.reason = "the field of view changed"; + return false; + } + + // the first cascade from the camera, like the fixed ones. setup_pass() takes distances up to 1 m as shares of the far + // end, so the others start past that + Splits[0] = 0.f; + for (std::size_t split = 1; split < Splits.size(); ++split) + Splits[split] = std::max(data.splits[split], Splits[split - 1] + 1.5f); + if (false == std::isfinite(Splits.back())) + { + state.reason = "invalid measurement"; + return false; + } + + auto const inverselight{glm::transpose(source.light)}; + // the scenery stays where it was while the camera moved, the cab and the train the camera rides with moved along. an area + // covers both + auto const lightshift{source.light * shift}; + auto const chord{2.f * std::sin(turn * 0.5f)}; + for (std::size_t cascade = 0; cascade < gl::MAX_CASCADES; ++cascade) + { + // with nothing of the view in a cascade, its map covers the whole slice + if (data.counts[cascade] == 0) + continue; + auto const *bounds{&data.bounds[cascade * 8]}; + glm::vec3 low{opengl46_shadow_fit::unorder(bounds[0]), opengl46_shadow_fit::unorder(bounds[1]), opengl46_shadow_fit::unorder(bounds[2])}; + glm::vec3 high{opengl46_shadow_fit::unorder(bounds[4]), opengl46_shadow_fit::unorder(bounds[5]), opengl46_shadow_fit::unorder(bounds[6])}; + if ((false == std::isfinite(low.x + low.y + high.x + high.y)) || (low.x > high.x) || (low.y > high.y)) + continue; + low = glm::min(low, low + lightshift); + high = glm::max(high, high + lightshift); + // and what came into the view as it turned, out to the reach of the cascade + auto const reach{Splits[cascade + 1] + static_cast(gl::MAX_CASCADES - cascade)}; + auto const margin{reach * chord + MARGIN + 0.01f * std::max(high.x - low.x, high.y - low.y)}; + low -= glm::vec3{margin}; + high += glm::vec3{margin}; + auto &area{Areas[cascade].emplace()}; + for (int corner = 0; corner < 8; ++corner) + area[corner] = inverselight * glm::vec3{(corner & 1) ? high.x : low.x, (corner & 2) ? high.y : low.y, (corner & 4) ? high.z : low.z}; + } + state.fitted = true; + return true; +} + void opengl46_renderer::Render_cluster_heatmap(viewport_config &vp, gl::framebuffer *Target, glm::ivec2 const &Targetsize) { glDebug("Render_cluster_heatmap"); diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index 5092fa9f..956967fc 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -41,6 +41,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/opengl46/opengl46localshadows.h" #include "rendering/opengl46/opengl46instancecull.h" #include "rendering/opengl46/opengl46hiz.h" +#include "rendering/opengl46/opengl46shadowfit.h" // bare-bones render controller, in lack of anything better yet class opengl46_renderer : public gfx_renderer { @@ -433,6 +434,19 @@ class opengl46_renderer : public gfx_renderer { void Setup_Contact_Shadows( viewport_config &Viewport ); // makes the target of the resolved depth of the viewport if it isn't there yet; false if it can't be made bool Resolved_Depth_Target( viewport_config &Viewport ); + // the direction the sun shadows are cast in (the sun, its height capped by gfx.shadow.angle.min), and the rotation from + // the world to the space of the light of the cascades: x and y across the maps, z along the light + glm::vec3 Shadow_Light_Vector() const; + glm::mat3 Shadow_Light_Rotation() const; + // the distances the fixed cascades of the sun shadows go from and to (shadowtune) + std::array Shadow_Fixed_Splits() const; + // why the cascades of the sun shadows can't be fitted to the depth of the viewport (gfx.shadow.sdsm), null if they can + char const *Shadow_Fit_Unavailable( viewport_config const &Viewport ) const; + // the cascades of the frame fitted to the latest measurement of the depth of the view (gfx.shadow.sdsm): the distances + // they go from and to, and for each cascade the corners of the area its map is to cover, camera-relative world space + // (none: the whole slice of the view). false: the fixed cascades. fills m_shadowfitstate + bool Shadow_Fit( viewport_config const &Viewport, std::array &Splits, + std::array>, gl::MAX_CASCADES> &Areas ); // whether the depth pass of the whole scene draws the material bound last (Bind_Material()) and the geometry bool Scene_Depth_Draws( gfx::geometry_handle const &Geometry ) const; bool Render(TDynamicObject *Dynamic); @@ -653,6 +667,26 @@ class opengl46_renderer : public gfx_renderer { std::unordered_map, scene::basic_cell::instance_bucket_key_hash> m_sleeperbandcounts; // depth pyramid for the occlusion test of the gpu culling opengl46_hiz m_hiz; + // the cascades of the sun shadows fitted to the depth of the main view (Global.gfx_shadow_sdsm) + opengl46_shadow_fit m_shadowfit; + // with a cascade fitted: the corners of the area its map is to cover, for setup_pass() + std::array const *m_shadowfitarea { nullptr }; + // what the last shadow pass did, for the debug panel + struct shadow_fit_state { + bool fitted { false }; + char const *reason { nullptr }; // why not, with gfx.shadow.sdsm on + unsigned int age { 0 }; // frames since the measurement + float depthmin { 0.f }; + float depthmax { 0.f }; + float turn { 0.f }; // of the view since the measurement, degrees + std::array splits {}; + // size of the area of the map of each cascade: of the whole slice of the view, and as drawn; metres + std::array slice {}; + std::array drawn {}; + } m_shadowfitstate; + // the sizes setup_pass() found for the last shadow pass: of the whole slice of the view and as drawn + float m_shadowslicesize { 0.f }; + float m_shadowdrawnsize { 0.f }; // a draw recorded from the walk of the submodel tree of a group in one lod band (Render_Instances_Indirect()), to be // replayed once the instances of its command are known: the state the walk left for it and what it bound struct indirect_draw { diff --git a/rendering/opengl46/opengl46shadowfit.cpp b/rendering/opengl46/opengl46shadowfit.cpp new file mode 100644 index 00000000..f9c554dc --- /dev/null +++ b/rendering/opengl46/opengl46shadowfit.cpp @@ -0,0 +1,171 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "rendering/opengl46/opengl46shadowfit.h" + +#include + +#include "model/Texture.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" + +namespace +{ + +// the nearest distance the first cascade may start from and the least one the last reaches, metres: the cascades don't +// get any closer to the camera than this, however close the pixels are +float const SPLIT_NEAR_MIN{0.5f}; +float const SPLIT_FAR_MIN{16.f}; +// how far past its distances a cascade takes the pixels, this many metres and this share of the distance: the camera moves +// in the frame or two before the cascades are drawn from the measurement, and a pixel which came into another cascade since +// has to be within its map +float const MARGIN_METRES{1.f}; +float const MARGIN_SHARE{0.05f}; + +} // namespace + +opengl46_shadow_fit::~opengl46_shadow_fit() +{ + release(); +} + +void opengl46_shadow_fit::init() +{ + auto const make_compute = [](std::string const &Name) { + gl::shader shader(Name); + return std::make_unique(std::vector>{shader}); + }; + m_depthprogram = make_compute("shadow_fit_depth.comp"); + m_boundsprogram = make_compute("shadow_fit_bounds.comp"); + m_buffer = std::make_unique(sizeof(gl::shadow_fit_data), gl::SHADOW_FIT_SSBO_BINDING); + m_readback = std::make_unique(sizeof(gl::shadow_fit_data)); +} + +void opengl46_shadow_fit::release() +{ + if (m_depthfb != 0) + glDeleteFramebuffers(1, &m_depthfb); + if (m_depth != 0) + glDeleteTextures(1, &m_depth); + m_depthfb = 0; + m_depth = 0; + m_depthsize = glm::ivec2{0}; +} + +void opengl46_shadow_fit::allocate(int const Width, int const Height) +{ + release(); + m_depthsize = glm::ivec2{Width, Height}; + // the blit of depth needs the format of the source + glCreateTextures(GL_TEXTURE_2D, 1, &m_depth); + glTextureStorage2D(m_depth, 1, Global.gfx_format_depth, Width, Height); + glTextureParameteri(m_depth, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTextureParameteri(m_depth, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTextureParameteri(m_depth, GL_TEXTURE_COMPARE_MODE, GL_NONE); + glCreateFramebuffers(1, &m_depthfb); + glNamedFramebufferTexture(m_depthfb, GL_DEPTH_ATTACHMENT, m_depth, 0); + glNamedFramebufferDrawBuffer(m_depthfb, GL_NONE); + glNamedFramebufferReadBuffer(m_depthfb, GL_NONE); +} + +void opengl46_shadow_fit::measure(GLuint const Depth, GLuint const Source, int const Width, int const Height, view const &View, unsigned int const Frame) +{ + if ((Width <= 0) || (Height <= 0) || (View.range <= 0.f)) + return; + + m_timer.begin(); + + auto depth{Depth}; + if (depth == 0) + { + if (m_depthsize != glm::ivec2{Width, Height}) + allocate(Width, Height); + // the scissor test would limit the blit + auto const scissor{glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE}; + if (scissor) + glDisable(GL_SCISSOR_TEST); + glBlitNamedFramebuffer(Source, m_depthfb, 0, 0, Width, Height, 0, 0, Width, Height, GL_DEPTH_BUFFER_BIT, GL_NEAREST); + if (scissor) + glEnable(GL_SCISSOR_TEST); + depth = m_depth; + } + + // numbers start from 1, 0 is no measurement + if (++m_measurement == 0) + ++m_measurement; + gl::shadow_fit_data data{}; + data.measurement = m_measurement; + data.depthmin = std::numeric_limits::infinity(); + for (std::size_t cascade = 0; cascade < gl::MAX_CASCADES; ++cascade) + { + for (std::size_t axis = 0; axis < 4; ++axis) + { + data.bounds[cascade * 8 + axis] = 0xFFFFFFFFu; + data.bounds[cascade * 8 + 4 + axis] = 0u; + } + } + m_buffer->update(data); + m_buffer->bind_base(); + + auto const inverseprojection{glm::inverse(View.projection)}; + auto const viewtolight{glm::mat4{View.light * glm::inverse(View.rotation)}}; + auto const groupsx{(static_cast(Width) + gl::SHADOW_FIT_GROUP_SIZE - 1) / gl::SHADOW_FIT_GROUP_SIZE}; + auto const groupsy{(static_cast(Height) + gl::SHADOW_FIT_GROUP_SIZE - 1) / gl::SHADOW_FIT_GROUP_SIZE}; + glBindTextureUnit(0, depth); + + glProgramUniformMatrix4fv(*m_depthprogram, 0, 1, GL_FALSE, glm::value_ptr(inverseprojection)); + m_depthprogram->dispatch(groupsx, groupsy); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + + auto const farmin{std::min(SPLIT_FAR_MIN, View.range)}; + glProgramUniformMatrix4fv(*m_boundsprogram, 0, 1, GL_FALSE, glm::value_ptr(inverseprojection)); + glProgramUniformMatrix4fv(*m_boundsprogram, 4, 1, GL_FALSE, glm::value_ptr(viewtolight)); + glProgramUniform4f(*m_boundsprogram, 8, View.range, std::min(SPLIT_NEAR_MIN, farmin * 0.25f), farmin, std::clamp(Global.gfx_shadow_sdsm_split, 0.f, 1.f)); + glProgramUniform4f(*m_boundsprogram, 9, MARGIN_METRES, MARGIN_SHARE, 0.f, 0.f); + m_boundsprogram->dispatch(groupsx, groupsy); + // for the copy of the read back + glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT); + + glBindTextureUnit(0, 0); + opengl_texture::reset_unit_cache(); + + m_timer.end(); + + m_readback->push(*m_buffer); + m_pending[m_measurement % PENDING] = pending{m_measurement, View, Frame}; +} + +opengl46_shadow_fit::result const *opengl46_shadow_fit::latest() +{ + gl::shadow_fit_data data; + if (m_readback && m_readback->pop(&data)) + { + auto const &source{m_pending[data.measurement % PENDING]}; + if ((data.measurement != 0) && (source.measurement == data.measurement)) + m_latest = result{data, source.source, source.frame}; + } + return m_latest ? &(*m_latest) : nullptr; +} + +float opengl46_shadow_fit::unorder(std::uint32_t const Value) +{ + auto const bits{((Value & 0x80000000u) != 0u) ? (Value & 0x7FFFFFFFu) : ~Value}; + float result; + std::memcpy(&result, &bits, sizeof(result)); + return result; +} + +std::string opengl46_shadow_fit::info_times() +{ + std::string text; + if (auto const time = m_timer.result()) + text += " sun shadows fitted to the depth: " + to_string(static_cast(*time) / 1000000.0, 3) + " ms\n"; + return text; +} diff --git a/rendering/opengl46/opengl46shadowfit.h b/rendering/opengl46/opengl46shadowfit.h new file mode 100644 index 00000000..a59bc66c --- /dev/null +++ b/rendering/opengl46/opengl46shadowfit.h @@ -0,0 +1,94 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include + +#include "gl/ubo.h" +#include "gl/ssbo.h" +#include "gl/shader.h" +#include "gl/query.h" +#include "gl/readback.h" + +// the cascades of the sun shadows fitted to the depth of the main view (sample distribution shadow maps, gfx.shadow.sdsm). +// at the end of the colour pass of the main view two compute passes go over its resolved depth: the nearest and the farthest +// distance of the pixels which aren't the sky (shadow_fit_depth.comp), then the distances split between the cascades and, +// for each cascade, the box of the pixels it takes in the space of the light (shadow_fit_bounds.comp). the result comes back +// to the cpu a frame or two later without waiting (gl::readback), with the camera and the light it was measured with, and +// the renderer makes the cascades of the frame cover the boxes (opengl46_renderer::Shadow_Fit()) instead of the whole slices +// of the view between the fixed distances of shadowtune +class opengl46_shadow_fit +{ + public: + // the view and the light a measurement was made with + struct view + { + glm::dvec3 camera; // position of the camera + glm::mat4 projection; // projection of the view + glm::mat3 rotation; // world to view space, rotation only + glm::mat3 light; // world to the space of the light, rotation only + float range{0.f}; // the farthest distance the cascades may reach + }; + // a measurement which came back + struct result + { + gl::shadow_fit_data data; + view source; + unsigned int frame{0}; // frame of the renderer it was made in + }; + + opengl46_shadow_fit() = default; + ~opengl46_shadow_fit(); + opengl46_shadow_fit(const opengl46_shadow_fit &) = delete; + opengl46_shadow_fit &operator=(const opengl46_shadow_fit &) = delete; + + // creates the programs and the buffer. throws gl::shader_exception + void init(); + // measures the depth of the view: Depth, its resolved depth texture of Width x Height pixels, or 0 to resolve the depth + // attachment of Source, the multisampled framebuffer, into a texture of its own. Frame: the frame of the renderer. + // texture unit 0 is used (the unit cache is reset), framebuffer bindings are left alone + void measure(GLuint const Depth, GLuint const Source, int const Width, int const Height, view const &View, unsigned int const Frame); + // takes in the newest measurement the gpu has finished, if any, and returns the newest one taken in so far + result const *latest(); + // the time of the passes, for the debug panel + std::string info_times(); + + // the ends of the bounds, as the passes store them (shadow_fit_order() in shadow_fit_bounds.comp) + static float unorder(std::uint32_t const Value); + + private: + // the texture the depth is resolved to when there is no resolved depth to measure + void allocate(int const Width, int const Height); + void release(); + + // a measurement on its way back, kept by its number + struct pending + { + std::uint32_t measurement{0}; // 0: none + view source; + unsigned int frame{0}; + }; + static constexpr std::size_t PENDING = 8; // more than the copies gl::readback keeps in flight + + std::unique_ptr m_depthprogram; + std::unique_ptr m_boundsprogram; + std::unique_ptr m_buffer; + std::unique_ptr m_readback; + std::array m_pending; + std::uint32_t m_measurement{0}; // number of the last measurement + std::optional m_latest; + GLuint m_depth{0}; + GLuint m_depthfb{0}; + glm::ivec2 m_depthsize{0}; + gl::gpu_timer m_timer; +}; diff --git a/shaders/fog_inject.comp b/shaders/fog_inject.comp index 5e65646d..1b864811 100644 --- a/shaders/fog_inject.comp +++ b/shaders/fog_inject.comp @@ -87,13 +87,22 @@ float sun_shadow(vec3 position) for (cascade = 0U; cascade < MAX_CASCADES; cascade++) if (span_distance <= cascade_end[cascade]) break; - if (cascade >= MAX_CASCADES) - return 0.0; - vec4 lightpos = lightview[cascade] * vec4(position, 1.0); - vec3 coords = lightpos.xyz / lightpos.w; - if (coords.z < 0.0 || any(lessThan(coords.xy, vec2(0.0))) || any(greaterThan(coords.xy, vec2(1.0)))) - return 0.0; - return texture(shadowmap, vec4(coords.xy, float(cascade), coords.z + 0.0001 * float(cascade + 1U))); + for (; cascade < MAX_CASCADES; cascade++) + { + vec4 lightpos = lightview[cascade] * vec4(position, 1.0); + vec3 coords = lightpos.xyz / lightpos.w; + if (all(greaterThanEqual(coords.xy, vec2(0.0))) && all(lessThanEqual(coords.xy, vec2(1.0)))) + { + if (coords.z < 0.0) + return 0.0; + return texture(shadowmap, vec4(coords.xy, float(cascade), coords.z + 0.0001 * float(cascade + 1U))); + } + // outside the map: lit, unless the cascades are fitted to the depth of the view (gfx.shadow.sdsm), whose maps cover + // only the area of the surfaces seen; then the first further cascade whose map holds the point + if (shadow_fit.x <= 0.0) + return 0.0; + } + return 0.0; #else return 0.0; #endif diff --git a/shaders/light_common.glsl b/shaders/light_common.glsl index 913f6ffc..87dca89a 100644 --- a/shaders/light_common.glsl +++ b/shaders/light_common.glsl @@ -128,6 +128,21 @@ float calc_shadow_map() for (cascade = 0U; cascade < MAX_CASCADES; cascade++) if (distance <= cascade_end[cascade]) break; + // cascades fitted to the depth of the view of an earlier frame (gfx.shadow.sdsm) cover only the area of the points seen + // then: a point at the edge of the view or one which came closer since may be outside the map of its cascade. it goes to + // the first further cascade whose map holds it with the reach of the filter, and is lit past the last one + if (shadow_fit.x > 0.0) + { + float border = 4.0 / float(textureSize(shadowmap, 0).x); + for (; cascade < MAX_CASCADES; cascade++) + { + vec2 fitcoords = (lightview[cascade] * vec4(f_pos.xyz, 1.0)).xy; + if (all(greaterThanEqual(fitcoords, vec2(border))) && all(lessThanEqual(fitcoords, vec2(1.0 - border)))) + break; + } + if (cascade >= MAX_CASCADES) + return 0.0; + } // Normal offset: sample the shadow map from a point pushed along the surface normal // by a fraction of the cascade's texel footprint. A constant depth bias can't keep up // with large texels (far cascades, small shadow maps) on surfaces at a grazing angle diff --git a/shaders/shadow_fit_bounds.comp b/shaders/shadow_fit_bounds.comp new file mode 100644 index 00000000..2112a48f --- /dev/null +++ b/shaders/shadow_fit_bounds.comp @@ -0,0 +1,126 @@ +// the cascades of the sun shadows fitted to the depth of the main view (opengl46_shadow_fit), second pass: the distances +// from the nearest to the farthest pixel which isn't the sky (shadow_fit_depth.comp) split between the cascades, and for each +// cascade the box of the pixels it takes, in the space of the light. the cpu makes the maps of the cascades of a frame or +// two later cover the boxes: the camera has moved since, so a cascade takes the pixels a little past its distances as well, +// and the shading goes to the next cascade with a point outside the map of its own (light_common.glsl) + +#include + +layout(local_size_x = SHADOW_FIT_GROUP_SIZE, local_size_y = SHADOW_FIT_GROUP_SIZE) in; + +layout(binding = 0) uniform sampler2D scene_depth; + +layout(std430, binding = SHADOW_FIT_SSBO_BINDING) buffer shadow_fit_ssbo +{ + uint measurement; + uint samples; + // positive floats, as bits + uint depthmin; + uint depthmax; + float splits[4]; + uint counts[4]; + // per cascade the lowest x, y, z, unused, then the highest x, y, z, unused, as shadow_fit_order() gives them + uint bounds[MAX_CASCADES * 8U]; +}; + +// the inverse of the projection of the view +layout(location = 0) uniform mat4 inv_projection; +// rotation from the view space to the space of the light: x and y across the maps, z along the light +layout(location = 4) uniform mat4 view_to_light; +// x: the farthest distance the cascades may reach (the one of the fixed cascades), y: the nearest one the first may start +// from, z: the least one the last reaches, w: the share of equal ratios in the splits, the rest in equal lengths +layout(location = 8) uniform vec4 split_params; +// how far past its distances a cascade takes the pixels, x + y * distance metres +layout(location = 9) uniform vec4 margin_params; + +shared uint shared_bounds[MAX_CASCADES * 8U]; +shared uint shared_counts[MAX_CASCADES]; + +// a float as a uint which orders the same way, also the negative ones +uint shadow_fit_order(float value) +{ + uint bits = floatBitsToUint(value); + return ((bits & 0x80000000U) != 0U) ? ~bits : (bits | 0x80000000U); +} + +// the distances the cascades go from and to. both ends are moved out to the next step of 1/8 of a power of two, so that +// the cascades change only now and then with the pixels coming and going +void shadow_fit_splits(out float result[MAX_CASCADES + 1U]) +{ + float rangemax = split_params.x; + float far = clamp(uintBitsToFloat(depthmax), split_params.z, rangemax); + far = min(exp2(ceil(log2(far) * 8.0) / 8.0), rangemax); + float near = clamp(uintBitsToFloat(depthmin), split_params.y, far * 0.25); + near = exp2(floor(log2(near) * 8.0) / 8.0); + result[0] = near; + result[MAX_CASCADES] = far; + for (uint cascade = 1U; cascade < MAX_CASCADES; ++cascade) + { + float share = float(cascade) / float(MAX_CASCADES); + result[cascade] = mix(near + (far - near) * share, near * pow(far / near, share), split_params.w); + } +} + +void main() +{ + uint index = gl_LocalInvocationIndex; + if (index < MAX_CASCADES * 8U) + shared_bounds[index] = ((index & 4U) == 0U) ? 0xFFFFFFFFU : 0U; + if (index < MAX_CASCADES) + shared_counts[index] = 0U; + barrier(); + + float cascadesplits[MAX_CASCADES + 1U]; + shadow_fit_splits(cascadesplits); + + ivec2 pixel = ivec2(gl_GlobalInvocationID.xy); + ivec2 size = textureSize(scene_depth, 0); + if ((samples > 0U) && all(lessThan(pixel, size))) + { + float depth = texelFetch(scene_depth, pixel, 0).r; + if (depth > 0.0) + { + vec2 ndc = (vec2(pixel) + 0.5) / vec2(size) * 2.0 - 1.0; + vec4 position = inv_projection * vec4(ndc, depth, 1.0); + vec3 view = position.xyz / position.w; + float distance = length(view); + vec3 light = mat3(view_to_light) * view; + uvec3 ordered = uvec3(shadow_fit_order(light.x), shadow_fit_order(light.y), shadow_fit_order(light.z)); + for (uint cascade = 0U; cascade < MAX_CASCADES; ++cascade) + { + // the end of a cascade has the padding of opengl46_renderer::Render_pass(), MAX_CASCADES - cascade metres + float low = (cascade == 0U) ? 0.0 : cascadesplits[cascade] - (margin_params.x + margin_params.y * cascadesplits[cascade]); + float high = cascadesplits[cascade + 1U] + float(MAX_CASCADES - cascade) + margin_params.x + margin_params.y * cascadesplits[cascade + 1U]; + if ((distance < low) || (distance > high)) + continue; + uint first = cascade * 8U; + atomicAdd(shared_counts[cascade], 1U); + atomicMin(shared_bounds[first + 0U], ordered.x); + atomicMin(shared_bounds[first + 1U], ordered.y); + atomicMin(shared_bounds[first + 2U], ordered.z); + atomicMax(shared_bounds[first + 4U], ordered.x); + atomicMax(shared_bounds[first + 5U], ordered.y); + atomicMax(shared_bounds[first + 6U], ordered.z); + } + } + } + barrier(); + + if ((index < MAX_CASCADES * 8U) && ((index & 3U) != 3U)) + { + if ((index & 4U) == 0U) + { + if (shared_bounds[index] != 0xFFFFFFFFU) + atomicMin(bounds[index], shared_bounds[index]); + } + else if (shared_bounds[index] != 0U) + atomicMax(bounds[index], shared_bounds[index]); + } + if ((index < MAX_CASCADES) && (shared_counts[index] > 0U)) + atomicAdd(counts[index], shared_counts[index]); + if (all(equal(gl_GlobalInvocationID.xy, uvec2(0U)))) + { + for (uint split = 0U; split <= MAX_CASCADES; ++split) + splits[split] = cascadesplits[split]; + } +} diff --git a/shaders/shadow_fit_depth.comp b/shaders/shadow_fit_depth.comp new file mode 100644 index 00000000..2f2dee18 --- /dev/null +++ b/shaders/shadow_fit_depth.comp @@ -0,0 +1,65 @@ +// the cascades of the sun shadows fitted to the depth of the main view (opengl46_shadow_fit), first pass: how many pixels of +// the resolved depth of the view aren't the sky, and the nearest and the farthest of their distances from the camera. +// the depth is reversed with the far plane at infinity: the sky, which writes no depth, stays at 0. +// shadow_fit_bounds.comp splits the distances between the cascades + +#include + +layout(local_size_x = SHADOW_FIT_GROUP_SIZE, local_size_y = SHADOW_FIT_GROUP_SIZE) in; + +layout(binding = 0) uniform sampler2D scene_depth; + +layout(std430, binding = SHADOW_FIT_SSBO_BINDING) buffer shadow_fit_ssbo +{ + uint measurement; + uint samples; + // positive floats, as bits + uint depthmin; + uint depthmax; + float splits[4]; + uint counts[4]; + uint bounds[MAX_CASCADES * 8U]; +}; + +// the inverse of the projection of the view +layout(location = 0) uniform mat4 inv_projection; + +shared uint shared_samples; +shared uint shared_min; +shared uint shared_max; + +void main() +{ + if (gl_LocalInvocationIndex == 0U) + { + shared_samples = 0U; + // infinity + shared_min = 0x7F800000U; + shared_max = 0U; + } + barrier(); + + ivec2 pixel = ivec2(gl_GlobalInvocationID.xy); + ivec2 size = textureSize(scene_depth, 0); + if (all(lessThan(pixel, size))) + { + float depth = texelFetch(scene_depth, pixel, 0).r; + if (depth > 0.0) + { + vec2 ndc = (vec2(pixel) + 0.5) / vec2(size) * 2.0 - 1.0; + vec4 position = inv_projection * vec4(ndc, depth, 1.0); + uint distance = floatBitsToUint(length(position.xyz / position.w)); + atomicAdd(shared_samples, 1U); + atomicMin(shared_min, distance); + atomicMax(shared_max, distance); + } + } + barrier(); + + if ((gl_LocalInvocationIndex == 0U) && (shared_samples > 0U)) + { + atomicAdd(samples, shared_samples); + atomicMin(depthmin, shared_min); + atomicMax(depthmax, shared_max); + } +} diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 30923aa4..57fed74e 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1607,6 +1607,17 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token Parser >> gfx_shadow_rank_cutoff; gfx_shadow_rank_cutoff = std::clamp(gfx_shadow_rank_cutoff, 1, 3); } + else if (Token == "gfx.shadow.sdsm") + { + Parser.getTokens(1); + Parser >> gfx_shadow_sdsm; + } + else if (Token == "gfx.shadow.sdsm.split") + { + Parser.getTokens(1); + Parser >> gfx_shadow_sdsm_split; + gfx_shadow_sdsm_split = std::clamp(gfx_shadow_sdsm_split, 0.f, 1.f); + } else if (ConfigParse_gfx_sky(Parser, Token)) { // parsed @@ -2006,6 +2017,8 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.shadergamma", gfx_shadergamma ); export_as_text( Output, "gfx.shadow.angle.min", gfx_shadow_angle_min ); export_as_text( Output, "gfx.shadow.rank.cutoff", gfx_shadow_rank_cutoff ); + export_as_text( Output, "gfx.shadow.sdsm", gfx_shadow_sdsm ); + export_as_text( Output, "gfx.shadow.sdsm.split", gfx_shadow_sdsm_split ); export_as_text( Output, "gfx.ambient.sh", gfx_ambient_sh ); export_as_text( Output, "gfx.ambient.sh.strength", gfx_ambient_sh_strength ); export_as_text( Output, "gfx.ambient.groundalbedo", gfx_ambient_groundalbedo ); diff --git a/utilities/Globals.h b/utilities/Globals.h index f836fe04..97fe91c4 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -334,6 +334,13 @@ struct global_settings { float gfx_distance_factor_max { 3.f }; float gfx_shadow_angle_min { -0.2f }; int gfx_shadow_rank_cutoff { 3 }; + // (opengl 4.6) the cascades of the sun shadows are fitted to the depth of the main view of a frame or two before (SDSM): + // the distances they cover go from the nearest to the farthest pixel which isn't the sky, and the map of each covers only + // the area of the pixels it takes. false: the fixed cascades of shadowtune + bool gfx_shadow_sdsm { false }; + // with gfx_shadow_sdsm: how the distances between the nearest and the farthest pixel are split between the cascades, + // 0: in equal lengths, 1: in equal ratios + float gfx_shadow_sdsm_split { 0.9f }; // sky light and exposure of the opengl 4.6 renderer. the switches can also be changed at run time in the debug window // "Sky and exposure"; the scales are multipliers of the calibrated defaults (1 = as calibrated)