17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 22:39:19 +02:00

Fix the screen space ambient occlusion and add ground truth ambient occlusion to the GL 4.6 renderer

This commit is contained in:
2026-10-15 23:03:50 +02:00
parent ce725c3d62
commit 5f365a00c5
13 changed files with 564 additions and 50 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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);

View File

@@ -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<gl::program>(std::vector<std::reference_wrapper<const gl::shader>>{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<gl::program>(std::vector<std::reference_wrapper<const gl::shader>>{vertex, fragment});
}
m_vao = std::make_unique<gl::vao>();
}
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<GLuint>((Raw.get_width() + GROUP_SIZE - 1) / GROUP_SIZE)};
auto const groups_y{static_cast<GLuint>((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<float>(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<double>(*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);
}

View File

@@ -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 <memory>
#include <string>
#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<gl::program> m_main_program;
std::unique_ptr<gl::program> m_denoise_program;
std::unique_ptr<gl::program> m_apply_program;
std::unique_ptr<gl::vao> m_vao;
gl::gpu_timer m_timer;
bool m_timing{false};
};

View File

@@ -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<gl::postfx>("ssao");
m_pfx_ssao_blur = std::make_unique<gl::postfx>("ssao_blur");
m_pfx_ssao_apply = std::make_unique<gl::postfx>("ssao_apply");
// Generate hemisphere kernel (z > 0 = toward surface normal)
std::uniform_real_distribution<float> 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<opengl_texture>();
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<gl::framebuffer>();
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<gl::framebuffer>();
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<gl::framebuffer>();
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<double>(*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");

View File

@@ -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<gl::framebuffer> main2_fb;
std::unique_ptr<opengl_texture> main2_tex;
// ssao
// ambient occlusion, half size: raw and smoothed (gtao), or the ssao and its blur (older ssao)
std::unique_ptr<opengl_texture> ssao_tex;
std::unique_ptr<opengl_texture> ssao_blur_tex;
std::unique_ptr<gl::framebuffer> ssao_fb;
std::unique_ptr<gl::framebuffer> 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<gl::framebuffer> ao_apply_fb;
// LDR backbuffer for offscreen rendering
std::unique_ptr<gl::framebuffer> 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<gl::postfx> m_pfx_ssao;
std::unique_ptr<gl::postfx> m_pfx_ssao_blur;
std::unique_ptr<gl::postfx> m_pfx_ssao_apply;
std::unique_ptr<opengl_texture> m_ssao_noise_tex;
glm::vec3 m_ssao_kernel[32];

50
shaders/gtao_denoise.comp Normal file
View File

@@ -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));
}

152
shaders/gtao_main.comp Normal file
View File

@@ -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));
}

View File

@@ -9,8 +9,9 @@ uniform sampler2D noise_tex;
#include <common>
// 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);

View File

@@ -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);
}
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);
}

View File

@@ -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 );

View File

@@ -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