From 5f365a00c59a66eaee8594773c32c0018f490426 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 15 Oct 2026 23:03:50 +0200 Subject: [PATCH] Fix the screen space ambient occlusion and add ground truth ambient occlusion to the GL 4.6 renderer --- CMakeLists.txt | 1 + docs/opengl46-renderer.md | 20 +++- nodebank_previews_standalone.cpp | 2 +- rendering/opengl46/opengl46ao.cpp | 130 ++++++++++++++++++++ rendering/opengl46/opengl46ao.h | 55 +++++++++ rendering/opengl46/opengl46renderer.cpp | 72 ++++++----- rendering/opengl46/opengl46renderer.h | 10 +- shaders/gtao_denoise.comp | 50 ++++++++ shaders/gtao_main.comp | 152 ++++++++++++++++++++++++ shaders/postfx_ssao.frag | 5 +- shaders/postfx_ssao_apply.frag | 86 +++++++++++--- utilities/Globals.cpp | 20 ++++ utilities/Globals.h | 11 +- 13 files changed, 564 insertions(+), 50 deletions(-) create mode 100644 rendering/opengl46/opengl46ao.cpp create mode 100644 rendering/opengl46/opengl46ao.h create mode 100644 shaders/gtao_denoise.comp create mode 100644 shaders/gtao_main.comp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8785eeea..da5ee1e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -336,6 +336,7 @@ if (WITH_OPENGL_MODERN) "rendering/opengl46/opengl46skydome.cpp" "rendering/opengl46/opengl46sky.cpp" "rendering/opengl46/opengl46exposure.cpp" + "rendering/opengl46/opengl46ao.cpp" "rendering/opengl46/opengl46precipitation.cpp" "rendering/opengl46/opengl46particles.cpp" "rendering/opengl46/opengl46terrain.cpp" diff --git a/docs/opengl46-renderer.md b/docs/opengl46-renderer.md index 0ef23d93..0528bfe0 100644 --- a/docs/opengl46-renderer.md +++ b/docs/opengl46-renderer.md @@ -81,6 +81,21 @@ existed. The automatic exposure and the bloom need the post processing pipeline (`gfx.skippipeline no`, the default). Model previews of the editor use none of these. +### Ambient occlusion + +Darkening of creases, corners and the ground under objects, worked out from the depth of the image (screen space) +and multiplied into the image before the motion blur and the tonemapping. It works with and without motion blur. +It darkens all light alike, the sun and the headlights included; limiting it to the ambient light needs the depth +before the shading (a depth prepass), which is planned later. + +| Entry | Default | Meaning | +|---|---|---| +| `gfx.postfx.ao gtao/ssao/no` | `gtao` | `gtao` (or `yes`): ground truth ambient occlusion (Jimenez et al. 2016), at half the resolution with a depth aware smoothing and upsampling. `ssao`: the older, experimental ssao. `no`: none, and no targets are made for it; the mode can then not be turned on in the debug window | +| `gfx.postfx.ao.radius` | `1.5` | 0.1..10: reach of the occluders, metres | +| `gfx.postfx.ao.strength` | `1.0` | 0..4: power the occlusion is raised to; 1 = as computed, 0 = none | +| `gfx.postfx.ao.bounce` | `0.3` | 0..1: albedo of the surfaces assumed for the light that comes back after more bounces (brightens the occlusion a little) | +| `gfx.postfx.ao.quality slices steps` | `3 4` | directions searched around each pixel (1..8) and samples on each side of a direction (1..16) | + ### Changed option `dynamiclights` (0..7) used to limit the number of headlight sets that light the surroundings. It is still @@ -96,7 +111,7 @@ read, but it only takes effect with `gfx.lights.clustered no`. `gfx.lights.clustered no`. - **Reflections (environment map)** are lit with the sun as seen from the reflection itself, and vehicle headlights no longer light the scene in them. -- **Motion blur and SSAO** read the real depth of the scene. Before, the depth texture was read in a way the +- **Motion blur and ambient occlusion** read the real depth of the scene. Before, the depth texture was read in a way the specification leaves undefined, and the result depended on the driver. - **Lines are 1 pixel wide**, as they always were on this kind of context. The widths the code asked for (catenary wires, map tracks) never took effect. @@ -116,7 +131,8 @@ editor goes through the first `dynamiclights` headlights, the reflections throug - **Sky and exposure** window (next to the Light clusters window): the switches and scales above, the share of the atmosphere, the colours of the sun, ambient light and fog in use, and the measured luminance and exposure. Shift+F12 shows the GPU time of `atmosphere:` (look-up tables, only when the sun moved or the weather changed), - `envmap ggx:` (prefiltering, with each update of the environment map), `exposure:` and `bloom:`. + `envmap ggx:` (prefiltering, with each update of the environment map), `exposure:` and `bloom:`. The same window holds + the ambient occlusion mode and its settings; Shift+F12 shows its time as `ambient occlusion:` (gtao only). - `gfx.gldebug yes` turns on the OpenGL debug output (`KHR_debug`); messages go to the log. ## Building diff --git a/nodebank_previews_standalone.cpp b/nodebank_previews_standalone.cpp index afc84753..eb08af2f 100644 --- a/nodebank_previews_standalone.cpp +++ b/nodebank_previews_standalone.cpp @@ -315,7 +315,7 @@ int generate_nodebank_previews(int Argc, char *Argv[]) Global.gfx_skippipeline = false; Global.gfx_postfx_motionblur_enabled = false; Global.gfx_postfx_chromaticaberration_enabled = false; - Global.gfx_postfx_ssao_enabled = false; + Global.gfx_postfx_ao = 0; Global.gfx_envmap_enabled = false; Global.gfx_shadowmap_enabled = settings.shadows; Global.shadowtune.map_size = std::max(Global.shadowtune.map_size, 2048u); diff --git a/rendering/opengl46/opengl46ao.cpp b/rendering/opengl46/opengl46ao.cpp new file mode 100644 index 00000000..b9e83317 --- /dev/null +++ b/rendering/opengl46/opengl46ao.cpp @@ -0,0 +1,130 @@ +/* +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/opengl46ao.h" + +#include "model/Texture.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" + +namespace +{ + +// work group of the compute passes, 8x8 (gtao_*.comp) +int const GROUP_SIZE{8}; + +} // namespace + +void opengl46_ao::init() +{ + auto const make_compute = [](std::string const &Name) { + gl::shader shader(Name); + return std::make_unique(std::vector>{shader}); + }; + m_main_program = make_compute("gtao_main.comp"); + m_denoise_program = make_compute("gtao_denoise.comp"); + { + gl::shader vertex("quad.vert"); + gl::shader fragment("postfx_ssao_apply.frag"); + m_apply_program = std::make_unique(std::vector>{vertex, fragment}); + } + m_vao = std::make_unique(); +} + +void opengl46_ao::compute(opengl_texture &Depth, opengl_texture &Raw, opengl_texture &Smooth, glm::mat4 const &Projection) +{ + m_timer.begin(); + m_timing = true; + + auto const inverseprojection{glm::inverse(Projection)}; + auto const groups_x{static_cast((Raw.get_width() + GROUP_SIZE - 1) / GROUP_SIZE)}; + auto const groups_y{static_cast((Raw.get_height() + GROUP_SIZE - 1) / GROUP_SIZE)}; + + // horizon search + Depth.bind(0); + glProgramUniformMatrix4fv(*m_main_program, 0, 1, GL_FALSE, glm::value_ptr(inverseprojection)); + glProgramUniform2f(*m_main_program, 1, Global.gfx_postfx_ao_radius, Projection[0][0] * 0.5f * static_cast(Raw.get_width())); + glProgramUniform4f(*m_main_program, 2, Projection[0][3], Projection[1][3], Projection[2][3], Projection[3][3]); + glProgramUniform2i(*m_main_program, 3, Global.gfx_postfx_ao_slices, Global.gfx_postfx_ao_steps); + glBindImageTexture(0, Raw.id, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_R8); + m_main_program->dispatch(groups_x, groups_y); + glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT); + + // smoothing + Raw.bind(1); + glProgramUniformMatrix4fv(*m_denoise_program, 0, 1, GL_FALSE, glm::value_ptr(inverseprojection)); + glBindImageTexture(0, Smooth.id, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_R8); + m_denoise_program->dispatch(groups_x, groups_y); + glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT); + + glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_R8); + opengl_texture::reset_unit_cache(); +} + +void opengl46_ao::apply(int const Mode, opengl_texture &Depth, opengl_texture &Occlusion, gl::framebuffer &Target, glm::mat4 const &Projection) +{ + Target.bind(); + m_apply_program->bind(); + m_vao->bind(); + Depth.bind(0); + Occlusion.bind(1); + glProgramUniformMatrix4fv(*m_apply_program, 0, 1, GL_FALSE, glm::value_ptr(glm::inverse(Projection))); + glProgramUniform4f(*m_apply_program, 1, (Mode == 1 ? 1.f : 0.f), Global.gfx_postfx_ao_strength, Global.gfx_postfx_ao_bounce, 0.f); + + // result = image * occlusion; the blending state is put back as it was + auto const blending{glIsEnabled(GL_BLEND) == GL_TRUE}; + GLint blendfunc[4]; + glGetIntegerv(GL_BLEND_SRC_RGB, &blendfunc[0]); + glGetIntegerv(GL_BLEND_DST_RGB, &blendfunc[1]); + glGetIntegerv(GL_BLEND_SRC_ALPHA, &blendfunc[2]); + glGetIntegerv(GL_BLEND_DST_ALPHA, &blendfunc[3]); + glEnable(GL_BLEND); + glBlendFunc(GL_ZERO, GL_SRC_COLOR); + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBlendFuncSeparate(blendfunc[0], blendfunc[1], blendfunc[2], blendfunc[3]); + if (!blending) + glDisable(GL_BLEND); + + opengl_texture::reset_unit_cache(); + + if (m_timing) + { + m_timer.end(); + m_timing = false; + } +} + +std::string opengl46_ao::info_times() +{ + std::string text; + if (auto const time = m_timer.result()) + text += " ambient occlusion: " + to_string(static_cast(*time) / 1000000.0, 3) + " ms\n"; + return text; +} + +void opengl46_ao::draw_debug_ui(bool const Available) +{ + if (!Available) + { + ImGui::TextUnformatted("ambient occlusion: off at the start, set gfx.postfx.ao in eu07.ini"); + return; + } + char const *modes[]{"off", "older ssao", "ground truth (gtao)"}; + ImGui::Combo("ambient occlusion (gfx.postfx.ao)", &Global.gfx_postfx_ao, modes, IM_ARRAYSIZE(modes)); + if (Global.gfx_postfx_ao != 2) + return; + ImGui::SliderFloat("ao radius (m)", &Global.gfx_postfx_ao_radius, 0.1f, 10.f, "%.2f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("ao strength", &Global.gfx_postfx_ao_strength, 0.f, 4.f); + ImGui::SliderFloat("ao bounce albedo", &Global.gfx_postfx_ao_bounce, 0.f, 1.f); + ImGui::SliderInt("ao slices", &Global.gfx_postfx_ao_slices, 1, 8); + ImGui::SliderInt("ao steps per side", &Global.gfx_postfx_ao_steps, 1, 16); +} diff --git a/rendering/opengl46/opengl46ao.h b/rendering/opengl46/opengl46ao.h new file mode 100644 index 00000000..d74ff27c --- /dev/null +++ b/rendering/opengl46/opengl46ao.h @@ -0,0 +1,55 @@ +/* +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 "gl/shader.h" +#include "gl/vao.h" +#include "gl/framebuffer.h" +#include "gl/query.h" + +class opengl_texture; + +// screen space ambient occlusion of the post processing of the opengl 4.6 renderer: +// - ground truth ambient occlusion (Jimenez et al. 2016) at half the resolution in two compute passes, the horizon search +// (gtao_main.comp) and a depth aware smoothing (gtao_denoise.comp) +// - its application to the image: a multiplier drawn over it (postfx_ssao_apply.frag), which also applies the older +// ssao (postfx_ssao.frag, run by the renderer) when that one is chosen +// switched by Global.gfx_postfx_ao (0 off, 1 older ssao, 2 gtao); the targets are made with the viewport when it is not 0 +class opengl46_ao +{ + public: + opengl46_ao() = default; + opengl46_ao(const opengl46_ao &) = delete; + opengl46_ao &operator=(const opengl46_ao &) = delete; + + // creates the programs. throws gl::shader_exception + void init(); + // ground truth ambient occlusion from the depth of the scene drawn with the given projection: Raw gets the result of + // the horizon search, Smooth the smoothed one. both half the size of Depth + void compute(opengl_texture &Depth, opengl_texture &Raw, opengl_texture &Smooth, glm::mat4 const &Projection); + // multiplies the image of the framebuffer by the occlusion. Mode: Global.gfx_postfx_ao. the viewport has to be set + // to the size of Depth. leaves the framebuffer bound + void apply(int const Mode, opengl_texture &Depth, opengl_texture &Occlusion, gl::framebuffer &Target, glm::mat4 const &Projection); + // lines for the gpu times of the debug panel + std::string info_times(); + // controls of the debug window. Available: the targets were made, so the mode can be changed + void draw_debug_ui(bool const Available); + + private: + std::unique_ptr m_main_program; + std::unique_ptr m_denoise_program; + std::unique_ptr m_apply_program; + std::unique_ptr m_vao; + gl::gpu_timer m_timer; + bool m_timing{false}; +}; diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 7c304e0a..8cbafb86 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -185,6 +185,7 @@ bool opengl46_renderer::Init(GLFWwindow *Window) { m_sky.init(); m_exposure.init(); + m_ao.init(); } catch (gl::shader_exception const &e) { @@ -220,7 +221,6 @@ bool opengl46_renderer::Init(GLFWwindow *Window) m_pfx_ssao = std::make_unique("ssao"); m_pfx_ssao_blur = std::make_unique("ssao_blur"); - m_pfx_ssao_apply = std::make_unique("ssao_apply"); // Generate hemisphere kernel (z > 0 = toward surface normal) std::uniform_real_distribution rnd(0.0f, 1.0f); @@ -494,7 +494,7 @@ bool opengl46_renderer::init_viewport(viewport_config &vp) vp.msaa_fb->attach(*vp.msaa_rbc, GL_COLOR_ATTACHMENT0); vp.msaa_fb->attach(*vp.msaa_rbd, GL_DEPTH_ATTACHMENT); - if (Global.gfx_postfx_chromaticaberration_enabled || Global.gfx_postfx_motionblur_enabled || Global.gfx_postfx_ssao_enabled) + if (Global.gfx_postfx_chromaticaberration_enabled || Global.gfx_postfx_motionblur_enabled || Global.gfx_postfx_ao != 0) { vp.main_tex = std::make_unique(); vp.main_tex->alloc_rendertarget(Global.gfx_format_color, GL_RGB, vp.width, vp.height, 1, 1, GL_CLAMP_TO_EDGE); @@ -539,7 +539,7 @@ bool opengl46_renderer::init_viewport(viewport_config &vp) vp.main2_fb = std::make_unique(); vp.main2_fb->attach(*vp.main2_tex, GL_COLOR_ATTACHMENT0); - if (Global.gfx_postfx_ssao_enabled) + if (Global.gfx_postfx_ao != 0) { int ssao_w = std::max(1, vp.width / 2); int ssao_h = std::max(1, vp.height / 2); @@ -555,6 +555,13 @@ bool opengl46_renderer::init_viewport(viewport_config &vp) vp.ssao_blur_fb = std::make_unique(); vp.ssao_blur_fb->attach(*vp.ssao_blur_tex, GL_COLOR_ATTACHMENT0); if (!vp.ssao_blur_fb->is_complete()) { ErrorLog("ssao blur framebuffer setup failed"); return false; } + + if (Global.gfx_postfx_motionblur_enabled) + { + vp.ao_apply_fb = std::make_unique(); + vp.ao_apply_fb->attach(*vp.main_tex, GL_COLOR_ATTACHMENT0); + if (!vp.ao_apply_fb->is_complete()) { ErrorLog("ambient occlusion framebuffer setup failed"); return false; } + } } if (!vp.main2_fb->is_complete()) @@ -693,6 +700,7 @@ void opengl46_renderer::SwapBuffers() m_debugtimestext += " cull: " + to_string(static_cast(*culltime) / 1000000.0, 3) + " ms\n"; m_debugtimestext += m_sky.info_times(); m_debugtimestext += m_exposure.info_times(); + m_debugtimestext += m_ao.info_times(); m_debugtimestext += "uilayer: " + to_string( Timer::subsystem.gfx_gui.average(), 2 ) + " ms\n"; if( DebugModeFlag ) m_debugtimestext += m_textures.info(); @@ -785,6 +793,8 @@ void opengl46_renderer::draw_debug_ui() m_sky.draw_debug_ui(); ImGui::Separator(); m_exposure.draw_debug_ui(); + ImGui::Separator(); + m_ao.draw_debug_ui(!m_viewports.empty() && m_viewports.front()->ssao_tex != nullptr); } ImGui::End(); @@ -1078,6 +1088,8 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) if (!Global.gfx_skippipeline) { + // the targets of the ambient occlusion are there only when it was on at the start + auto const aomode{(vp.ssao_tex != nullptr) ? Global.gfx_postfx_ao : 0}; if (Global.gfx_postfx_motionblur_enabled) { vp.main_fb->clear(GL_COLOR_BUFFER_BIT); @@ -1086,38 +1098,24 @@ if (!Global.gfx_skippipeline) vp.msaa_fb->blit_to(vp.main_fb.get(), vp.width, vp.height, GL_COLOR_BUFFER_BIT, GL_COLOR_ATTACHMENT1); vp.msaa_fb->blit_to(vp.main_fb.get(), vp.width, vp.height, GL_DEPTH_BUFFER_BIT, GL_DEPTH_ATTACHMENT); + // the occlusion goes into the image before the blur, which smears both alike + if (aomode != 0) + Render_ambient_occlusion(vp, *vp.ao_apply_fb, aomode); + model_ubs.param[0].x = m_framerate / (1.0 / Global.gfx_postfx_motionblur_shutter); model_ubo->update(model_ubs); m_pfx_motionblur->apply({vp.main_tex.get(), vp.main_texv.get(), vp.main_texd.get()}, vp.main2_fb.get()); } - else if (Global.gfx_postfx_ssao_enabled) - { - // resolve color+depth so SSAO can sample the depth texture - vp.main_fb->clear(GL_COLOR_BUFFER_BIT); - vp.main_fb->setup_drawing(1); - vp.msaa_fb->blit_to(vp.main_fb.get(), vp.width, vp.height, GL_COLOR_BUFFER_BIT, GL_COLOR_ATTACHMENT0); - vp.msaa_fb->blit_to(vp.main_fb.get(), vp.width, vp.height, GL_DEPTH_BUFFER_BIT, GL_DEPTH_ATTACHMENT); - } else { vp.main2_fb->clear(GL_COLOR_BUFFER_BIT); vp.msaa_fb->blit_to(vp.main2_fb.get(), vp.width, vp.height, GL_COLOR_BUFFER_BIT, GL_COLOR_ATTACHMENT0); - } - - // SSAO: depth -> occlusion -> blur -> modulate color into main2_fb - if (Global.gfx_postfx_ssao_enabled && !Global.gfx_postfx_motionblur_enabled) - { - int ssao_w = std::max(1, vp.width / 2); - int ssao_h = std::max(1, vp.height / 2); - - // (kernel is generated procedurally in the shader; no UBO upload needed) - glViewport(0, 0, ssao_w, ssao_h); - m_pfx_ssao ->apply({ vp.main_texd.get(), m_ssao_noise_tex.get() }, vp.ssao_fb.get()); - m_pfx_ssao_blur->apply({ vp.ssao_tex.get() }, vp.ssao_blur_fb.get()); - - glViewport(0, 0, vp.width, vp.height); - vp.main2_fb->clear(GL_COLOR_BUFFER_BIT); - m_pfx_ssao_apply->apply({ vp.main_tex.get(), vp.ssao_blur_tex.get() }, vp.main2_fb.get()); + if (aomode != 0) + { + // the depth for the occlusion to read, the image stays in main2 + vp.msaa_fb->blit_to(vp.main_fb.get(), vp.width, vp.height, GL_DEPTH_BUFFER_BIT, GL_DEPTH_ATTACHMENT); + Render_ambient_occlusion(vp, *vp.main2_fb, aomode); + } } // the exposure follows the main view; the other viewports use it too @@ -6200,6 +6198,26 @@ void opengl46_renderer::Cull_Lights(viewport_config &vp) m_cullstimer.end(); } +void opengl46_renderer::Render_ambient_occlusion(viewport_config &vp, gl::framebuffer &Target, int const Mode) +{ + glDebug("ambient occlusion"); + // the projection the depth was drawn with + auto const &projection{m_colorpass.pass_camera.projection()}; + if (Mode == 1) + { + // the older ssao, which reads the projection from the scene uniforms + glViewport(0, 0, vp.ssao_tex->get_width(), vp.ssao_tex->get_height()); + m_pfx_ssao->apply({vp.main_texd.get(), m_ssao_noise_tex.get()}, vp.ssao_fb.get()); + m_pfx_ssao_blur->apply({vp.ssao_tex.get()}, vp.ssao_blur_fb.get()); + } + else + { + m_ao.compute(*vp.main_texd, *vp.ssao_tex, *vp.ssao_blur_tex, projection); + } + glViewport(0, 0, vp.width, vp.height); + m_ao.apply(Mode, *vp.main_texd, *vp.ssao_blur_tex, Target, projection); +} + 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 748db688..dbb22cc6 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -34,6 +34,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/opengl46/opengl46terrain.h" #include "rendering/opengl46/opengl46sky.h" #include "rendering/opengl46/opengl46exposure.h" +#include "rendering/opengl46/opengl46ao.h" // bare-bones render controller, in lack of anything better yet class opengl46_renderer : public gfx_renderer { @@ -253,11 +254,13 @@ class opengl46_renderer : public gfx_renderer { std::unique_ptr main2_fb; std::unique_ptr main2_tex; - // ssao + // ambient occlusion, half size: raw and smoothed (gtao), or the ssao and its blur (older ssao) std::unique_ptr ssao_tex; std::unique_ptr ssao_blur_tex; std::unique_ptr ssao_fb; std::unique_ptr ssao_blur_fb; + // main_tex alone, for the ambient occlusion to be applied to it before the motion blur (main_fb holds the depth it reads) + std::unique_ptr ao_apply_fb; // LDR backbuffer for offscreen rendering std::unique_ptr backbuffer_fb; @@ -358,6 +361,8 @@ class opengl46_renderer : public gfx_renderer { // 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); + // ambient occlusion from the resolved depth (vp.main_texd), multiplied into the image of Target. Mode: Global.gfx_postfx_ao + void Render_ambient_occlusion(viewport_config &vp, gl::framebuffer &Target, int const Mode); glm::vec3 pick_color(std::size_t const Index); std::size_t pick_index(glm::ivec3 const &Color); @@ -391,6 +396,8 @@ class opengl46_renderer : public gfx_renderer { opengl46_sky m_sky; // automatic exposure and bloom of the post processing opengl46_exposure m_exposure; + // screen space ambient occlusion of the post processing + opengl46_ao m_ao; // true while the environment map bound for the shading is the real one (not the empty stand-in of the reflection pass) bool m_envmap_bound{false}; /* @@ -554,7 +561,6 @@ class opengl46_renderer : public gfx_renderer { // postfx ssao std::unique_ptr m_pfx_ssao; std::unique_ptr m_pfx_ssao_blur; - std::unique_ptr m_pfx_ssao_apply; std::unique_ptr m_ssao_noise_tex; glm::vec3 m_ssao_kernel[32]; diff --git a/shaders/gtao_denoise.comp b/shaders/gtao_denoise.comp new file mode 100644 index 00000000..f8a32089 --- /dev/null +++ b/shaders/gtao_denoise.comp @@ -0,0 +1,50 @@ +// smooths the ambient occlusion of gtao_main.comp: the average of the 4x4 pixels around each one, which takes out the +// repeating 4x4 pattern of the slices, with the pixels at a clearly different distance left out so the occlusion does +// not leak across the edges of objects. one invocation per pixel of the half size target + +layout(local_size_x = 8, local_size_y = 8) in; + +layout(binding = 0) uniform sampler2D depth_tex; +layout(binding = 1) uniform sampler2D ao_tex; +layout(binding = 0, r8) writeonly uniform image2D ao_target; + +layout(location = 0) uniform mat4 inv_projection; + +// relative difference of the distance at which a neighbour stops counting +const float DEPTH_TOLERANCE = 0.05; + +float view_distance(ivec2 texel) +{ + ivec2 full_size = textureSize(depth_tex, 0); + float depth = texelFetch(depth_tex, min(texel * 2, full_size - 1), 0).r; + if (depth <= 0.0) + return 1e30; + vec2 uv = (vec2(texel * 2) + 0.5) / vec2(full_size); + vec4 position = inv_projection * vec4(uv * 2.0 - 1.0, depth, 1.0); + return length(position.xyz / position.w); +} + +void main() +{ + ivec2 texel = ivec2(gl_GlobalInvocationID.xy); + ivec2 size = imageSize(ao_target); + if (texel.x >= size.x || texel.y >= size.y) + return; + + float distance = view_distance(texel); + float sum = 0.0; + float weights = 0.0; + for (int y = -2; y < 2; ++y) + { + for (int x = -2; x < 2; ++x) + { + ivec2 sampletexel = clamp(texel + ivec2(x, y), ivec2(0), size - 1); + float sampledistance = view_distance(sampletexel); + float weight = clamp(1.0 - abs(sampledistance - distance) / (DEPTH_TOLERANCE * distance), 0.0, 1.0); + sum += texelFetch(ao_tex, sampletexel, 0).r * weight; + weights += weight; + } + } + // the pixel itself is in the window with the full weight, so weights >= 1 + imageStore(ao_target, texel, vec4(sum / weights)); +} diff --git a/shaders/gtao_main.comp b/shaders/gtao_main.comp new file mode 100644 index 00000000..a925dcd3 --- /dev/null +++ b/shaders/gtao_main.comp @@ -0,0 +1,152 @@ +// ground truth ambient occlusion (Jimenez et al. 2016, "Practical realtime strategies for accurate indirect occlusion"; +// integration as in Intel XeGTAO) at half the resolution of the image. for every pixel, a few slices through the +// hemisphere around the view direction; in each the horizons on both sides are searched in the depth buffer and the +// cosine weighted visible arc between them is integrated against the normal rebuilt from the depth. +// one invocation per pixel of the half size target. the pattern of the slice directions and steps repeats every 4x4 +// pixels; gtao_denoise.comp averages it out + +layout(local_size_x = 8, local_size_y = 8) in; + +// depth of the scene at full resolution: reversed, 1 at the near plane, 0 at infinity (the sky) +layout(binding = 0) uniform sampler2D depth_tex; +layout(binding = 0, r8) writeonly uniform image2D ao_target; + +layout(location = 0) uniform mat4 inv_projection; +// x: radius in metres, y: projection[0][0] times half the width of the target in pixels +layout(location = 1) uniform vec2 params; +// the row of the projection which gives the clip w of a view space position +layout(location = 2) uniform vec4 clipw_row; +// x: number of slices, y: steps on each side of a slice +layout(location = 3) uniform ivec2 quality; + +const float PI = 3.14159265; +const float HALF_PI = 1.57079633; +// share of the radius over which the weight of an occluder falls to nothing +const float FALLOFF_RANGE = 0.615; + +// depth of the pixel of the half size target: the top left one of the four full size pixels it covers, so the +// application (postfx_ssao_apply.frag) finds the same depth for it +float half_depth(ivec2 texel) +{ + ivec2 full_size = textureSize(depth_tex, 0); + return texelFetch(depth_tex, min(texel * 2, full_size - 1), 0).r; +} + +vec3 view_position(ivec2 texel, float depth) +{ + ivec2 full_size = textureSize(depth_tex, 0); + vec2 uv = (vec2(texel * 2) + 0.5) / vec2(full_size); + vec4 position = inv_projection * vec4(uv * 2.0 - 1.0, depth, 1.0); + return position.xyz / position.w; +} + +// 4x4 ordered pattern, values 0..15 +const float BAYER[16] = float[](0.0, 8.0, 2.0, 10.0, 12.0, 4.0, 14.0, 6.0, 3.0, 11.0, 1.0, 9.0, 15.0, 7.0, 13.0, 5.0); + +void main() +{ + ivec2 texel = ivec2(gl_GlobalInvocationID.xy); + ivec2 size = imageSize(ao_target); + if (texel.x >= size.x || texel.y >= size.y) + return; + + float depth = half_depth(texel); + if (depth <= 0.0) + { + imageStore(ao_target, texel, vec4(1.0)); + return; + } + vec3 position = view_position(texel, depth); + vec3 view = normalize(-position); + + // normal from the neighbours: on each axis the one closer in depth, so edges of objects do not bend it + vec3 left = view_position(texel - ivec2(1, 0), half_depth(texel - ivec2(1, 0))); + vec3 right = view_position(texel + ivec2(1, 0), half_depth(texel + ivec2(1, 0))); + vec3 down = view_position(texel - ivec2(0, 1), half_depth(texel - ivec2(0, 1))); + vec3 up = view_position(texel + ivec2(0, 1), half_depth(texel + ivec2(0, 1))); + vec3 dx = (abs(right.z - position.z) < abs(position.z - left.z)) ? right - position : position - left; + vec3 dy = (abs(up.z - position.z) < abs(position.z - down.z)) ? up - position : position - down; + vec3 normal = normalize(cross(dx, dy)); + if (dot(normal, view) < 0.0) + normal = -normal; + + float radius = params.x; + float clipw = dot(clipw_row, vec4(position, 1.0)); + float screenradius = params.y * radius / max(clipw, 1e-6); + // smaller than a pixel: nothing to find + if (screenradius < 1.0) + { + imageStore(ao_target, texel, vec4(1.0)); + return; + } + screenradius = min(screenradius, 256.0); + + float falloffrange = FALLOFF_RANGE * radius; + float falloffmul = -1.0 / falloffrange; + float falloffadd = (radius - falloffrange) / falloffrange + 1.0; + + int pattern = (texel.x & 3) + 4 * (texel.y & 3); + float slicenoise = (BAYER[pattern] + 0.5) / 16.0; + float stepnoise = fract(float(pattern) * 0.618034 + 0.25); + + int slices = max(quality.x, 1); + int steps = max(quality.y, 1); + float visibility = 0.0; + for (int slice = 0; slice < slices; ++slice) + { + float angle = (float(slice) + slicenoise) * PI / float(slices); + vec2 omega = vec2(cos(angle), sin(angle)); + vec3 direction = vec3(omega, 0.0); + vec3 orthodirection = direction - dot(direction, view) * view; + vec3 axis = normalize(cross(orthodirection, view)); + vec3 projectednormal = normal - axis * dot(normal, axis); + float projectedlength = length(projectednormal); + if (projectedlength < 1e-4) + continue; + float signnormal = sign(dot(orthodirection, projectednormal)); + float cosnormal = clamp(dot(projectednormal, view) / projectedlength, 0.0, 1.0); + float n = signnormal * acos(cosnormal); + + float lowcos0 = cos(n + HALF_PI); + float lowcos1 = cos(n - HALF_PI); + float horizoncos0 = lowcos0; + float horizoncos1 = lowcos1; + + for (int step = 0; step < steps; ++step) + { + // denser near the pixel, where the occluders matter most + float s = (float(step) + stepnoise) / float(steps); + s = s * s; + vec2 offset = omega * max(s * screenradius, float(step) + 1.0); + for (int side = 0; side < 2; ++side) + { + ivec2 sampletexel = texel + ivec2(round(side == 0 ? offset : -offset)); + if (any(lessThan(sampletexel, ivec2(0))) || any(greaterThanEqual(sampletexel, size))) + continue; + float sampledepth = half_depth(sampletexel); + if (sampledepth <= 0.0) + continue; + vec3 delta = view_position(sampletexel, sampledepth) - position; + float distance = length(delta); + if (distance < 1e-5) + continue; + float horizoncos = dot(delta / distance, view); + float weight = clamp(distance * falloffmul + falloffadd, 0.0, 1.0); + if (side == 0) + horizoncos0 = max(horizoncos0, mix(lowcos0, horizoncos, weight)); + else + horizoncos1 = max(horizoncos1, mix(lowcos1, horizoncos, weight)); + } + } + + float h0 = -acos(clamp(horizoncos1, -1.0, 1.0)); + float h1 = acos(clamp(horizoncos0, -1.0, 1.0)); + h0 = n + clamp(h0 - n, -HALF_PI, HALF_PI); + h1 = n + clamp(h1 - n, -HALF_PI, HALF_PI); + float arc0 = (cosnormal + 2.0 * h0 * sin(n) - cos(2.0 * h0 - n)) * 0.25; + float arc1 = (cosnormal + 2.0 * h1 * sin(n) - cos(2.0 * h1 - n)) * 0.25; + visibility += projectedlength * (arc0 + arc1); + } + visibility = clamp(visibility / float(slices), 0.0, 1.0); + imageStore(ao_target, texel, vec4(visibility)); +} diff --git a/shaders/postfx_ssao.frag b/shaders/postfx_ssao.frag index 05ff9692..35a34841 100644 --- a/shaders/postfx_ssao.frag +++ b/shaders/postfx_ssao.frag @@ -9,8 +9,9 @@ uniform sampler2D noise_tex; #include +// the depth is reversed (glClipControl GL_ZERO_TO_ONE, 1 at the near plane, 0 at infinity), so it is the clip z itself vec3 view_pos_from_depth(vec2 uv, float depth) { - vec4 clip = vec4(uv * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0); + vec4 clip = vec4(uv * 2.0 - 1.0, depth, 1.0); vec4 view = inverse(projection) * clip; return view.xyz / view.w; } @@ -31,7 +32,7 @@ vec3 kernel_sample(int i) { void main() { float d = texture(depth_tex, f_coords).r; - if (d >= 1.0) { out_color = vec4(1.0); return; } // skybox -> no occlusion + if (d <= 0.0) { out_color = vec4(1.0); return; } // skybox -> no occlusion vec3 pos = view_pos_from_depth(f_coords, d); diff --git a/shaders/postfx_ssao_apply.frag b/shaders/postfx_ssao_apply.frag index 177c5b29..795c1db7 100644 --- a/shaders/postfx_ssao_apply.frag +++ b/shaders/postfx_ssao_apply.frag @@ -1,22 +1,80 @@ +// applies the ambient occlusion of the half size target to the image at full size, as a multiplier: drawn with the +// blending dst * src over the image (opengl46_ao::apply). the four nearest pixels of the occlusion are weighed by how +// close they are and by how close their distance is to that of the pixel, so the occlusion does not spill over the +// edges of objects. each pixel of the occlusion stands for the top left one of the four full size pixels it covers +// params.x: 0 ground truth ambient occlusion, 1 the older ssao (its own fixed curve) +// params.y: strength, a power of the occlusion (1 = as computed); params.z: albedo assumed by the multiple bounce term + in vec2 f_coords; layout(location = 0) out vec4 out_color; -#texture (color_tex, 0, RGB) -uniform sampler2D color_tex; +layout(binding = 0) uniform sampler2D depth_tex; +layout(binding = 1) uniform sampler2D ao_tex; -#texture (ssao_tex, 1, R) -uniform sampler2D ssao_tex; +layout(location = 0) uniform mat4 inv_projection; +layout(location = 1) uniform vec4 params; -const float SSAO_STRENGTH = 0.8; -const float SSAO_MIN = 0.3; // floor on darkening so shadows don't go pitch black +float view_distance(ivec2 pixel, float depth) +{ + ivec2 full_size = textureSize(depth_tex, 0); + vec2 uv = (vec2(pixel) + 0.5) / vec2(full_size); + vec4 position = inv_projection * vec4(uv * 2.0 - 1.0, depth, 1.0); + return length(position.xyz / position.w); +} -void main() { - vec3 c = texture(color_tex, f_coords).rgb; - float occ = texture(ssao_tex, f_coords).r; +// light which comes back after more than one bounce off surfaces of the given albedo, fit of Jimenez et al. 2016 +float multi_bounce(float visibility, float albedo) +{ + float a = 2.0404 * albedo - 0.3324; + float b = -4.7951 * albedo + 0.6417; + float c = 2.7552 * albedo + 0.6903; + return max(visibility, ((visibility * a + b) * visibility + c) * visibility); +} - // remap occlusion with strength and a minimum floor - occ = pow(clamp(occ, 0.0, 1.0), SSAO_STRENGTH); - occ = mix(SSAO_MIN, 1.0, occ); +void main() +{ + ivec2 full_size = textureSize(depth_tex, 0); + ivec2 half_size = textureSize(ao_tex, 0); + ivec2 pixel = ivec2(gl_FragCoord.xy); + float depth = texelFetch(depth_tex, pixel, 0).r; + if (depth <= 0.0) + { + // the sky + out_color = vec4(1.0); + return; + } + float distance = view_distance(pixel, depth); - out_color = vec4(c * occ, 1.0); -} \ No newline at end of file + vec2 position = vec2(pixel) * 0.5; + ivec2 base = ivec2(floor(position)); + vec2 fraction = position - vec2(base); + float sum = 0.0; + float weights = 0.0; + for (int y = 0; y < 2; ++y) + { + for (int x = 0; x < 2; ++x) + { + ivec2 texel = clamp(base + ivec2(x, y), ivec2(0), half_size - 1); + ivec2 texelpixel = min(texel * 2, full_size - 1); + float texeldepth = texelFetch(depth_tex, texelpixel, 0).r; + float texeldistance = (texeldepth > 0.0) ? view_distance(texelpixel, texeldepth) : 1e30; + float bilinear = (x == 0 ? 1.0 - fraction.x : fraction.x) * (y == 0 ? 1.0 - fraction.y : fraction.y); + float weight = (bilinear + 1e-3) / (1e-3 + abs(texeldistance - distance) / distance); + sum += texelFetch(ao_tex, texel, 0).r * weight; + weights += weight; + } + } + float ao = clamp(sum / max(weights, 1e-6), 0.0, 1.0); + + if (params.x > 0.5) + { + // the curve the older ssao was applied with, a floor so the shadows do not go black + ao = mix(0.3, 1.0, pow(ao, 0.8)); + } + else + { + ao = pow(ao, params.y); + ao = multi_bounce(ao, params.z); + } + out_color = vec4(vec3(ao), 1.0); +} diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index c5e3cd21..863e111d 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1578,6 +1578,21 @@ global_settings::ConfigParse_gfx_sky( cParser &Parser, std::string_view const To else if( Token == "gfx.postfx.bloom" ) { readswitch( gfx_postfx_bloom ); } else if( Token == "gfx.postfx.bloom.intensity" ) { readvalue( gfx_postfx_bloom_intensity, 0.f, 1.f ); } else if( Token == "gfx.postfx.bloom.threshold" ) { readvalue( gfx_postfx_bloom_threshold, 0.f, 1000.f ); } + else if( Token == "gfx.postfx.ao" ) { + // yes/gtao: ground truth ambient occlusion, ssao: the older one, anything else: off + Parser.getTokens( 1 ); + std::string mode; + Parser >> mode; + gfx_postfx_ao = ( mode == "yes" || mode == "true" || mode == "1" || mode == "gtao" ) ? 2 : ( mode == "ssao" ) ? 1 : 0; } + else if( Token == "gfx.postfx.ao.radius" ) { readvalue( gfx_postfx_ao_radius, 0.1f, 10.f ); } + else if( Token == "gfx.postfx.ao.strength" ) { readvalue( gfx_postfx_ao_strength, 0.f, 4.f ); } + else if( Token == "gfx.postfx.ao.bounce" ) { readvalue( gfx_postfx_ao_bounce, 0.f, 1.f ); } + else if( Token == "gfx.postfx.ao.quality" ) { + // slices and steps per side + Parser.getTokens( 2, false ); + Parser >> gfx_postfx_ao_slices >> gfx_postfx_ao_steps; + gfx_postfx_ao_slices = std::clamp( gfx_postfx_ao_slices, 1, 8 ); + gfx_postfx_ao_steps = std::clamp( gfx_postfx_ao_steps, 1, 16 ); } else { return false; } return true; @@ -1839,6 +1854,11 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.postfx.bloom", gfx_postfx_bloom ); export_as_text( Output, "gfx.postfx.bloom.intensity", gfx_postfx_bloom_intensity ); export_as_text( Output, "gfx.postfx.bloom.threshold", gfx_postfx_bloom_threshold ); + export_as_text( Output, "gfx.postfx.ao", std::string{ gfx_postfx_ao == 2 ? "gtao" : gfx_postfx_ao == 1 ? "ssao" : "no" } ); + export_as_text( Output, "gfx.postfx.ao.radius", gfx_postfx_ao_radius ); + export_as_text( Output, "gfx.postfx.ao.strength", gfx_postfx_ao_strength ); + export_as_text( Output, "gfx.postfx.ao.bounce", gfx_postfx_ao_bounce ); + Output << "gfx.postfx.ao.quality " << gfx_postfx_ao_slices << " " << gfx_postfx_ao_steps << "\n"; export_as_text( Output, "python.enabled", python_enabled ); export_as_text( Output, "python.threadedupload", python_threadedupload ); export_as_text( Output, "python.uploadmain", python_uploadmain ); diff --git a/utilities/Globals.h b/utilities/Globals.h index 05b98121..c935b105 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -324,6 +324,14 @@ struct global_settings { bool gfx_postfx_bloom { true }; // glow around bright parts of the image float gfx_postfx_bloom_intensity { 0.04f }; // share of the blurred image in the result float gfx_postfx_bloom_threshold { 0.f }; // luminance below which nothing glows, 0 = everything contributes + // screen space ambient occlusion of the opengl 4.6 renderer, applied to the image (debug window "Sky and exposure"). + // 0 off, 1 the older ssao, 2 ground truth ambient occlusion; when 0 at the start the targets are not made at all + int gfx_postfx_ao { 2 }; + float gfx_postfx_ao_radius { 1.5f }; // reach of the occluders, metres + float gfx_postfx_ao_strength { 1.f }; // power the occlusion is raised to, 1 = as computed, 0 = none + float gfx_postfx_ao_bounce { 0.3f }; // albedo of the surfaces assumed for the light coming back after more bounces + int gfx_postfx_ao_slices { 3 }; // directions searched around each pixel + int gfx_postfx_ao_steps { 4 }; // samples on each side of a direction float ui_fontsize = 13.0f; float ui_scale = 1.0f; @@ -333,7 +341,6 @@ struct global_settings { std::string exec_on_exit; std::string prepend_scn; - bool gfx_postfx_ssao_enabled {false}; struct extraviewport_config { std::string monitor; @@ -398,7 +405,7 @@ struct global_settings { bool ConfigParseHardware(cParser& Parser, const std::string& token); bool ConfigParseDebug(cParser& Parser, const std::string& token); bool ConfigParse_gfx( cParser &parser, std::string_view const Token ); - // sky light and exposure switches of the opengl 4.6 renderer (gfx.ambient.*, gfx.envmap.ggx, gfx.atmosphere*, gfx.postfx.autoexposure*, gfx.postfx.bloom*) + // sky light and exposure switches of the opengl 4.6 renderer (gfx.ambient.*, gfx.envmap.ggx, gfx.atmosphere*, gfx.postfx.autoexposure*, gfx.postfx.bloom*, gfx.postfx.ao*) bool ConfigParse_gfx_sky( cParser &parser, std::string_view const Token ); // sends basic content of the class in legacy (text) format to provided stream void