From e8dbaf70a95f7b5e80e4f00112851668d496757e Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sat, 17 Oct 2026 00:11:27 +0200 Subject: [PATCH] Sample the fog cells in their middle across the view and dither the output --- gl/shader.cpp | 2 ++ rendering/opengl46/opengl46renderer.cpp | 2 ++ rendering/opengl46/opengl46weather.cpp | 22 ++++++++-------------- shaders/dither.glsl | 21 +++++++++++++++++++++ shaders/fog_inject.comp | 14 +++++++------- shaders/postfx_chromaticaberration.frag | 3 +++ shaders/postfx_tonemapping.frag | 5 ++++- 7 files changed, 47 insertions(+), 22 deletions(-) create mode 100644 shaders/dither.glsl diff --git a/gl/shader.cpp b/gl/shader.cpp index c025a233..2fd20f9e 100644 --- a/gl/shader.cpp +++ b/gl/shader.cpp @@ -99,6 +99,8 @@ std::pair gl::shader::process_source(const std::string &fil else if (type == GL_GEOMETRY_SHADER) str += "#define STAGE_GEOMETRY 1\n"; else if (type == GL_COMPUTE_SHADER) str += "#define STAGE_COMPUTE 1\n"; str += "vec4 FBOUT(vec4 x) { return " + (Global.gfx_shadergamma ? std::string("vec4(pow(x.rgb, vec3(1.0 / 2.2)), x.a)") : std::string("x")) + "; }\n"; + // whether the colour written to the window is encoded by the shader (FBOUT) or left to the sRGB framebuffer (dither.glsl) + str += std::string("#define OUTPUT_GAMMA ") + (Global.gfx_shadergamma ? "1" : "0") + "\n"; str += read_file(basedir + filename); diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 7de98831..e5ded79a 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -1228,6 +1228,8 @@ if (!Global.gfx_skippipeline) bloomlevels = vp.bloom->sizes.size(); } model_ubs.param[0] = m_exposure.tonemap_params(bloomlevels); + // the dither goes to the last pass before the window: the chromatic aberration, when it follows + model_ubs.param[1] = glm::vec4{Global.gfx_postfx_chromaticaberration_enabled ? 0.f : 1.f, 0.f, 0.f, 0.f}; model_ubo->update(model_ubs); std::vector tonemapinput{vp.main2_tex.get()}; if (bloomtexture != nullptr) diff --git a/rendering/opengl46/opengl46weather.cpp b/rendering/opengl46/opengl46weather.cpp index a6312aa7..bc522e75 100644 --- a/rendering/opengl46/opengl46weather.cpp +++ b/rendering/opengl46/opengl46weather.cpp @@ -519,21 +519,16 @@ void opengl46_weather::render_view(glm::mat4 const &Projection, glm::mat4 const { m_fog_timer.begin(); auto const farthest{std::max(Global.gfx_fog_range, FOG_VOLUME_NEAR * 4.f)}; - // the cells are sampled at another point in every frame: along the depth in the Halton sequence of 2, across the view in - // those of 3 and 5 (fog_inject.comp moves the point of every cell by a value of its own) - auto const halton = [](int Index, int const Base) { - float value{0.f}, fraction{1.f}; - for (; Index > 0; Index /= Base) - { - fraction /= static_cast(Base); - value += fraction * static_cast(Index % Base); - } + // the cells are sampled at another depth in every frame, in the Halton sequence of 2 (fog_inject.comp moves the depth of + // every cell by a value of its own) + auto const jitter{[](int Frame) { + float value{0.f}, fraction{0.5f}; + for (Frame = (Frame % 64) + 1; Frame > 0; Frame /= 2, fraction *= 0.5f) + value += fraction * static_cast(Frame % 2); return value; - }; + }(m_frame)}; auto const temporal{Global.gfx_fog_temporal && continuity(m_previous_fog)}; - auto const sample{(m_frame % 64) + 1}; - glm::vec3 const jitter{temporal ? glm::vec3{halton(sample, 3), halton(sample, 5), halton(sample, 2)} : glm::vec3{0.5f}}; - glm::vec4 const slicing{FOG_VOLUME_NEAR, farthest, static_cast(m_fog_grid.z) / std::log((farthest + gl::FOG_VOLUME_BIAS) / (FOG_VOLUME_NEAR + gl::FOG_VOLUME_BIAS)), jitter.z}; + glm::vec4 const slicing{FOG_VOLUME_NEAR, farthest, static_cast(m_fog_grid.z) / std::log((farthest + gl::FOG_VOLUME_BIAS) / (FOG_VOLUME_NEAR + gl::FOG_VOLUME_BIAS)), temporal ? jitter : 0.5f}; bind_noise(); auto const next{1 - m_fog_current}; glBindImageTexture(0, m_fog_scattering[next], 0, GL_TRUE, 0, GL_WRITE_ONLY, GL_RGBA16F); @@ -550,7 +545,6 @@ void opengl46_weather::render_view(glm::mat4 const &Projection, glm::mat4 const glProgramUniform4f(*m_inject_program, 6, share, Global.gfx_fog_lights, sunshadows ? 1.f : 0.f, Global.gfx_fog_shadows ? 1.f : 0.f); glProgramUniform1i(*m_inject_program, 7, Cablamp); glProgramUniform2f(*m_inject_program, 8, m_previous_fog.slicing.x, m_previous_fog.slicing.z); - glProgramUniform2f(*m_inject_program, 9, jitter.x, jitter.y); m_inject_program->dispatch(groups(m_fog_grid.x), groups(m_fog_grid.y), static_cast(m_fog_grid.z)); glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); diff --git a/shaders/dither.glsl b/shaders/dither.glsl new file mode 100644 index 00000000..5b26e8eb --- /dev/null +++ b/shaders/dither.glsl @@ -0,0 +1,21 @@ +// noise of half a step of the 8-bit output, added to the colour about to be written to the window: smooth dark gradients (the +// fog at night) show no bands, which otherwise crawl over the image with the smallest change of the light. interleaved gradient +// noise (Jimenez 2014) of the pixel, the same in every frame. the colour is the one written to the window: encoded already with +// OUTPUT_GAMMA, linear otherwise (the sRGB framebuffer encodes it, so the step is taken in the encoded values) +#ifndef DITHER_GLSL +#define DITHER_GLSL + +vec3 dither_output(vec3 color) +{ + float noise = (fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))) - 0.5) / 255.0; +#if OUTPUT_GAMMA + return max(color + noise, vec3(0.0)); +#else + vec3 linear = max(color, vec3(0.0)); + vec3 encoded = mix(linear * 12.92, 1.055 * pow(linear, vec3(1.0 / 2.4)) - 0.055, step(vec3(0.0031308), linear)); + encoded = max(encoded + noise, vec3(0.0)); + return mix(encoded / 12.92, pow((encoded + 0.055) / 1.055, vec3(2.4)), step(vec3(0.04045), encoded)); +#endif +} + +#endif diff --git a/shaders/fog_inject.comp b/shaders/fog_inject.comp index 68044b01..de3277e6 100644 --- a/shaders/fog_inject.comp +++ b/shaders/fog_inject.comp @@ -2,8 +2,10 @@ // depth), how dense the fog is and how much light it scatters towards the camera. the density: the haze, the patches of the // ground fog, the low clouds; the light: the colour the fog has towards the horizon (weather_common.glsl), less what the // shadows of the sun and of the clouds hold back, and the lamps and headlights of the light clusters. blended with the -// previous frame, found where the cell was then; the cells are sampled at another point every frame, each cell going through -// the points from a place of its own, so the volume doesn't flicker as a whole and the blending smooths the edges of the cells. +// previous frame, found where the cell was then; the cells are sampled at another depth every frame, each cell going through +// the depths from a place of its own, so the volume doesn't flicker as a whole. across the view the cells are sampled in their +// middle: moved across, the ray of a cell crosses the edges of the cones of lamps somewhere else every frame, which the blending +// doesn't smooth out (the edges of the cells are smoothed when the fog is read, fog_volume.glsl) // rgb: light scattered towards the camera per metre, a: extinction per metre #include @@ -36,8 +38,6 @@ layout(location = 6) uniform vec4 options; layout(location = 7) uniform int cablamp; // x: near, y: slices / log((far + FOG_VOLUME_BIAS) / (near + FOG_VOLUME_BIAS)) of the previous frame layout(location = 8) uniform vec2 previous_slicing; -// where in its cell the cell is sampled this frame across the view (0-1), as slicing.w is along the depth -layout(location = 9) uniform vec2 jitter; // how many times brighter or darker than this frame the light of the history may be const float FOG_HISTORY_REACH = 4.0; @@ -56,8 +56,8 @@ float slice_depth(float slice) return (slicing.x + FOG_VOLUME_BIAS) * exp(slice / slicing.z) - FOG_VOLUME_BIAS; } -// where in the cell it is sampled this frame, 0-1 on each axis: the point of the sequence of the frames moved by a value of -// the cell's own (pcg3d hash), the middle without the blending +// where in the cell it is sampled this frame, 0-1 on each axis: across the view the middle, along the depth the point of the +// sequence of the frames moved by a value of the cell's own (pcg3d hash), the middle without the blending vec3 cell_offset(ivec3 cell) { if (options.x >= 1.0) @@ -70,7 +70,7 @@ vec3 cell_offset(ivec3 cell) v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y; - return fract(vec3(jitter, slicing.w) + vec3(v >> 8U) * (1.0 / 16777216.0)); + return vec3(0.5, 0.5, fract(slicing.w + float(v.z >> 8U) * (1.0 / 16777216.0))); } float henyey_greenstein(float cosangle, float g) diff --git a/shaders/postfx_chromaticaberration.frag b/shaders/postfx_chromaticaberration.frag index 26a36045..09fa7b00 100644 --- a/shaders/postfx_chromaticaberration.frag +++ b/shaders/postfx_chromaticaberration.frag @@ -6,6 +6,8 @@ layout(location = 0) out vec4 out_color; uniform sampler2D iChannel0; +#include + vec2 barrelDistortion(vec2 f_coords, float amt) { vec2 cc = f_coords - 0.5; float dist = dot(cc, cc); @@ -54,4 +56,5 @@ void main() } out_color = sumcol / sumw; + out_color.rgb = dither_output(out_color.rgb); } diff --git a/shaders/postfx_tonemapping.frag b/shaders/postfx_tonemapping.frag index c9e076f4..90134147 100644 --- a/shaders/postfx_tonemapping.frag +++ b/shaders/postfx_tonemapping.frag @@ -22,10 +22,11 @@ layout(std430, binding = EXPOSURE_SSBO_BINDING) readonly buffer exposure_ssbo }; #include +#include // param[0].x: share of the bloom in the image (0: no bloom, the texture is not read), param[0].y: 1 if the automatic // exposure is applied, param[0].z: fixed exposure multiplier, param[0].w: scale of the bloom texture (it holds the sum of -// the levels of the chain) +// the levels of the chain), param[1].x: 1 when this is the last pass before the window, which takes the dither void main() { vec2 texcoord = f_coords; @@ -36,4 +37,6 @@ void main() if (param[0].y > 0.5 && exposure > 0.0) exposurescale *= exposure; out_color = tonemap(vec4(hdr_color * exposurescale, 1.0)); + if (param[1].x > 0.5) + out_color.rgb = dither_output(out_color.rgb); }