From ce725c3d621d9132bdd12593676d8d6865ebccf6 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 15 Oct 2026 22:16:41 +0200 Subject: [PATCH] Add sky light, physical atmosphere, auto exposure and bloom to the GL 4.6 renderer --- CMakeLists.txt | 5 +- docs/opengl46-renderer.md | 36 ++ environment/skydome.h | 2 + gl/cubemap.cpp | 13 + gl/cubemap.h | 3 + gl/glsl_common.cpp | 30 ++ gl/readback.cpp | 51 +++ gl/readback.h | 40 ++ gl/ubo.h | 112 +++++- rendering/opengl46/opengl46exposure.cpp | 175 ++++++++ rendering/opengl46/opengl46exposure.h | 78 ++++ rendering/opengl46/opengl46renderer.cpp | 88 ++++- rendering/opengl46/opengl46renderer.h | 13 + rendering/opengl46/opengl46sky.cpp | 417 ++++++++++++++++++++ rendering/opengl46/opengl46sky.h | 128 ++++++ rendering/opengl46/opengl46skydome.cpp | 11 +- rendering/opengl46/opengl46skydome.h | 6 +- shaders/atmosphere_ambient.comp | 127 ++++++ shaders/atmosphere_common.glsl | 194 +++++++++ shaders/atmosphere_integrate.glsl | 105 +++++ shaders/atmosphere_multiscattering.comp | 69 ++++ shaders/atmosphere_skyview.comp | 39 ++ shaders/atmosphere_transmittance.comp | 40 ++ shaders/color.frag | 17 +- shaders/envmap_prefilter.comp | 100 +++++ shaders/envmapping.glsl | 24 ++ shaders/exposure_average.comp | 79 ++++ shaders/exposure_histogram.comp | 49 +++ shaders/light_common.glsl | 30 +- shaders/mat_sunlessnormalmap_specgloss.frag | 3 +- shaders/postfx_bloom_down.frag | 77 ++++ shaders/postfx_bloom_up.frag | 29 ++ shaders/postfx_tonemapping.frag | 28 +- shaders/sky_atmosphere.frag | 54 +++ shaders/sky_shoulder.glsl | 16 + utilities/Globals.cpp | 61 +++ utilities/Globals.h | 25 ++ 37 files changed, 2342 insertions(+), 32 deletions(-) create mode 100644 gl/readback.cpp create mode 100644 gl/readback.h create mode 100644 rendering/opengl46/opengl46exposure.cpp create mode 100644 rendering/opengl46/opengl46exposure.h create mode 100644 rendering/opengl46/opengl46sky.cpp create mode 100644 rendering/opengl46/opengl46sky.h create mode 100644 shaders/atmosphere_ambient.comp create mode 100644 shaders/atmosphere_common.glsl create mode 100644 shaders/atmosphere_integrate.glsl create mode 100644 shaders/atmosphere_multiscattering.comp create mode 100644 shaders/atmosphere_skyview.comp create mode 100644 shaders/atmosphere_transmittance.comp create mode 100644 shaders/envmap_prefilter.comp create mode 100644 shaders/exposure_average.comp create mode 100644 shaders/exposure_histogram.comp create mode 100644 shaders/postfx_bloom_down.frag create mode 100644 shaders/postfx_bloom_up.frag create mode 100644 shaders/sky_atmosphere.frag create mode 100644 shaders/sky_shoulder.glsl diff --git a/CMakeLists.txt b/CMakeLists.txt index 258c7929..8785eeea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -260,6 +260,7 @@ set(SOURCES "gl/query.cpp" "gl/ssbo.cpp" "gl/cluster_grid.cpp" +"gl/readback.cpp" "imgui/imgui.cpp" "imgui/imgui_demo.cpp" @@ -333,6 +334,8 @@ if (WITH_OPENGL_MODERN) "rendering/opengl46/opengl46geometrybank.cpp" "rendering/opengl46/opengl46light.cpp" "rendering/opengl46/opengl46skydome.cpp" + "rendering/opengl46/opengl46sky.cpp" + "rendering/opengl46/opengl46exposure.cpp" "rendering/opengl46/opengl46precipitation.cpp" "rendering/opengl46/opengl46particles.cpp" "rendering/opengl46/opengl46terrain.cpp" @@ -686,4 +689,4 @@ if(ENABLE_AVX2) else() target_compile_options(${PROJECT_NAME} PRIVATE -mavx2) endif() -endif() \ No newline at end of file +endif() diff --git a/docs/opengl46-renderer.md b/docs/opengl46-renderer.md index d23c49e2..0ef23d93 100644 --- a/docs/opengl46-renderer.md +++ b/docs/opengl46-renderer.md @@ -49,6 +49,38 @@ Old `eu07.ini` files still load. These entries are read and ignored, with a line | `gfx.lights.clustered yes/no` | `yes` | `yes`: every lit vehicle headlight within 1 km lights the surroundings (see below). `no`: the shading goes through the first `dynamiclights` headlights only, as the old renderer did; useful to compare images or to work around a problem | | `gfx.lights.clusterheatmap yes/no` | `no` | debug view: the number of lights in each cluster, drawn over the image | +### Sky light and exposure + +All of these are optional; a missing entry takes the default. The switches can also be changed while the simulator +runs, in the **Sky and exposure** debug window. With every switch set to `no` the image is as before these options +existed. + +| Entry | Default | Meaning | +|---|---|---| +| `gfx.ambient.sh yes/no` | `yes` | the ambient light depends on where a surface faces: more from the bright part of the sky, less from the ground below. The average stays the ambient light the simulation works out | +| `gfx.ambient.sh.strength` | `1.0` | 0..2: how much the ambient light differs between directions (0 = uniform, as before) | +| `gfx.ambient.groundalbedo` | `0.1` | 0..1: how much light the ground below the horizon gives back (to the directional ambient light and to the atmosphere) | +| `gfx.envmap.ggx yes/no` | `yes` | rough reflections read the environment map prefiltered with the GGX lobe (made in a compute pass after each update of the map) instead of its plain mipmaps | +| `gfx.atmosphere yes/no` | `yes` | the sky, the colour of the sunlight, the ambient light and the fog colour come from a physical model of the atmosphere (Hillaire 2020) during the day; the old sky takes over between 10 degrees of sun elevation and 6 degrees below the horizon, and at night. Render only: the simulation keeps its own values | +| `gfx.atmosphere.sky` | `1.0` | brightness of the drawn sky and of the fog colour (1 = calibrated against the old sky) | +| `gfx.atmosphere.ambient` | `1.0` | brightness of the ambient light from the atmosphere | +| `gfx.atmosphere.sun` | `1.0` | brightness of the sunlight from the atmosphere | +| `gfx.atmosphere.haze` | `1.0` | amount of aerosols on a clear day (whiter, hazier sky); the weather adds to it with the overcast | +| `gfx.atmosphere.altitude` | `200` | height of the scenery above the sea level, metres | +| `gfx.postfx.autoexposure yes/no` | `yes` | the exposure follows the average brightness of the image (luminance histogram, compute pass) | +| `gfx.postfx.autoexposure.key` | `0.3` | average luminance of the image the exposure is set for | +| `gfx.postfx.autoexposure.min` | `-1.0` | the most the exposure darkens a bright image, EV | +| `gfx.postfx.autoexposure.max` | `0.5` | the most the exposure brightens a dark image, EV. Kept low on purpose: tunnels and the night are not meant to be brightened much | +| `gfx.postfx.autoexposure.speed.dark` | `1.0` | rate of adaptation to a darker image, 1/s | +| `gfx.postfx.autoexposure.speed.bright` | `3.0` | rate of adaptation to a brighter image, 1/s | +| `gfx.postfx.autoexposure.compensation` | `0.0` | fixed exposure correction, EV; applies also with the automatic exposure off | +| `gfx.postfx.bloom yes/no` | `yes` | glow around bright parts of the image | +| `gfx.postfx.bloom.intensity` | `0.04` | share of the glow in the image | +| `gfx.postfx.bloom.threshold` | `0.0` | luminance below which nothing glows; 0 = everything contributes a little (physically based) | + +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. + ### Changed option `dynamiclights` (0..7) used to limit the number of headlight sets that light the surroundings. It is still @@ -81,6 +113,10 @@ editor goes through the first `dynamiclights` headlights, the reflections throug (`overflow`, with its peak). A non-zero overflow means some cluster held more than 64 lights. - **Light clusters** window (in debug mode, menu *Windows*, item *Headlight config* opens the renderer debug windows): the switch for clustered lights, the heat map, the grid state and the same counters. +- **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:`. - `gfx.gldebug yes` turns on the OpenGL debug output (`KHR_debug`); messages go to the log. ## Building diff --git a/environment/skydome.h b/environment/skydome.h index bc22f9c7..9dcfed09 100644 --- a/environment/skydome.h +++ b/environment/skydome.h @@ -27,6 +27,8 @@ public: return m_vertices; } std::vector & colors() { return m_colours; } + std::vector const & colors() const { + return m_colours; } std::vector const & indices() const { return m_indices; } auto const & is_dirty() const { return m_dirty; } diff --git a/gl/cubemap.cpp b/gl/cubemap.cpp index 07aa1b1c..3bf8b70c 100644 --- a/gl/cubemap.cpp +++ b/gl/cubemap.cpp @@ -29,6 +29,19 @@ void gl::cubemap::alloc(GLint format, int width, int height, GLenum components, glBindTexture(GL_TEXTURE_CUBE_MAP, 0); } +void gl::cubemap::alloc_levels(GLenum format, int size, int levels) const +{ + glBindTexture(GL_TEXTURE_CUBE_MAP, *this); + glTexStorage2D(GL_TEXTURE_CUBE_MAP, levels, format, size, size); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_LEVEL, levels - 1); + glBindTexture(GL_TEXTURE_CUBE_MAP, 0); +} + void gl::cubemap::bind(int unit) const { glActiveTexture(unit); diff --git a/gl/cubemap.h b/gl/cubemap.h index f5706b65..ba3c7b22 100644 --- a/gl/cubemap.h +++ b/gl/cubemap.h @@ -13,6 +13,9 @@ namespace gl ~cubemap(); void alloc(GLint format, int width, int height, GLenum components, GLenum type) const; + // immutable storage of square faces with the given number of mipmap levels, filtered between the levels. + // the levels are meant to be written by compute shaders (glBindImageTexture), so format has to be an image format + void alloc_levels(GLenum format, int size, int levels) const; void bind(int unit) const; void generate_mipmaps() const; }; diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index 1037795c..77008286 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -28,6 +28,25 @@ void gl::glsl_common_setup() "#define CLUSTER_RANGE_SSBO_BINDING " + std::to_string(CLUSTER_RANGE_SSBO_BINDING) + "\n" + "#define CLUSTER_INDEX_SSBO_BINDING " + std::to_string(CLUSTER_INDEX_SSBO_BINDING) + "\n" + "#define CLUSTER_COUNTER_SSBO_BINDING " + std::to_string(CLUSTER_COUNTER_SSBO_BINDING) + "\n" + + "#define ENVMAP_GGX_LEVELS " + std::to_string(ENVMAP_GGX_LEVELS) + "\n" + + "#define ENVMAP_GGX_ROUGHNESS_MIN " + std::to_string(ENVMAP_GGX_ROUGHNESS_MIN) + "\n" + + "#define ENVMAP_GGX_GROUP_SIZE " + std::to_string(ENVMAP_GGX_GROUP_SIZE) + "\n" + + "#define SKY_SSBO_BINDING " + std::to_string(SKY_SSBO_BINDING) + "\n" + + "#define EXPOSURE_SSBO_BINDING " + std::to_string(EXPOSURE_SSBO_BINDING) + "\n" + + "#define EXPOSURE_HISTOGRAM_BINS " + std::to_string(EXPOSURE_HISTOGRAM_BINS) + "\n" + + "#define EXPOSURE_HISTOGRAM_GROUP_SIZE " + std::to_string(EXPOSURE_HISTOGRAM_GROUP_SIZE) + "\n" + + "#define ATMOSPHERE_UBO_BINDING " + std::to_string(ATMOSPHERE_UBO_BINDING) + "\n" + + "#define ATMOSPHERE_RESULT_SSBO_BINDING " + std::to_string(ATMOSPHERE_RESULT_SSBO_BINDING) + "\n" + + "#define ATMOSPHERE_TRANSMITTANCE_WIDTH " + std::to_string(ATMOSPHERE_TRANSMITTANCE_WIDTH) + "\n" + + "#define ATMOSPHERE_TRANSMITTANCE_HEIGHT " + std::to_string(ATMOSPHERE_TRANSMITTANCE_HEIGHT) + "\n" + + "#define ATMOSPHERE_MULTISCATTERING_SIZE " + std::to_string(ATMOSPHERE_MULTISCATTERING_SIZE) + "\n" + + "#define ATMOSPHERE_SKYVIEW_WIDTH " + std::to_string(ATMOSPHERE_SKYVIEW_WIDTH) + "\n" + + "#define ATMOSPHERE_SKYVIEW_HEIGHT " + std::to_string(ATMOSPHERE_SKYVIEW_HEIGHT) + "\n" + + "#define ATMOSPHERE_AMBIENT_GROUP_SIZE " + std::to_string(ATMOSPHERE_AMBIENT_GROUP_SIZE) + "\n" + + "#define ATMOSPHERE_TRANSMITTANCE_UNIT " + std::to_string(ATMOSPHERE_TRANSMITTANCE_UNIT) + "\n" + + "#define ATMOSPHERE_MULTISCATTERING_UNIT " + std::to_string(ATMOSPHERE_MULTISCATTERING_UNIT) + "\n" + + "#define ATMOSPHERE_SKYVIEW_UNIT " + std::to_string(ATMOSPHERE_SKYVIEW_UNIT) + "\n" + + "#define SKY_VIEW_TEX " + std::to_string(SKY_VIEW_TEX) + "\n" + R"STRING( const uint LIGHT_SPOT = 0U; const uint LIGHT_POINT = 1U; @@ -68,9 +87,20 @@ void gl::glsl_common_setup() uint lights_count; // 1 if the shading takes the lights from the list of the cluster of a fragment, see cluster_ubo uint lights_clustered; + // scale of the directional part of the sky ambient light, see sky_ssbo + float sky_ambient_factor; + // 1 if rough reflections come from the prefiltered environment map + uint env_ggx; light_s lights[]; }; + + // directional part of the sky ambient light: real spherical harmonics (bands 1 and 2, band 0 is ambient) of the light + // a surface facing a world space direction gets, in units of ambient. see gl::sky_data + layout(std430, binding = SKY_SSBO_BINDING) readonly buffer sky_ssbo + { + vec4 sky_ambient_sh[9]; + }; #endif // light clusters, see gl/cluster_grid.h diff --git a/gl/readback.cpp b/gl/readback.cpp new file mode 100644 index 00000000..834f8643 --- /dev/null +++ b/gl/readback.cpp @@ -0,0 +1,51 @@ +#include "stdafx.h" +#include "readback.h" + +gl::readback::readback(GLsizeiptr size) : m_size(size) {} + +gl::readback::~readback() +{ + for (auto &slot : m_slots) + { + // fences first, they belong to the copies + slot.copied.reset(); + if (slot.buffer != 0) + glDeleteBuffers(1, &slot.buffer); + } +} + +void gl::readback::push(GLuint Source, GLintptr Offset) +{ + if (m_used >= SLOTS) + return; + + auto &slot{m_slots[(m_first + m_used) % SLOTS]}; + if (slot.buffer == 0) + { + glCreateBuffers(1, &slot.buffer); + glNamedBufferStorage(slot.buffer, m_size, nullptr, GL_CLIENT_STORAGE_BIT); + } + glCopyNamedBufferSubData(Source, slot.buffer, Offset, 0, m_size); + slot.copied = std::make_unique(); + ++m_used; +} + +bool gl::readback::pop(void *Data) +{ + bool result{false}; + // oldest first, as long as the gpu has finished the copies; the newest finished one ends in Data + while (m_used > 0) + { + auto &slot{m_slots[m_first]}; + if (!slot.copied->is_signalled()) + break; + + glGetNamedBufferSubData(slot.buffer, 0, m_size, Data); + slot.copied.reset(); + result = true; + + m_first = (m_first + 1) % SLOTS; + --m_used; + } + return result; +} diff --git a/gl/readback.h b/gl/readback.h new file mode 100644 index 00000000..f3e185ed --- /dev/null +++ b/gl/readback.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include "object.h" +#include "fence.h" + +namespace gl +{ + // copies of a small range of a buffer the gpu writes, for the cpu to read a few frames later without waiting for the + // gpu: a queue of buffers, each with the fence of the copy into it. if the gpu is so far behind that every buffer is + // still waiting, the copy is skipped + class readback + { + public: + explicit readback(GLsizeiptr size); + ~readback(); + readback(const readback &) = delete; + readback &operator=(const readback &) = delete; + + // queues a copy of size bytes of Source, from Offset + void push(GLuint Source, GLintptr Offset = 0); + // writes the newest copy which the gpu has finished to Data and returns true; false if none was finished since the last call + bool pop(void *Data); + + private: + static constexpr size_t SLOTS = 4; + struct slot + { + GLuint buffer = 0; + std::unique_ptr copied; + }; + GLsizeiptr m_size; + std::array m_slots; + // oldest slot in use and the number of slots in use + size_t m_first = 0; + size_t m_used = 0; + }; +} // namespace gl diff --git a/gl/ubo.h b/gl/ubo.h index 487554f1..23f897ad 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -47,6 +47,17 @@ namespace gl const size_t SHADOW_TEX = MAX_TEXTURES + 0; const size_t ENV_TEX = MAX_TEXTURES + 1; const size_t HEADLIGHT_TEX = MAX_TEXTURES + 2; + // environment map prefiltered with the GGX lobe, see envmapping.glsl + const size_t ENV_GGX_TEX = MAX_TEXTURES + 3; + // the prefiltered map: size of its first level and the number of levels. level k holds the reflection of a surface of + // perceptual roughness ENVMAP_GGX_ROUGHNESS_MIN + (1 - ENVMAP_GGX_ROUGHNESS_MIN) * k / (ENVMAP_GGX_LEVELS - 1); smoother + // surfaces read the mipmaps of the plain map, which differ little from the lobe there + const size_t ENVMAP_GGX_SIZE = 256; + const size_t ENVMAP_GGX_LEVELS = 7; + const float ENVMAP_GGX_ROUGHNESS_MIN = 0.2f; + const size_t ENVMAP_GGX_GROUP_SIZE = 8; + // sky-view table of the atmosphere (sky_atmosphere.frag). past the units the texture cache keeps track of, bound directly + const size_t SKY_VIEW_TEX = MAX_TEXTURES + HELPER_TEXTURES + 0; struct scene_ubs { @@ -173,8 +184,13 @@ namespace gl // 1 if the shading takes the lights of a fragment from the list of its cluster (lights_count is not used then), 0 if not. // set per pass: the cluster grid exists only for the color pass of a perspective view uint32_t lights_clustered; + // scale of the directional part of the sky ambient light (sky_ssbo): the factor the ambient light of the object + // is scaled with (tunnels), 0 where the ambient light is not the sky's (model previews) + float sky_ambient_factor; + // 1 if the shading reads rough reflections from the prefiltered environment map, 0 if from the mipmaps of the plain one + uint32_t env_ggx; // lights[] begins at a multiple of 16 bytes - uint32_t padding[3]; + uint32_t padding; light_element lights[MAX_LIGHTS]; }; @@ -184,6 +200,8 @@ namespace gl static_assert(offsetof(light_data, fog_color) == 16, "bad layout of light_data"); static_assert(offsetof(light_data, lights_count) == 28, "bad layout of light_data"); static_assert(offsetof(light_data, lights_clustered) == 32, "bad layout of light_data"); + static_assert(offsetof(light_data, sky_ambient_factor) == 36, "bad layout of light_data"); + static_assert(offsetof(light_data, env_ggx) == 40, "bad layout of light_data"); static_assert(offsetof(light_data, lights) == 48, "bad layout of light_data"); static_assert(sizeof(light_data) == 48 + sizeof(light_element) * MAX_LIGHTS, "bad size of light_data"); @@ -271,5 +289,97 @@ namespace gl static_assert(offsetof(cluster_counters, index_count) == 0, "bad layout of cluster_counters"); static_assert(offsetof(cluster_counters, overflow_count) == 4, "bad layout of cluster_counters"); + // binding points of the sky, exposure and atmosphere buffers: layout(binding = ...) in glsl_common + const GLuint SKY_SSBO_BINDING = 5; + const GLuint EXPOSURE_SSBO_BINDING = 6; + const GLuint ATMOSPHERE_RESULT_SSBO_BINDING = 7; + const GLuint ATMOSPHERE_UBO_BINDING = 4; + + // number of coefficients of the spherical harmonics of order 2 (bands 0..2) + const size_t SKY_SH_COEFFICIENTS = 9; + + // layout std430: directional part of the sky ambient light (GLSL: sky_ssbo). the coefficients of the real spherical + // harmonics of the light a surface facing a world space direction gets, in units of light_data.ambient, without the + // constant band (that one is light_data.ambient itself); rgb used, w is padding + struct sky_data + { + glm::vec4 ambient_sh[SKY_SH_COEFFICIENTS]; + }; + + static_assert(sizeof(sky_data) == 144, "bad size of sky_data"); + + // layout std430: luminance histogram of the image and the exposure worked out from it (GLSL: exposure_ssbo) + const GLuint EXPOSURE_HISTOGRAM_BINS = 256; + const GLuint EXPOSURE_HISTOGRAM_GROUP_SIZE = 16; + + struct exposure_data + { + uint32_t histogram[EXPOSURE_HISTOGRAM_BINS]; + // luminance the exposure is adapted to, the time-smoothed average of the image + float adapted_luminance; + // multiplier the tonemapping applies to the image + float exposure; + // average luminance of the last image + float average_luminance; + // 0 until the first average was taken, the adaptation starts from it + uint32_t valid; + }; + + static_assert(sizeof(exposure_data) == 4 * EXPOSURE_HISTOGRAM_BINS + 16, "bad size of exposure_data"); + static_assert(offsetof(exposure_data, adapted_luminance) == 4 * EXPOSURE_HISTOGRAM_BINS, "bad layout of exposure_data"); + + // sizes of the look-up tables of the atmosphere (Hillaire 2020) + const GLuint ATMOSPHERE_TRANSMITTANCE_WIDTH = 256; + const GLuint ATMOSPHERE_TRANSMITTANCE_HEIGHT = 64; + const GLuint ATMOSPHERE_MULTISCATTERING_SIZE = 32; + const GLuint ATMOSPHERE_SKYVIEW_WIDTH = 192; + const GLuint ATMOSPHERE_SKYVIEW_HEIGHT = 108; + // work group of atmosphere_ambient.comp, which projects the sky on the spherical harmonics in one group + const GLuint ATMOSPHERE_AMBIENT_GROUP_SIZE = 256; + // binding points of the look-up tables in the compute passes (images and samplers) + const GLuint ATMOSPHERE_TRANSMITTANCE_UNIT = 0; + const GLuint ATMOSPHERE_MULTISCATTERING_UNIT = 1; + const GLuint ATMOSPHERE_SKYVIEW_UNIT = 2; + + // layout std140: parameters of the atmosphere (GLSL: atmosphere_ubo). distances in kilometres, coefficients per kilometre + struct atmosphere_ubs + { + // rgb: rayleigh scattering, w: rayleigh scale height + glm::vec4 rayleigh; + // x: mie scattering, y: mie extinction, z: mie scale height, w: mie phase asymmetry + glm::vec4 mie; + // rgb: ozone absorption, w: altitude of the middle of the ozone layer + glm::vec4 ozone; + // rgb: albedo of the ground, w: half width of the ozone layer + glm::vec4 ground; + // world space direction towards the sun (y up), w unused + glm::vec4 sun_direction; + float bottom_radius; + float top_radius; + // distance of the observer from the centre of the planet + float observer_radius; + float padding; + }; + + static_assert(sizeof(atmosphere_ubs) == 96, "bad size of atmosphere_ubs"); + + // layout std430: what atmosphere_ambient.comp works out for the cpu (GLSL: atmosphere_result_ssbo). radiance with + // the sun of illuminance 1 + struct atmosphere_result + { + // radiance arriving at the observer from every direction, ground included, as real spherical harmonics (world, y up) + glm::vec4 radiance_sh[SKY_SH_COEFFICIENTS]; + // transmittance of the atmosphere towards the sun at the observer + glm::vec4 sun_transmittance; + // average radiance of the sky just above the horizon + glm::vec4 horizon; + // average radiance of the upper hemisphere + glm::vec4 average; + // transmittance of the atmosphere straight up from the observer: the colour of the sun high in the sky + glm::vec4 zenith_transmittance; + }; + + static_assert(sizeof(atmosphere_result) == 16 * (SKY_SH_COEFFICIENTS + 4), "bad size of atmosphere_result"); + #pragma pack(pop) } diff --git a/rendering/opengl46/opengl46exposure.cpp b/rendering/opengl46/opengl46exposure.cpp new file mode 100644 index 00000000..ea9a1ba1 --- /dev/null +++ b/rendering/opengl46/opengl46exposure.cpp @@ -0,0 +1,175 @@ +/* +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/opengl46exposure.h" + +#include "model/Texture.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" + +namespace +{ + +// luminance range of the histogram, log2: 2^-12 .. 2^6. darker pixels count as black, brighter ones as the brightest +float const HISTOGRAM_LOG_MIN{-12.f}; +float const HISTOGRAM_LOG_MAX{6.f}; +// the bloom chain goes down this many levels at most, and not below this size +std::size_t const BLOOM_LEVELS{6}; +int const BLOOM_MIN_SIZE{8}; +// format of the levels: no alpha, no negative values, half the memory of rgba16f +GLint const BLOOM_FORMAT{GL_R11F_G11F_B10F}; +// soft knee of the threshold, a fraction of it +float const BLOOM_KNEE{0.5f}; + +} // namespace + +void opengl46_exposure::init() +{ + auto const make_compute = [](std::string const &Name) { + gl::shader shader(Name); + return std::make_unique(std::vector>{shader}); + }; + m_histogram_program = make_compute("exposure_histogram.comp"); + m_average_program = make_compute("exposure_average.comp"); + m_bloom_down = std::make_unique("bloom_down"); + m_bloom_up = std::make_unique("bloom_up"); + + m_exposure_ssbo = std::make_unique(sizeof(gl::exposure_data), gl::EXPOSURE_SSBO_BINDING); + gl::exposure_data data{}; + data.adapted_luminance = Global.gfx_postfx_autoexposure_key; + data.exposure = 1.f; + data.average_luminance = Global.gfx_postfx_autoexposure_key; + data.valid = 0; + m_exposure_ssbo->update(data); +} + +void opengl46_exposure::bind() +{ + m_exposure_ssbo->bind_base(); +} + +std::unique_ptr opengl46_exposure::make_bloom_chain(int const Width, int const Height) +{ + auto chain{std::make_unique()}; + glm::ivec2 size{Width, Height}; + for (std::size_t level = 0; level < BLOOM_LEVELS; ++level) + { + size = glm::max(size / 2, glm::ivec2{1}); + if (level > 0 && std::min(size.x, size.y) < BLOOM_MIN_SIZE) + break; + chain->sizes.push_back(size); + auto &texture{chain->down.emplace_back(std::make_unique())}; + texture->alloc_rendertarget(BLOOM_FORMAT, GL_RGB, size.x, size.y, 1, 1, GL_CLAMP_TO_EDGE); + auto &framebuffer{chain->down_fb.emplace_back(std::make_unique())}; + framebuffer->attach(*texture, GL_COLOR_ATTACHMENT0); + if (!framebuffer->is_complete()) + return nullptr; + } + for (std::size_t level = 0; level + 1 < chain->sizes.size(); ++level) + { + auto const &size{chain->sizes[level]}; + auto &texture{chain->up.emplace_back(std::make_unique())}; + texture->alloc_rendertarget(BLOOM_FORMAT, GL_RGB, size.x, size.y, 1, 1, GL_CLAMP_TO_EDGE); + auto &framebuffer{chain->up_fb.emplace_back(std::make_unique())}; + framebuffer->attach(*texture, GL_COLOR_ATTACHMENT0); + if (!framebuffer->is_complete()) + return nullptr; + } + return chain->sizes.empty() ? nullptr : std::move(chain); +} + +void opengl46_exposure::measure(opengl_texture &Image, double const Deltatime) +{ + float data[4]; + if (m_readback.pop(data)) + std::copy(std::begin(data), std::end(data), std::begin(m_readback_data)); + + m_exposure_timer.begin(); + m_exposure_ssbo->bind_base(); + Image.bind(0); + auto const bins{static_cast(gl::EXPOSURE_HISTOGRAM_BINS - 2)}; + glProgramUniform2f(*m_histogram_program, 0, HISTOGRAM_LOG_MIN, bins / (HISTOGRAM_LOG_MAX - HISTOGRAM_LOG_MIN)); + auto const groupsize{2 * static_cast(gl::EXPOSURE_HISTOGRAM_GROUP_SIZE)}; + m_histogram_program->dispatch((Image.get_width() + groupsize - 1) / groupsize, (Image.get_height() + groupsize - 1) / groupsize); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + + glProgramUniform2f(*m_average_program, 0, HISTOGRAM_LOG_MIN, (HISTOGRAM_LOG_MAX - HISTOGRAM_LOG_MIN) / bins); + glProgramUniform4f(*m_average_program, 1, + static_cast(std::clamp(Deltatime, 0.0, 1.0)), + Global.gfx_postfx_autoexposure_speed_dark, + Global.gfx_postfx_autoexposure_speed_bright, + Global.gfx_postfx_autoexposure_key); + glProgramUniform2f(*m_average_program, 2, std::exp2(Global.gfx_postfx_autoexposure_min), std::exp2(Global.gfx_postfx_autoexposure_max)); + m_average_program->dispatch(1); + // read by the tonemapping, and copied for the debug window + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT); + m_exposure_timer.end(); + + m_readback.push(*m_exposure_ssbo, offsetof(gl::exposure_data, adapted_luminance)); + opengl_texture::reset_unit_cache(); +} + +opengl_texture *opengl46_exposure::bloom(bloom_chain &Chain, opengl_texture &Image, gl::ubo &Modelubo, gl::model_ubs &Model) +{ + m_bloom_timer.begin(); + // down, the first step from the image itself + for (std::size_t level = 0; level < Chain.sizes.size(); ++level) + { + Model.param[0] = glm::vec4{(level == 0 ? 1.f : 0.f), Global.gfx_postfx_bloom_threshold, BLOOM_KNEE, 0.f}; + Modelubo.update(Model); + glViewport(0, 0, Chain.sizes[level].x, Chain.sizes[level].y); + m_bloom_down->apply(level == 0 ? Image : *Chain.down[level - 1], Chain.down_fb[level].get()); + } + // up, from the lowest level + for (std::size_t level = Chain.up.size(); level-- > 0;) + { + auto &lower{(level + 1 < Chain.up.size()) ? *Chain.up[level + 1] : *Chain.down[level + 1]}; + glViewport(0, 0, Chain.sizes[level].x, Chain.sizes[level].y); + m_bloom_up->apply({&lower, Chain.down[level].get()}, Chain.up_fb[level].get()); + } + m_bloom_timer.end(); + opengl_texture::reset_unit_cache(); + return Chain.up.empty() ? Chain.down.front().get() : Chain.up.front().get(); +} + +glm::vec4 opengl46_exposure::tonemap_params(std::size_t const Bloomlevels) const +{ + auto const bloom{(Global.gfx_postfx_bloom && Bloomlevels > 0) ? Global.gfx_postfx_bloom_intensity : 0.f}; + return glm::vec4{ + bloom, + Global.gfx_postfx_autoexposure ? 1.f : 0.f, + std::exp2(Global.gfx_postfx_autoexposure_compensation), + Bloomlevels > 0 ? 1.f / static_cast(Bloomlevels) : 0.f}; +} + +std::string opengl46_exposure::info_times() +{ + std::string text; + if (auto const time = m_exposure_timer.result()) + text += " exposure: " + to_string(static_cast(*time) / 1000000.0, 3) + " ms\n"; + if (auto const time = m_bloom_timer.result()) + text += " bloom: " + to_string(static_cast(*time) / 1000000.0, 3) + " ms\n"; + return text; +} + +void opengl46_exposure::draw_debug_ui() +{ + ImGui::Checkbox("automatic exposure (gfx.postfx.autoexposure)", &Global.gfx_postfx_autoexposure); + ImGui::SliderFloat("key", &Global.gfx_postfx_autoexposure_key, 0.02f, 2.f, "%.3f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("min EV", &Global.gfx_postfx_autoexposure_min, -4.f, 0.f); + ImGui::SliderFloat("max EV", &Global.gfx_postfx_autoexposure_max, 0.f, 4.f); + ImGui::SliderFloat("adaptation to dark (1/s)", &Global.gfx_postfx_autoexposure_speed_dark, 0.05f, 10.f); + ImGui::SliderFloat("adaptation to bright (1/s)", &Global.gfx_postfx_autoexposure_speed_bright, 0.05f, 10.f); + ImGui::SliderFloat("compensation EV", &Global.gfx_postfx_autoexposure_compensation, -4.f, 4.f); + ImGui::Text("average luminance %.3f, adapted %.3f, exposure %.3f (%+.2f EV)", m_readback_data[2], m_readback_data[0], m_readback_data[1], std::log2(std::max(m_readback_data[1], 1e-6f))); + ImGui::Checkbox("bloom (gfx.postfx.bloom)", &Global.gfx_postfx_bloom); + ImGui::SliderFloat("bloom intensity", &Global.gfx_postfx_bloom_intensity, 0.f, 0.5f); + ImGui::SliderFloat("bloom threshold", &Global.gfx_postfx_bloom_threshold, 0.f, 10.f); +} diff --git a/rendering/opengl46/opengl46exposure.h b/rendering/opengl46/opengl46exposure.h new file mode 100644 index 00000000..d634b6c7 --- /dev/null +++ b/rendering/opengl46/opengl46exposure.h @@ -0,0 +1,78 @@ +/* +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 "gl/ubo.h" +#include "gl/ssbo.h" +#include "gl/shader.h" +#include "gl/postfx.h" +#include "gl/framebuffer.h" +#include "gl/query.h" +#include "gl/readback.h" + +class opengl_texture; + +// automatic exposure and bloom of the post processing of the opengl 4.6 renderer: +// - a luminance histogram of the final hdr image in a compute pass, and the exposure adapted to its average over time, +// within the limits of gfx.postfx.autoexposure.* (exposure_*.comp). the exposure stays on the gpu, the tonemapping reads it +// - bloom: the image taken down a chain of half size levels and back up, each level added (postfx_bloom_*.frag) +// both are switched by Global.gfx_postfx_autoexposure and gfx_postfx_bloom; off, the tonemapping gets the image as before +class opengl46_exposure +{ + public: + // the levels of the bloom chain of one viewport + struct bloom_chain + { + std::vector> down; + std::vector> down_fb; + // one fewer than down: the lowest level goes up as it is + std::vector> up; + std::vector> up_fb; + std::vector sizes; + }; + + opengl46_exposure() = default; + opengl46_exposure(const opengl46_exposure &) = delete; + opengl46_exposure &operator=(const opengl46_exposure &) = delete; + + // creates the programs and the buffer. throws gl::shader_exception + void init(); + // binds the buffer the tonemapping reads; bindings are context state, so done in every context + void bind(); + // the bloom chain for an image of the given size; null if it could not be made + static std::unique_ptr make_bloom_chain(int const Width, int const Height); + // measures the luminance of the image and adapts the exposure to it. Deltatime: seconds since the last measurement + void measure(opengl_texture &Image, double const Deltatime); + // takes the image down and up the chain; returns the result, which the tonemapping blends in. leaves the viewport + // and the framebuffer binding changed. Model is the model_ubs the post processing shaders read the parameters from + opengl_texture *bloom(bloom_chain &Chain, opengl_texture &Image, gl::ubo &Modelubo, gl::model_ubs &Model); + // parameters of the tonemapping (postfx_tonemapping.frag), with bloom from a chain of Bloomlevels levels (0: none) + glm::vec4 tonemap_params(std::size_t const Bloomlevels) const; + // lines for the gpu times of the debug panel + std::string info_times(); + // controls of the debug window + void draw_debug_ui(); + + private: + std::unique_ptr m_histogram_program; + std::unique_ptr m_average_program; + std::unique_ptr m_bloom_down; + std::unique_ptr m_bloom_up; + std::unique_ptr m_exposure_ssbo; + // the exposure and luminances, read back for the debug window only + gl::readback m_readback{4 * sizeof(float)}; + float m_readback_data[4]{1.f, 1.f, 1.f, 0.f}; + gl::gpu_timer m_exposure_timer; + gl::gpu_timer m_bloom_timer; +}; diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index ece06d0d..7c304e0a 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -181,6 +181,17 @@ bool opengl46_renderer::Init(GLFWwindow *Window) light_ssbo->bind_base(); instance_ubo = std::make_unique(sizeof(gl::instance_ubs), 3, GL_STREAM_DRAW); + try + { + m_sky.init(); + m_exposure.init(); + } + catch (gl::shader_exception const &e) + { + ErrorLog("invalid shader: " + std::string(e.what())); + return false; + } + // better initialize with 0 to not crash driver/whole system // when we forget memset(&light_data, 0, sizeof(light_data)); @@ -462,6 +473,8 @@ bool opengl46_renderer::init_viewport(viewport_config &vp) model_ubo->bind_uniform(); scene_ubo->bind_uniform(); light_ssbo->bind_base(); + m_sky.bind(); + m_exposure.bind(); vp.clusters = std::make_unique(); // the material shaders declare the cluster blocks whether they read them or not, and drawing with an active storage @@ -546,6 +559,11 @@ bool opengl46_renderer::init_viewport(viewport_config &vp) if (!vp.main2_fb->is_complete()) return false; + + // made whether the bloom is on or not, it can be switched on in the debug window + vp.bloom = opengl46_exposure::make_bloom_chain(vp.width, vp.height); + if (!vp.bloom) + ErrorLog("bloom framebuffer setup failed, no bloom"); } if (vp.custom_backbuffer) @@ -588,6 +606,10 @@ bool opengl46_renderer::Render() // quantize sun angle to reduce shadow crawl auto const quantizationstep{0.004f}; m_sunlight.direction = glm::normalize(quantizationstep * glm::roundEven(m_sunlight.direction * (1.f / quantizationstep))); + // the light of the sky for the render: colours of the sun, ambient light and fog from the atmosphere where it is + // used, the directional part of the ambient light + m_fogcolor = Global.FogColor; + m_sky.update(m_sunlight, m_fogcolor, simulation::Environment.skydome(), simulation::Environment.sun().getDirection(), simulation::Environment.sun().getAngle()); } // generate new frame opengl_texture::reset_unit_cache(); @@ -669,6 +691,8 @@ void opengl46_renderer::SwapBuffers() m_debugtimestext += " clusters: " + to_string(static_cast(*clusterstime) / 1000000.0, 3) + " ms\n"; if (auto const culltime = m_cullstimer.result()) 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 += "uilayer: " + to_string( Timer::subsystem.gfx_gui.average(), 2 ) + " ms\n"; if( DebugModeFlag ) m_debugtimestext += m_textures.info(); @@ -756,6 +780,14 @@ void opengl46_renderer::draw_debug_ui() } ImGui::End(); + if (ImGui::Begin("Sky and exposure")) + { + m_sky.draw_debug_ui(); + ImGui::Separator(); + m_exposure.draw_debug_ui(); + } + ImGui::End(); + ImGui::SetNextWindowSize(ImVec2S(400, 400)); if (ImGui::Begin("Pickbuffer") && m_pick_tex) { ImGui::Image((ImTextureID)(intptr_t)(m_pick_tex->id), ImGui::GetContentRegionAvail(), ImVec2(0, 1.0), ImVec2(1.0, 0)); @@ -873,6 +905,9 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) Timer::subsystem.gfx_reflections.start(); if (Render_reflections(vp)) m_renderpass = m_colorpass; // restore color pass settings + // the prefiltered map is made with every update of the environment map, or once the switch is turned on + if (Global.gfx_envmap_ggx && m_environmentready && (m_sky.prefiltered() == nullptr)) + m_sky.prefilter(*m_env_tex); Timer::subsystem.gfx_reflections.stop(); setup_env_map(m_env_tex.get()); } @@ -956,6 +991,8 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) glDebug("render environment"); scene_ubs.projection = OpenGLMatrices.data(GL_PROJECTION); + // back to the rotation of the camera after the faces of the environment map, which are drawn with their own + scene_ubs.inv_view = glm::inverse(glm::mat4{glm::mat3{m_colorpass.pass_camera.modelview()}}); scene_ubo->update(scene_ubs); Render(&simulation::Environment); @@ -1083,6 +1120,22 @@ if (!Global.gfx_skippipeline) m_pfx_ssao_apply->apply({ vp.main_tex.get(), vp.ssao_blur_tex.get() }, vp.main2_fb.get()); } + // the exposure follows the main view; the other viewports use it too + if (Global.gfx_postfx_autoexposure && vp.main) + m_exposure.measure(*vp.main2_tex, Timer::GetDeltaRenderTime()); + opengl_texture *bloomtexture{nullptr}; + std::size_t bloomlevels{0}; + if (Global.gfx_postfx_bloom && vp.bloom) + { + bloomtexture = m_exposure.bloom(*vp.bloom, *vp.main2_tex, *model_ubo, model_ubs); + bloomlevels = vp.bloom->sizes.size(); + } + model_ubs.param[0] = m_exposure.tonemap_params(bloomlevels); + model_ubo->update(model_ubs); + std::vector tonemapinput{vp.main2_tex.get()}; + if (bloomtexture != nullptr) + tonemapinput.push_back(bloomtexture); + if (!Global.gfx_shadergamma) glEnable(GL_FRAMEBUFFER_SRGB); @@ -1093,14 +1146,14 @@ if (!Global.gfx_skippipeline) if( Global.gfx_postfx_chromaticaberration_enabled ) { glViewport(0, 0, vp.width, vp.height); vp.main_fb->setup_drawing(1); - m_pfx_tonemapping->apply( *vp.main2_tex, vp.main_fb.get() ); + m_pfx_tonemapping->apply( tonemapinput, vp.main_fb.get() ); glViewport(0, 0, target_size.x, target_size.y); m_pfx_chromaticaberration->apply( *vp.main_tex, target ); } else { glViewport(0, 0, target_size.x, target_size.y); - m_pfx_tonemapping->apply( *vp.main2_tex, target ); + m_pfx_tonemapping->apply( tonemapinput, target ); } if (Global.gfx_lights_clusterheatmap && vp.clusters->valid()) @@ -1235,6 +1288,8 @@ if (!Global.gfx_skippipeline) setup_shadow_empty_map(); scene_ubs.projection = OpenGLMatrices.data(GL_PROJECTION); + // world space directions of the shading of the face (the sky, the directional ambient light) + scene_ubs.inv_view = glm::inverse(glm::mat4{glm::mat3{m_renderpass.pass_camera.modelview()}}); scene_ubo->update(scene_ubs); Render(&simulation::Environment); // opaque parts... @@ -1478,6 +1533,8 @@ bool opengl46_renderer::Render_reflections(viewport_config &vp) { m_env_tex->generate_mipmaps(); m_environmentready = true; + if (Global.gfx_envmap_ggx) + m_sky.prefilter(*m_env_tex); } m_env_fb->detach(GL_COLOR_ATTACHMENT0); @@ -1899,6 +1956,15 @@ void opengl46_renderer::setup_env_map(gl::cubemap *tex) glBindTexture(GL_TEXTURE_CUBE_MAP, 0); glActiveTexture(GL_TEXTURE0); } + // rough reflections read the prefiltered map along with the real environment map; with the empty stand-in (the reflection + // pass) or without the map the shading keeps to the mipmaps of what is bound + auto const *prefiltered{(tex != nullptr && tex == m_env_tex.get()) ? m_sky.prefiltered() : nullptr}; + m_envmap_bound = (prefiltered != nullptr); + if (prefiltered != nullptr) + prefiltered->bind(GL_TEXTURE0 + gl::ENV_GGX_TEX); + else if (m_empty_cubemap) + m_empty_cubemap->bind(GL_TEXTURE0 + gl::ENV_GGX_TEX); + glActiveTexture(GL_TEXTURE0); opengl_texture::reset_unit_cache(); } @@ -1937,6 +2003,7 @@ void opengl46_renderer::setup_sunlight_intensity( float const Factor ) { m_sunlight.apply_intensity( Factor ); light_data.lights[ 0 ].intensity = m_sunlight.factor * (1.0f - Global.Overcast * 0.33f); light_data.ambient = m_sunlight.ambient * m_sunlight.factor; + light_data.sky_ambient_factor = ( Global.gfx_ambient_sh ? m_sunlight.factor : 0.f ); upload_light_header(); } @@ -1967,10 +2034,14 @@ bool opengl46_renderer::Render(world_environment *Environment) { glScalef( 500.0f, 500.0f, 500.0f ); model_ubs.set_modelview( OpenGLMatrices.data( GL_MODELVIEW ) ); + auto const atmosphere{ m_sky.atmosphere_weight() > 0.f }; + if( atmosphere ) { + m_sky.setup_sky( model_ubs ); + } model_ubo->update( model_ubs ); m_skydomerenderer.update(); - m_skydomerenderer.render(); + m_skydomerenderer.render( atmosphere ); // skydome uses a custom vbo which could potentially confuse the main geometry system. hardly elegant but, eh gfx::opengl46_vaogeometrybank::reset(); } @@ -2002,6 +2073,7 @@ bool opengl46_renderer::Render(world_environment *Environment) auto const fogfactor{std::clamp(Global.fFogEnd / 2000.f, 0.f, 1.f)}; // stronger fog reduces opacity of the celestial bodies float const duskfactor = 1.0f - std::clamp(std::abs(Environment->m_sun.getAngle()), 0.0f, 12.0f) / 12.0f; glm::vec3 suncolor = glm::mix(glm::vec3(1.0f, 242.0f / 255.0f, 231.0f / 255.0f), glm::vec3(235.0f / 255.0f, 140.0f / 255.0f, 36.0f / 255.0f), duskfactor); + suncolor = m_sky.sun_disc_color(suncolor); // sun { @@ -5643,6 +5715,9 @@ void opengl46_renderer::Preview_Color(TAnimModel *Instance, float const Squaredi light_data.lights_total = 1; light_data.lights_count = 1; light_data.lights_clustered = 0; + // a uniform ambient light, as the preview always had + light_data.sky_ambient_factor = 0.f; + light_data.env_ggx = 0; light_data.lights[0].type = gl::light_element::DIR; light_data.lights[0].dir = modelview * glm::vec4(m_sunlight.direction, 0.0f); light_data.lights[0].color = m_sunlight.diffuse; @@ -5855,6 +5930,9 @@ bool opengl46_renderer::Render_Preview(TAnimModel *Instance, int const Size, int glViewport(0, 0, targetsize, targetsize); if (!Global.gfx_shadergamma) glEnable(GL_FRAMEBUFFER_SRGB); + // no bloom, no automatic exposure + model_ubs.param[0] = glm::vec4{0.f, 0.f, 1.f, 0.f}; + model_ubo->update(model_ubs); m_pfx_tonemapping->apply(*m_preview.hdr_tex, m_preview.ldr_fb.get()); if (!Global.gfx_shadergamma) glDisable(GL_FRAMEBUFFER_SRGB); @@ -6067,15 +6145,17 @@ void opengl46_renderer::Update_Lights(light_array &Lights, bool const Headlights m_lights_lit = static_cast(light_i - 1); light_data.lights_count = 1 + std::min(static_cast(light_i - 1), static_cast(std::max(Global.DynamicLightCount, 0))); light_data.lights_clustered = Clustered ? 1 : 0; + light_data.env_ggx = ( Global.gfx_envmap_ggx && m_envmap_bound ) ? 1 : 0; // fill sunlight data light_data.ambient = m_sunlight.ambient * m_sunlight.factor;// *simulation::Environment.light_intensity(); + light_data.sky_ambient_factor = ( Global.gfx_ambient_sh ? m_sunlight.factor : 0.f ); light_data.lights[0].type = gl::light_element::DIR; light_data.lights[0].dir = mv * glm::vec4(m_sunlight.direction, 0.0f); light_data.lights[0].color = m_sunlight.diffuse * m_sunlight.factor * simulation::Environment.light_intensity(); light_data.lights[0].ambient = 0.0f; light_data.lights[0].intensity = 1.0f; // fill fog data - light_data.fog_color = Global.FogColor; + light_data.fog_color = m_fogcolor; if (Global.fFogEnd > 0) { m_fogrange = Global.fFogEnd / std::max(1.f, Global.Overcast * 2.f); diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index a54bf4ae..748db688 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -32,6 +32,8 @@ http://mozilla.org/MPL/2.0/. #include "gl/pbo.h" #include "gl/query.h" #include "rendering/opengl46/opengl46terrain.h" +#include "rendering/opengl46/opengl46sky.h" +#include "rendering/opengl46/opengl46exposure.h" // bare-bones render controller, in lack of anything better yet class opengl46_renderer : public gfx_renderer { @@ -261,6 +263,9 @@ class opengl46_renderer : public gfx_renderer { std::unique_ptr backbuffer_fb; std::unique_ptr backbuffer_tex; + // levels of the bloom of the post processing; null without the pipeline + std::unique_ptr bloom; + // light cluster grid of the viewport std::unique_ptr clusters; // depth of the finished scene resolved from the msaa buffer, for the cluster heatmap; made when it is first needed @@ -380,6 +385,14 @@ class opengl46_renderer : public gfx_renderer { texture_manager m_textures; opengl46_terrain_materials m_terrainmaterials{m_materials, m_textures}; opengl46_light m_sunlight; + // fog colour of the frame: the one of the simulation, or of the atmosphere (see opengl46_sky::update()) + glm::vec3 m_fogcolor{0.f}; + // light of the sky: directional ambient light, atmosphere, prefiltered environment map + opengl46_sky m_sky; + // automatic exposure and bloom of the post processing + opengl46_exposure m_exposure; + // 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}; /* float m_sunandviewangle; // cached dot product of sunlight and camera vectors */ diff --git a/rendering/opengl46/opengl46sky.cpp b/rendering/opengl46/opengl46sky.cpp new file mode 100644 index 00000000..33b63ffa --- /dev/null +++ b/rendering/opengl46/opengl46sky.cpp @@ -0,0 +1,417 @@ +/* +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/opengl46sky.h" + +#include + +#include "environment/skydome.h" +#include "model/Texture.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" + +namespace +{ + +// irradiance of a radiance given as spherical harmonics, divided by pi (the units of the ambient light of the shading: +// under a uniform sky of radiance L it is L): the coefficients of band l are scaled by A_l / pi (Ramamoorthi, Hanrahan 2001) +float const SH_IRRADIANCE_BAND[3]{1.f, 2.f / 3.f, 0.25f}; +int const SH_BAND[gl::SKY_SH_COEFFICIENTS]{0, 1, 1, 1, 2, 2, 2, 2, 2}; +float const SH_Y00{0.282095f}; +float const SH_Y1{0.488603f}; +float const PI{3.14159265359f}; + +// real spherical harmonics of order 2 in a world space direction, in the order of light_common.glsl +std::array sh_basis(glm::vec3 const &n) +{ + return {SH_Y00, + SH_Y1 * n.y, + SH_Y1 * n.z, + SH_Y1 * n.x, + 1.092548f * n.x * n.y, + 1.092548f * n.y * n.z, + 0.315392f * (3.f * n.z * n.z - 1.f), + 1.092548f * n.x * n.z, + 0.546274f * (n.x * n.x - n.y * n.y)}; +} + +float luminance(glm::vec3 const &Color) +{ + return glm::dot(Color, glm::vec3{0.2126f, 0.7152f, 0.0722f}); +} + +glm::vec3 desaturate(glm::vec3 const &Color, float const Amount) +{ + return glm::mix(Color, glm::vec3{luminance(Color)}, Amount); +} + +// the response of color.frag to the vertex colours of the dome, so the light taken from the dome is the light it shows +glm::vec3 sky_shoulder(glm::vec3 const &Color) +{ + float const knee{0.25f}; + float const range{0.95f - knee}; + auto const excess{glm::max(Color - glm::vec3{knee}, glm::vec3{0.f})}; + return glm::min(Color, glm::vec3{knee}) + range * (glm::vec3{1.f} - glm::exp(-excess / range)); +} + +// the atmosphere is calibrated against the old sky: these scales make a sky of the atmosphere, with the sun 15 to 60 degrees +// high on a clear day, as bright as the old Perez dome is drawn on average, and the ambient light and the sunlight as strong +// as the old ones, with the default ground albedo (measured on the tables worked out by the same shaders on a software +// renderer, see the notes of the plan, stage S2) +float const ATMOSPHERE_SKY_SCALE{16.f}; +float const ATMOSPHERE_AMBIENT_SCALE{44.f}; +float const ATMOSPHERE_SUN_SCALE{1.45f}; +// sun elevations (degrees) between which the sky goes over from the old model (night, twilight) to the atmosphere. the +// atmosphere darkens at dusk much faster than the old model, which the rest of the lighting was tuned to +float const ATMOSPHERE_FADE_START{-6.f}; +float const ATMOSPHERE_FADE_END{10.f}; +// the shading scales the sunlight and the ambient light by these (light_common.glsl), so the light the sun gives the +// ground, in the units of the ambient light, is the diffuse colour times their ratio +float const SHADING_SUN_TO_AMBIENT{1.5f / 0.65f}; + +// parameters of the earth's atmosphere after Hillaire 2020 (km, 1/km) +glm::vec3 const RAYLEIGH_SCATTERING{5.802e-3f, 13.558e-3f, 33.1e-3f}; +float const RAYLEIGH_SCALE_HEIGHT{8.f}; +float const MIE_SCATTERING{3.996e-3f}; +float const MIE_EXTINCTION{4.440e-3f}; +float const MIE_SCALE_HEIGHT{1.2f}; +float const MIE_ASYMMETRY{0.8f}; +glm::vec3 const OZONE_ABSORPTION{0.650e-3f, 1.881e-3f, 0.085e-3f}; +float const OZONE_ALTITUDE{25.f}; +float const OZONE_HALF_WIDTH{15.f}; +float const BOTTOM_RADIUS{6360.f}; +float const TOP_RADIUS{6460.f}; + +} // namespace + +opengl46_sky::~opengl46_sky() +{ + GLuint const tables[]{m_transmittance_table, m_multiscattering_table, m_skyview_table}; + for (auto const table : tables) + { + if (table != 0) + glDeleteTextures(1, &table); + } +} + +GLuint opengl46_sky::create_table(GLsizei const Width, GLsizei const Height) +{ + GLuint table{0}; + glCreateTextures(GL_TEXTURE_2D, 1, &table); + glTextureStorage2D(table, 1, GL_RGBA16F, Width, Height); + glTextureParameteri(table, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTextureParameteri(table, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTextureParameteri(table, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTextureParameteri(table, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + float const black[4]{0.f, 0.f, 0.f, 0.f}; + glClearTexImage(table, 0, GL_RGBA, GL_FLOAT, black); + return table; +} + +void opengl46_sky::init() +{ + auto const make_compute = [](std::string const &Name) { + gl::shader shader(Name); + return std::make_unique(std::vector>{shader}); + }; + m_transmittance_program = make_compute("atmosphere_transmittance.comp"); + m_multiscattering_program = make_compute("atmosphere_multiscattering.comp"); + m_skyview_program = make_compute("atmosphere_skyview.comp"); + m_ambient_program = make_compute("atmosphere_ambient.comp"); + m_prefilter_program = make_compute("envmap_prefilter.comp"); + + m_sky_ssbo = std::make_unique(sizeof(gl::sky_data), gl::SKY_SSBO_BINDING); + m_sky_ssbo->update(m_sky_data); + m_atmosphere_ubo = std::make_unique(sizeof(gl::atmosphere_ubs), gl::ATMOSPHERE_UBO_BINDING); + m_atmosphere_result_ssbo = std::make_unique(sizeof(gl::atmosphere_result), gl::ATMOSPHERE_RESULT_SSBO_BINDING); + + m_transmittance_table = create_table(gl::ATMOSPHERE_TRANSMITTANCE_WIDTH, gl::ATMOSPHERE_TRANSMITTANCE_HEIGHT); + m_multiscattering_table = create_table(gl::ATMOSPHERE_MULTISCATTERING_SIZE, gl::ATMOSPHERE_MULTISCATTERING_SIZE); + m_skyview_table = create_table(gl::ATMOSPHERE_SKYVIEW_WIDTH, gl::ATMOSPHERE_SKYVIEW_HEIGHT); + + // a valid atmosphere for the sky shader from the start, the tables stay black until the first update + m_atmosphere.rayleigh = glm::vec4{RAYLEIGH_SCATTERING, RAYLEIGH_SCALE_HEIGHT}; + m_atmosphere.mie = glm::vec4{MIE_SCATTERING, MIE_EXTINCTION, MIE_SCALE_HEIGHT, MIE_ASYMMETRY}; + m_atmosphere.ozone = glm::vec4{OZONE_ABSORPTION, OZONE_ALTITUDE}; + m_atmosphere.ground = glm::vec4{glm::vec3{Global.gfx_ambient_groundalbedo}, OZONE_HALF_WIDTH}; + m_atmosphere.sun_direction = glm::vec4{0.f, 1.f, 0.f, 0.f}; + m_atmosphere.bottom_radius = BOTTOM_RADIUS; + m_atmosphere.top_radius = TOP_RADIUS; + m_atmosphere.observer_radius = BOTTOM_RADIUS + 0.2f; + m_atmosphere_ubo->update(m_atmosphere); +} + +void opengl46_sky::bind() +{ + m_sky_ssbo->bind_base(); + m_atmosphere_ubo->bind_uniform(); + m_atmosphere_result_ssbo->bind_base(); +} + +opengl46_sky::sh_coefficients opengl46_sky::dome_directional(CSkyDome const &Skydome, glm::vec3 const &Ambient, glm::vec3 const &Sunlight, float const Sunheight) const +{ + sh_coefficients radiance{}; + auto const &vertices{Skydome.vertices()}; + auto const &colors{Skydome.colors()}; + // the vertices make a grid even in the elevation and the azimuth, so the solid angle of a vertex goes with the cosine of + // its elevation. the bottom ring lies a little under the horizon and is taken as the horizon + float totalweight{0.f}; + for (std::size_t i = 0; i < vertices.size() && i < colors.size(); ++i) + { + auto direction{glm::normalize(vertices[i])}; + // the dome darkens its colours towards the top and brightens the band above the horizon for the looks + // (CSkyDome::RebuildColors), which would make the zenith almost black and the horizon the main source of light. + // both are undone here, so the light comes from the sky the Perez model describes + auto const height{std::max(direction.y, 0.05f)}; + auto const horizonband{std::clamp(1.f - height / 0.2f, 0.f, 1.f)}; + auto const looks{std::clamp(1.f - height * 0.75f, 0.25f, 1.f) * std::lerp(1.f, 1.5f + 0.75f * m_overcast, horizonband)}; + direction.y = std::max(direction.y, 0.f); + direction = glm::normalize(direction); + auto const weight{std::sqrt(std::max(1.f - direction.y * direction.y, 0.f))}; + auto const color{sky_shoulder(glm::pow(glm::max(colors[i] / looks, glm::vec3{0.f}), glm::vec3{2.2f}))}; + auto const basis{sh_basis(direction)}; + for (std::size_t c = 0; c < radiance.size(); ++c) + radiance[c] += color * (basis[c] * weight); + totalweight += weight; + } + if (totalweight <= 0.f) + return {}; + for (auto &coefficient : radiance) + coefficient *= 2.f * PI / totalweight; + + // the ground, lit by the sky and the sun, fills the lower hemisphere with uniform light (see atmosphere_ambient.comp). + // the dome is in its own units: the light of the sky on an upward facing surface is taken to be the ambient light, + // which converts the sunlight to them + auto const up{sh_basis(glm::vec3{0.f, 1.f, 0.f})}; + glm::vec3 skyup{0.f}; + for (std::size_t c = 0; c < radiance.size(); ++c) + skyup += radiance[c] * (SH_IRRADIANCE_BAND[SH_BAND[c]] * up[c]); + auto const tosky{luminance(skyup) / std::max(luminance(Ambient), 1e-4f)}; + auto const groundradiance{Global.gfx_ambient_groundalbedo * (skyup + Sunlight * SHADING_SUN_TO_AMBIENT * std::max(Sunheight, 0.f) * tosky)}; + radiance[0] += groundradiance * (SH_Y00 * 2.f * PI); + radiance[1] += groundradiance * (SH_Y1 * -PI); + + // to the irradiance, scaled so its average over all directions is the ambient light, channel by channel + sh_coefficients directional{}; + glm::vec3 const average{radiance[0] * SH_Y00}; + auto const scale{Ambient / glm::max(average, glm::vec3{1e-4f})}; + for (std::size_t c = 1; c < radiance.size(); ++c) + directional[c] = radiance[c] * SH_IRRADIANCE_BAND[SH_BAND[c]] * scale; + return directional; +} + +void opengl46_sky::update_atmosphere(glm::vec3 const &Sundirection) +{ + // aerosols grow with the turbidity the weather sets (3 clear .. 9 overcast) + auto const turbidity{std::clamp(Global.fTurbidity, 3.f, 9.f)}; + auto const miescale{Global.gfx_atmosphere_haze * (1.f + 0.5f * (turbidity - 3.f))}; + glm::vec4 const medium{miescale, Global.gfx_ambient_groundalbedo, Global.gfx_atmosphere_altitude, 0.f}; + auto const mediumchanged{!m_tables_medium || *m_tables_medium != medium}; + // the sky-view table follows the sun when it moved by more than about a twentieth of a degree + auto const sunchanged{!m_tables_sun || glm::dot(*m_tables_sun, Sundirection) < 0.9999995f}; + if (!mediumchanged && !sunchanged) + return; + + m_atmosphere.mie = glm::vec4{MIE_SCATTERING * miescale, MIE_EXTINCTION * miescale, MIE_SCALE_HEIGHT, MIE_ASYMMETRY}; + m_atmosphere.ground = glm::vec4{glm::vec3{Global.gfx_ambient_groundalbedo}, OZONE_HALF_WIDTH}; + m_atmosphere.sun_direction = glm::vec4{Sundirection, 0.f}; + m_atmosphere.observer_radius = BOTTOM_RADIUS + std::max(Global.gfx_atmosphere_altitude, 0.f) * 0.001f + 0.001f; + m_atmosphere_ubo->update(m_atmosphere); + m_atmosphere_ubo->bind_uniform(); + m_atmosphere_result_ssbo->bind_base(); + + m_atmosphere_timer.begin(); + if (mediumchanged) + { + glBindImageTexture(gl::ATMOSPHERE_TRANSMITTANCE_UNIT, m_transmittance_table, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA16F); + m_transmittance_program->dispatch((gl::ATMOSPHERE_TRANSMITTANCE_WIDTH + 7) / 8, (gl::ATMOSPHERE_TRANSMITTANCE_HEIGHT + 7) / 8); + glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); + + glBindTextureUnit(gl::ATMOSPHERE_TRANSMITTANCE_UNIT, m_transmittance_table); + glBindImageTexture(gl::ATMOSPHERE_MULTISCATTERING_UNIT, m_multiscattering_table, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA16F); + m_multiscattering_program->dispatch(gl::ATMOSPHERE_MULTISCATTERING_SIZE, gl::ATMOSPHERE_MULTISCATTERING_SIZE); + glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); + } + + glBindTextureUnit(gl::ATMOSPHERE_TRANSMITTANCE_UNIT, m_transmittance_table); + glBindTextureUnit(gl::ATMOSPHERE_MULTISCATTERING_UNIT, m_multiscattering_table); + glBindImageTexture(gl::ATMOSPHERE_SKYVIEW_UNIT, m_skyview_table, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA16F); + m_skyview_program->dispatch((gl::ATMOSPHERE_SKYVIEW_WIDTH + 7) / 8, (gl::ATMOSPHERE_SKYVIEW_HEIGHT + 7) / 8); + glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); + + glBindTextureUnit(gl::ATMOSPHERE_SKYVIEW_UNIT, m_skyview_table); + m_ambient_program->dispatch(1); + glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT); + m_atmosphere_timer.end(); + + // the units the passes used are below the ones of the texture cache, which is told they changed + for (GLuint unit : {gl::ATMOSPHERE_TRANSMITTANCE_UNIT, gl::ATMOSPHERE_MULTISCATTERING_UNIT, gl::ATMOSPHERE_SKYVIEW_UNIT}) + glBindTextureUnit(unit, 0); + opengl_texture::reset_unit_cache(); + + m_result_readback.push(*m_atmosphere_result_ssbo); + m_tables_medium = medium; + m_tables_sun = Sundirection; +} + +void opengl46_sky::update(opengl46_light &Sunlight, glm::vec3 &Fogcolor, CSkyDome const &Skydome, glm::vec3 const &Sundirection, float const Sunangle) +{ + // the light the simulation set, which is kept where the atmosphere is not used + glm::vec3 const oldambient{Sunlight.ambient}; + glm::vec3 const olddiffuse{Sunlight.diffuse}; + glm::vec3 const oldfog{Fogcolor}; + m_overcast = std::clamp(Global.Overcast, 0.f, 1.f); + + // atmosphere + m_weight = 0.f; + if (Global.gfx_atmosphere) + { + update_atmosphere(glm::normalize(Sundirection)); + gl::atmosphere_result result; + if (m_result_readback.pop(&result)) + m_result = result; + if (m_result) + m_weight = glm::smoothstep(ATMOSPHERE_FADE_START, ATMOSPHERE_FADE_END, Sunangle); + } + + sh_coefficients directional{}; + if (Global.gfx_ambient_sh) + { + // the dome changes with the sun, the weather and the light the simulation works out from them + std::array const key{glm::vec4{Sundirection, Sunlight.direction.y}, glm::vec4{oldambient, m_overcast}, glm::vec4{olddiffuse, Global.gfx_ambient_groundalbedo}}; + if (!m_dome_key || *m_dome_key != key) + { + m_dome_sh = dome_directional(Skydome, oldambient, olddiffuse, -Sunlight.direction.y); + m_dome_key = key; + } + directional = m_dome_sh; + } + + m_sunlight = olddiffuse; + m_ambient = oldambient; + m_fog = oldfog; + m_sun_disc_color = glm::vec3{1.f}; + if (m_weight > 0.f) + { + auto const &result{*m_result}; + // the camera's white balance: the sun high in the sky is white + auto const balance{1.f / glm::max(glm::vec3{result.zenith_transmittance}, glm::vec3{1e-3f})}; + m_white_balance = balance / std::max(luminance(balance), 1e-4f); + + // the atmosphere has no clouds: the overcast takes away the sunlight, greys the sky and evens out its light + auto const sun{glm::vec3{result.sun_transmittance} * m_white_balance * (ATMOSPHERE_SUN_SCALE * Global.gfx_atmosphere_sun) * (1.f - 0.65f * m_overcast)}; + auto const ambientscale{ATMOSPHERE_AMBIENT_SCALE * Global.gfx_atmosphere_ambient}; + auto const ambient{desaturate(glm::vec3{result.radiance_sh[0]} * SH_Y00 * m_white_balance * ambientscale, 0.5f * m_overcast)}; + m_sky_scale = ATMOSPHERE_SKY_SCALE * Global.gfx_atmosphere_sky; + auto const fog{desaturate(glm::vec3{result.horizon} * m_white_balance * m_sky_scale, 0.5f * m_overcast)}; + m_sky_flat_luminance = luminance(glm::vec3{result.average} * m_white_balance * m_sky_scale); + + m_sunlight = glm::mix(olddiffuse, sun, m_weight); + m_ambient = glm::mix(oldambient, ambient, m_weight); + m_fog = glm::mix(oldfog, fog, m_weight); + auto const sundisc{glm::vec3{result.sun_transmittance} * m_white_balance}; + m_sun_disc_color = sundisc / std::max({sundisc.r, sundisc.g, sundisc.b, 1e-4f}); + + if (Global.gfx_ambient_sh) + { + auto const evenness{1.f - 0.75f * m_overcast}; + for (std::size_t c = 1; c < directional.size(); ++c) + { + auto const atmosphere{desaturate(glm::vec3{result.radiance_sh[c]} * m_white_balance * (SH_IRRADIANCE_BAND[SH_BAND[c]] * ambientscale * evenness), 0.5f * m_overcast)}; + directional[c] = glm::mix(directional[c], atmosphere, m_weight); + } + } + + Sunlight.diffuse = glm::vec4{m_sunlight, Sunlight.diffuse.a}; + Sunlight.ambient = glm::vec4{m_ambient, Sunlight.ambient.a}; + Fogcolor = m_fog; + } + + // directional part of the ambient light for the shading + gl::sky_data skydata{}; + for (std::size_t c = 1; c < directional.size(); ++c) + skydata.ambient_sh[c] = glm::vec4{directional[c] * Global.gfx_ambient_sh_strength, 0.f}; + if (!m_sky_data_uploaded || std::memcmp(&skydata, &m_sky_data, sizeof(skydata)) != 0) + { + m_sky_data = skydata; + m_sky_ssbo->update(m_sky_data); + m_sky_data_uploaded = true; + } +} + +void opengl46_sky::setup_sky(gl::model_ubs &Model) const +{ + glBindTextureUnit(gl::SKY_VIEW_TEX, m_skyview_table); + Model.param[0] = glm::vec4{m_weight, m_sky_scale, 0.75f * m_overcast, m_sky_flat_luminance}; + Model.param[1] = glm::vec4{m_white_balance, 0.f}; +} + +glm::vec3 opengl46_sky::sun_disc_color(glm::vec3 const &Oldcolor) const +{ + return glm::mix(Oldcolor, m_sun_disc_color, m_weight); +} + +void opengl46_sky::prefilter(gl::cubemap const &Source) +{ + if (!m_prefiltered) + { + m_prefiltered = std::make_unique(); + m_prefiltered->alloc_levels(GL_RGBA16F, static_cast(gl::ENVMAP_GGX_SIZE), static_cast(gl::ENVMAP_GGX_LEVELS)); + } + + m_prefilter_timer.begin(); + Source.bind(GL_TEXTURE0 + gl::ENV_TEX); + glActiveTexture(GL_TEXTURE0); + for (std::size_t level = 0; level < gl::ENVMAP_GGX_LEVELS; ++level) + { + auto const size{static_cast(gl::ENVMAP_GGX_SIZE >> level)}; + auto const roughness{gl::ENVMAP_GGX_ROUGHNESS_MIN + (1.f - gl::ENVMAP_GGX_ROUGHNESS_MIN) * static_cast(level) / static_cast(gl::ENVMAP_GGX_LEVELS - 1)}; + glBindImageTexture(0, *m_prefiltered, static_cast(level), GL_TRUE, 0, GL_WRITE_ONLY, GL_RGBA16F); + glProgramUniform1f(*m_prefilter_program, 0, roughness); + glProgramUniform1i(*m_prefilter_program, 1, size); + glProgramUniform1f(*m_prefilter_program, 2, static_cast(gl::ENVMAP_SIZE)); + glProgramUniform1i(*m_prefilter_program, 3, 64); + auto const groups{static_cast((size + gl::ENVMAP_GGX_GROUP_SIZE - 1) / gl::ENVMAP_GGX_GROUP_SIZE)}; + m_prefilter_program->dispatch(groups, groups, 6); + } + glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); + m_prefilter_timer.end(); + glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA16F); + m_prefiltered_ready = true; +} + +std::string opengl46_sky::info_times() +{ + std::string text; + if (auto const time = m_atmosphere_timer.result()) + text += " atmosphere: " + to_string(static_cast(*time) / 1000000.0, 3) + " ms\n"; + if (auto const time = m_prefilter_timer.result()) + text += " envmap ggx: " + to_string(static_cast(*time) / 1000000.0, 3) + " ms\n"; + return text; +} + +void opengl46_sky::draw_debug_ui() +{ + ImGui::Checkbox("directional ambient light (gfx.ambient.sh)", &Global.gfx_ambient_sh); + ImGui::SliderFloat("directional strength", &Global.gfx_ambient_sh_strength, 0.f, 2.f); + ImGui::SliderFloat("ground albedo", &Global.gfx_ambient_groundalbedo, 0.f, 1.f); + ImGui::Checkbox("GGX prefiltered reflections (gfx.envmap.ggx)", &Global.gfx_envmap_ggx); + ImGui::Checkbox("physical atmosphere (gfx.atmosphere)", &Global.gfx_atmosphere); + ImGui::SliderFloat("sky", &Global.gfx_atmosphere_sky, 0.f, 4.f); + ImGui::SliderFloat("ambient", &Global.gfx_atmosphere_ambient, 0.f, 4.f); + ImGui::SliderFloat("sun", &Global.gfx_atmosphere_sun, 0.f, 4.f); + ImGui::SliderFloat("haze", &Global.gfx_atmosphere_haze, 0.f, 8.f); + ImGui::SliderFloat("altitude (m)", &Global.gfx_atmosphere_altitude, 0.f, 3000.f); + ImGui::Text("atmosphere share %.2f%s", m_weight, (Global.gfx_atmosphere && !m_result) ? " (waiting for the first read back)" : ""); + ImGui::Text("sun %.3f %.3f %.3f", m_sunlight.r, m_sunlight.g, m_sunlight.b); + ImGui::Text("ambient %.3f %.3f %.3f", m_ambient.r, m_ambient.g, m_ambient.b); + ImGui::Text("fog %.3f %.3f %.3f", m_fog.r, m_fog.g, m_fog.b); + ImGui::Text("up / down ambient %.3f / %.3f", luminance(m_ambient + glm::vec3{m_sky_data.ambient_sh[1]} * SH_Y1), luminance(m_ambient - glm::vec3{m_sky_data.ambient_sh[1]} * SH_Y1)); +} diff --git a/rendering/opengl46/opengl46sky.h b/rendering/opengl46/opengl46sky.h new file mode 100644 index 00000000..73fc3b92 --- /dev/null +++ b/rendering/opengl46/opengl46sky.h @@ -0,0 +1,128 @@ +/* +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/cubemap.h" +#include "gl/query.h" +#include "gl/readback.h" +#include "rendering/opengl46/opengl46light.h" + +class CSkyDome; + +// light of the sky for the opengl 4.6 renderer: +// - the directional part of the ambient light, as spherical harmonics in sky_ssbo, worked out from the colours of the sky +// dome or from the physical atmosphere. the shading adds it to the uniform ambient light (light_common.glsl), so the +// average of the ambient light stays what the rest of the renderer sets +// - the physical atmosphere (Hillaire 2020, atmosphere_*.comp): look-up tables made in compute passes, the sky drawn from +// them (sky_atmosphere.frag), and the sunlight, ambient light and fog colour read back from them a few frames later. +// these are values for the render only: the simulation (Global.DayLight, fLuminance) keeps its own +// - the environment map prefiltered with the GGX lobe, for rough reflections (envmap_prefilter.comp) +// all of it is switched by Global.gfx_ambient_sh, gfx_atmosphere and gfx_envmap_ggx; off, the renderer works as before +class opengl46_sky +{ + public: + opengl46_sky() = default; + ~opengl46_sky(); + opengl46_sky(const opengl46_sky &) = delete; + opengl46_sky &operator=(const opengl46_sky &) = delete; + + // creates the buffers, tables and programs. throws gl::shader_exception + void init(); + // binds the buffers the shaders read; bindings are context state, so done in every context + void bind(); + // works out the light of the sky for the frame and replaces with it the colours of Sunlight (diffuse, ambient) and + // Fogcolor, which come in as the simulation set them. runs the compute passes, so it is called in the main context, + // before the passes of the frame. Sundirection and Sunangle (degrees) are those of the sun, also when the moon is the key light + void update(opengl46_light &Sunlight, glm::vec3 &Fogcolor, CSkyDome const &Skydome, glm::vec3 const &Sundirection, float const Sunangle); + // share of the atmosphere in the sky of the frame; 0 when the old sky is drawn alone + float atmosphere_weight() const + { + return m_weight; + } + // sets the shader parameters of the sky drawn with the atmosphere (sky_atmosphere.frag) and binds its table + void setup_sky(gl::model_ubs &Model) const; + // colour of the sun disc, the old one blended with the colour the atmosphere gives it + glm::vec3 sun_disc_color(glm::vec3 const &Oldcolor) const; + // prefilters the levels of the GGX map from the environment map, which has to have its mipmaps + void prefilter(gl::cubemap const &Source); + // the prefiltered map, null until it was made once + gl::cubemap const *prefiltered() const + { + return m_prefiltered_ready ? m_prefiltered.get() : nullptr; + } + // lines for the gpu times of the debug panel + std::string info_times(); + // controls of the debug window + void draw_debug_ui(); + + private: + // spherical harmonics of order 2, rgb + using sh_coefficients = std::array; + + // directional part of the ambient light from the colours of the dome, scaled so its average is Ambient + sh_coefficients dome_directional(CSkyDome const &Skydome, glm::vec3 const &Ambient, glm::vec3 const &Sunlight, float const Sunheight) const; + // sets up the parameters of the atmosphere for the frame and runs the passes whose input changed + void update_atmosphere(glm::vec3 const &Sundirection); + // creates an immutable 2d texture for a look-up table + static GLuint create_table(GLsizei Width, GLsizei Height); + + // buffers the shaders read + std::unique_ptr m_sky_ssbo; + std::unique_ptr m_atmosphere_ubo; + std::unique_ptr m_atmosphere_result_ssbo; + gl::sky_data m_sky_data{}; + bool m_sky_data_uploaded{false}; + + // atmosphere + std::unique_ptr m_transmittance_program; + std::unique_ptr m_multiscattering_program; + std::unique_ptr m_skyview_program; + std::unique_ptr m_ambient_program; + GLuint m_transmittance_table{0}; + GLuint m_multiscattering_table{0}; + GLuint m_skyview_table{0}; + gl::atmosphere_ubs m_atmosphere{}; + // inputs the tables were last made for: the transmittance and multiple scattering tables depend on the medium only, + // the sky-view table also on the sun + std::optional m_tables_medium; + std::optional m_tables_sun; + gl::readback m_result_readback{sizeof(gl::atmosphere_result)}; + std::optional m_result; + gl::gpu_timer m_atmosphere_timer; + + // what update() made of the frame, for the sky, the sun disc and the debug window + float m_weight{0.f}; + glm::vec3 m_white_balance{1.f}; + float m_sky_scale{0.f}; + float m_overcast{0.f}; + float m_sky_flat_luminance{0.f}; + glm::vec3 m_sun_disc_color{1.f}; + glm::vec3 m_sunlight{0.f}; + glm::vec3 m_ambient{0.f}; + glm::vec3 m_fog{0.f}; + + // cache of the directional ambient light of the dome, remade when its inputs change + std::optional> m_dome_key; + sh_coefficients m_dome_sh{}; + + // prefiltered environment map + std::unique_ptr m_prefilter_program; + std::unique_ptr m_prefiltered; + bool m_prefiltered_ready{false}; + gl::gpu_timer m_prefilter_timer; +}; diff --git a/rendering/opengl46/opengl46skydome.cpp b/rendering/opengl46/opengl46skydome.cpp index 74a4abe1..cc248ec4 100644 --- a/rendering/opengl46/opengl46skydome.cpp +++ b/rendering/opengl46/opengl46skydome.cpp @@ -19,6 +19,8 @@ void opengl46_skydome::update() { gl::shader vert("vbocolor.vert"); gl::shader frag("color.frag"); m_shader.emplace(std::vector>({vert, frag})); + gl::shader atmospherefrag("sky_atmosphere.frag"); + m_atmosphereshader.emplace(std::vector>({vert, atmospherefrag})); } auto &skydome { simulation::Environment.skydome() }; @@ -55,11 +57,16 @@ void opengl46_skydome::update() { } // render skydome to screen -void opengl46_skydome::render() { +void opengl46_skydome::render( bool const Atmosphere ) { if( ( !m_shader) || ( !m_vao ) ) { return; } - m_shader->bind(); + if( Atmosphere && m_atmosphereshader ) { + m_atmosphereshader->bind(); + } + else { + m_shader->bind(); + } m_vao->bind(); auto const &skydome { simulation::Environment.skydome() }; diff --git a/rendering/opengl46/opengl46skydome.h b/rendering/opengl46/opengl46skydome.h index afb3bd38..1543fab3 100644 --- a/rendering/opengl46/opengl46skydome.h +++ b/rendering/opengl46/opengl46skydome.h @@ -23,8 +23,9 @@ public: // methods // updates data stores on the opengl end. NOTE: unbinds buffers void update(); - // draws the skydome - void render(); + // draws the skydome. with Atmosphere the colours come from the tables of the physical atmosphere (sky_atmosphere.frag), + // which the caller binds along with the parameters of the shader + void render( bool const Atmosphere = false ); private: // members @@ -32,5 +33,6 @@ private: std::optional m_indexbuffer; std::optional m_coloursbuffer; std::optional m_shader; + std::optional m_atmosphereshader; std::optional m_vao; }; diff --git a/shaders/atmosphere_ambient.comp b/shaders/atmosphere_ambient.comp new file mode 100644 index 00000000..a53c964e --- /dev/null +++ b/shaders/atmosphere_ambient.comp @@ -0,0 +1,127 @@ +// what the cpu needs from the atmosphere, worked out from the sky-view table in one work group: the light arriving at the +// observer from every direction as spherical harmonics (the ground below the horizon lit by the sun and the sky), the +// transmittance towards the sun, and the average light of the sky above the horizon and over the whole sky. +// run after the sky-view table; the result is read back by the cpu a few frames later + +#include +#include + +layout(binding = ATMOSPHERE_TRANSMITTANCE_UNIT) uniform sampler2D atmo_transmittance_lut; +layout(binding = ATMOSPHERE_SKYVIEW_UNIT) uniform sampler2D atmo_skyview_lut; + +layout(local_size_x = ATMOSPHERE_AMBIENT_GROUP_SIZE) in; + +layout(std430, binding = ATMOSPHERE_RESULT_SSBO_BINDING) writeonly buffer atmosphere_result_ssbo +{ + vec4 result_radiance_sh[9]; + vec4 result_sun_transmittance; + vec4 result_horizon; + vec4 result_average; + vec4 result_zenith_transmittance; +}; + +// directions of the upper hemisphere each invocation takes +const uint DIRECTIONS_PER_INVOCATION = 4U; +const uint DIRECTIONS = ATMOSPHERE_AMBIENT_GROUP_SIZE * DIRECTIONS_PER_INVOCATION; +// the sky up to this sine of the elevation is the horizon (about 6 degrees) +const float HORIZON_BAND = 0.1; + +// partial sums: the 9 coefficients, the light on an upward facing surface, the horizon and its weight, the sky average +const int SUMS = 12; +shared vec4 shared_sums[ATMOSPHERE_AMBIENT_GROUP_SIZE]; + +void sh_basis(vec3 n, out float basis[9]) +{ + basis[0] = 0.282095; + basis[1] = 0.488603 * n.y; + basis[2] = 0.488603 * n.z; + basis[3] = 0.488603 * n.x; + basis[4] = 1.092548 * n.x * n.y; + basis[5] = 1.092548 * n.y * n.z; + basis[6] = 0.315392 * (3.0 * n.z * n.z - 1.0); + basis[7] = 1.092548 * n.x * n.z; + basis[8] = 0.546274 * (n.x * n.x - n.y * n.y); +} + +// sum of the value of every invocation, valid in invocation 0 +vec4 reduce(vec4 value) +{ + uint index = gl_LocalInvocationID.x; + barrier(); + shared_sums[index] = value; + barrier(); + for (uint stride = ATMOSPHERE_AMBIENT_GROUP_SIZE / 2U; stride > 0U; stride >>= 1) + { + if (index < stride) + shared_sums[index] += shared_sums[index + stride]; + barrier(); + } + return shared_sums[0]; +} + +void main() +{ + uint index = gl_LocalInvocationID.x; + vec3 sundirection = normalize(atmo_sun_direction.xyz); + + vec3 sums[SUMS]; + for (int i = 0; i < SUMS; ++i) + sums[i] = vec3(0.0); + + // even spread over the upper hemisphere: a fibonacci spiral in the sine of the elevation + const float goldenangle = 2.39996323; + const float solidangle = 2.0 * ATMO_PI / float(DIRECTIONS); + for (uint k = 0U; k < DIRECTIONS_PER_INVOCATION; ++k) + { + uint i = index * DIRECTIONS_PER_INVOCATION + k; + float y = (float(i) + 0.5) / float(DIRECTIONS); + float r = sqrt(max(1.0 - y * y, 0.0)); + float azimuth = goldenangle * float(i); + vec3 direction = vec3(r * cos(azimuth), y, r * sin(azimuth)); + + vec3 radiance = textureLod(atmo_skyview_lut, atmo_skyview_uv_world(direction, sundirection), 0.0).rgb; + float basis[9]; + sh_basis(direction, basis); + for (int c = 0; c < 9; ++c) + sums[c] += radiance * (basis[c] * solidangle); + sums[9] += radiance * (y * solidangle); + if (y < HORIZON_BAND) + { + sums[10] += radiance; + sums[11] += vec3(1.0); + } + } + + vec3 total[SUMS]; + for (int i = 0; i < SUMS; ++i) + total[i] = reduce(vec4(sums[i], 0.0)).rgb; + + if (index != 0U) + return; + + // the transmittance towards the sun, zero once the planet is in the way + float radius = atmo_observer_radius; + vec3 origin = vec3(0.0, radius, 0.0); + vec3 suntransmittance = texture(atmo_transmittance_lut, atmo_transmittance_uv(radius, sundirection.y)).rgb; + if (atmo_ray_sphere_nearest(origin, sundirection, atmo_bottom_radius) >= 0.0) + suntransmittance = vec3(0.0); + + // the ground, a lambertian surface lit by the sun and the sky, fills the lower hemisphere with uniform light. its + // spherical harmonics have only the constant term and the vertical one: for a uniform radiance L over the lower + // hemisphere, 0.282095 * 2 pi * L and 0.488603 * (-pi) * L + // the band 0 coefficient of the sky alone over the solid angle of the hemisphere + vec3 skyaverage = total[0] / (0.282095 * 2.0 * ATMO_PI); + vec3 irradiance = suntransmittance * max(sundirection.y, 0.0) + total[9]; + vec3 groundradiance = atmo_ground.rgb / ATMO_PI * irradiance; + + total[0] += groundradiance * (0.282095 * 2.0 * ATMO_PI); + total[1] += groundradiance * (0.488603 * -ATMO_PI); + for (int c = 0; c < 9; ++c) + result_radiance_sh[c] = vec4(total[c], 0.0); + + result_sun_transmittance = vec4(suntransmittance, 0.0); + result_horizon = vec4(total[10] / max(total[11].x, 1.0), 0.0); + result_average = vec4(skyaverage, 0.0); + // what the sun high in the sky looks like at the observer, for the white balance + result_zenith_transmittance = vec4(texture(atmo_transmittance_lut, atmo_transmittance_uv(radius, 1.0)).rgb, 0.0); +} diff --git a/shaders/atmosphere_common.glsl b/shaders/atmosphere_common.glsl new file mode 100644 index 00000000..4ee814a9 --- /dev/null +++ b/shaders/atmosphere_common.glsl @@ -0,0 +1,194 @@ +// physically based sky after S. Hillaire, "A Scalable and Production Ready Sky and Atmosphere Rendering Technique" +// (EGSR 2020): transmittance, multiple scattering and sky-view look-up tables. distances in kilometres, coefficients per +// kilometre. the planet is centred at the origin; the look-up tables are worked out in a frame with z up at the observer, +// the world (y up) is mapped to it by the callers. radiance is that of a sun of illuminance 1 + +layout(std140, binding = ATMOSPHERE_UBO_BINDING) uniform atmosphere_ubo +{ + // rgb: rayleigh scattering, w: rayleigh scale height + vec4 atmo_rayleigh; + // x: mie scattering, y: mie extinction, z: mie scale height, w: mie phase asymmetry + vec4 atmo_mie; + // rgb: ozone absorption, w: altitude of the middle of the ozone layer + vec4 atmo_ozone; + // rgb: albedo of the ground, w: half width of the ozone layer + vec4 atmo_ground; + // world space direction towards the sun (y up) + vec4 atmo_sun_direction; + float atmo_bottom_radius; + float atmo_top_radius; + // distance of the observer from the centre of the planet + float atmo_observer_radius; +}; + +const float ATMO_PI = 3.14159265359; + +struct atmo_medium_s +{ + vec3 scattering; + vec3 extinction; + vec3 rayleigh_scattering; + vec3 mie_scattering; +}; + +atmo_medium_s atmo_sample_medium(float radius) +{ + float height = max(radius - atmo_bottom_radius, 0.0); + float rayleighdensity = exp(-height / atmo_rayleigh.w); + float miedensity = exp(-height / atmo_mie.z); + float ozonedensity = max(0.0, 1.0 - abs(height - atmo_ozone.w) / atmo_ground.w); + + atmo_medium_s medium; + medium.rayleigh_scattering = atmo_rayleigh.rgb * rayleighdensity; + medium.mie_scattering = vec3(atmo_mie.x * miedensity); + medium.scattering = medium.rayleigh_scattering + medium.mie_scattering; + medium.extinction = medium.rayleigh_scattering + vec3(atmo_mie.y * miedensity) + atmo_ozone.rgb * ozonedensity; + return medium; +} + +// distance along a ray (unit direction) to the nearest point ahead of the origin where it meets the sphere, -1 if it does not +float atmo_ray_sphere_nearest(vec3 origin, vec3 direction, float radius) +{ + float b = dot(origin, direction); + float c = dot(origin, origin) - radius * radius; + float discriminant = b * b - c; + if (discriminant < 0.0) + return -1.0; + float root = sqrt(discriminant); + float t0 = -b - root; + float t1 = -b + root; + if (t0 < 0.0 && t1 < 0.0) + return -1.0; + if (t0 < 0.0) + return t1; + if (t1 < 0.0) + return t0; + return min(t0, t1); +} + +float atmo_rayleigh_phase(float cosangle) +{ + return 3.0 / (16.0 * ATMO_PI) * (1.0 + cosangle * cosangle); +} + +// Cornette-Shanks +float atmo_mie_phase(float cosangle) +{ + float g = atmo_mie.w; + float k = 3.0 / (8.0 * ATMO_PI) * (1.0 - g * g) / (2.0 + g * g); + return k * (1.0 + cosangle * cosangle) / pow(max(1.0 + g * g - 2.0 * g * cosangle, 1e-4), 1.5); +} + +// texel centres of a table of the given resolution span [0, 1] of the parameter +float atmo_unit_to_sub_uv(float u, float resolution) +{ + return (u + 0.5 / resolution) * (resolution / (resolution + 1.0)); +} + +float atmo_sub_uv_to_unit(float u, float resolution) +{ + return (u - 0.5 / resolution) * (resolution / (resolution - 1.0)); +} + +// transmittance table: x the distance to the top of the atmosphere along the view, y the height +vec2 atmo_transmittance_uv(float radius, float cosviewzenith) +{ + float H = sqrt(max(0.0, atmo_top_radius * atmo_top_radius - atmo_bottom_radius * atmo_bottom_radius)); + float rho = sqrt(max(0.0, radius * radius - atmo_bottom_radius * atmo_bottom_radius)); + float discriminant = radius * radius * (cosviewzenith * cosviewzenith - 1.0) + atmo_top_radius * atmo_top_radius; + float d = max(0.0, -radius * cosviewzenith + sqrt(max(discriminant, 0.0))); + float dmin = atmo_top_radius - radius; + float dmax = rho + H; + return vec2((d - dmin) / max(dmax - dmin, 1e-6), rho / H); +} + +void atmo_transmittance_params(vec2 uv, out float radius, out float cosviewzenith) +{ + float H = sqrt(max(0.0, atmo_top_radius * atmo_top_radius - atmo_bottom_radius * atmo_bottom_radius)); + float rho = H * uv.y; + radius = sqrt(rho * rho + atmo_bottom_radius * atmo_bottom_radius); + float dmin = atmo_top_radius - radius; + float dmax = rho + H; + float d = dmin + uv.x * (dmax - dmin); + cosviewzenith = (d == 0.0) ? 1.0 : (H * H - rho * rho - d * d) / (2.0 * radius * d); + cosviewzenith = clamp(cosviewzenith, -1.0, 1.0); +} + +// multiple scattering table: x the cosine of the sun zenith angle, y the height +vec2 atmo_multiscattering_uv(float radius, float cossunzenith) +{ + vec2 uv = vec2(cossunzenith * 0.5 + 0.5, (radius - atmo_bottom_radius) / (atmo_top_radius - atmo_bottom_radius)); + return clamp(vec2(atmo_unit_to_sub_uv(uv.x, float(ATMOSPHERE_MULTISCATTERING_SIZE)), atmo_unit_to_sub_uv(uv.y, float(ATMOSPHERE_MULTISCATTERING_SIZE))), 0.0, 1.0); +} + +// sky-view table: x the angle between the view and the sun around the zenith (sqrt spaced), y the angle of the view from +// the zenith, packed towards the horizon, which splits the table in the sky (lower half) and the ground (upper half) +vec2 atmo_skyview_uv(bool intersectsground, float cosviewzenith, float coslightview, float radius) +{ + float vhorizon = sqrt(max(radius * radius - atmo_bottom_radius * atmo_bottom_radius, 0.0)); + float cosbeta = vhorizon / radius; + float beta = acos(clamp(cosbeta, -1.0, 1.0)); + float zenithhorizonangle = ATMO_PI - beta; + + vec2 uv; + if (!intersectsground) + { + float coord = acos(clamp(cosviewzenith, -1.0, 1.0)) / zenithhorizonangle; + coord = 1.0 - coord; + coord = sqrt(max(coord, 0.0)); + coord = 1.0 - coord; + uv.y = coord * 0.5; + } + else + { + float coord = (acos(clamp(cosviewzenith, -1.0, 1.0)) - zenithhorizonangle) / max(beta, 1e-6); + coord = sqrt(max(coord, 0.0)); + uv.y = coord * 0.5 + 0.5; + } + uv.x = sqrt(clamp(-coslightview * 0.5 + 0.5, 0.0, 1.0)); + return vec2(atmo_unit_to_sub_uv(uv.x, float(ATMOSPHERE_SKYVIEW_WIDTH)), atmo_unit_to_sub_uv(uv.y, float(ATMOSPHERE_SKYVIEW_HEIGHT))); +} + +void atmo_skyview_params(vec2 uv, float radius, out float cosviewzenith, out float coslightview) +{ + uv = vec2(atmo_sub_uv_to_unit(uv.x, float(ATMOSPHERE_SKYVIEW_WIDTH)), atmo_sub_uv_to_unit(uv.y, float(ATMOSPHERE_SKYVIEW_HEIGHT))); + uv = clamp(uv, 0.0, 1.0); + + float vhorizon = sqrt(max(radius * radius - atmo_bottom_radius * atmo_bottom_radius, 0.0)); + float cosbeta = vhorizon / radius; + float beta = acos(clamp(cosbeta, -1.0, 1.0)); + float zenithhorizonangle = ATMO_PI - beta; + + if (uv.y < 0.5) + { + float coord = 2.0 * uv.y; + coord = 1.0 - coord; + coord *= coord; + coord = 1.0 - coord; + cosviewzenith = cos(zenithhorizonangle * coord); + } + else + { + float coord = uv.y * 2.0 - 1.0; + coord *= coord; + cosviewzenith = cos(zenithhorizonangle + beta * coord); + } + float coord = uv.x * uv.x; + coslightview = -(coord * 2.0 - 1.0); +} + +// the same mapping for a world space view direction and sun direction (y up), as seen by the observer +vec2 atmo_skyview_uv_world(vec3 viewdirection, vec3 sundirection) +{ + float radius = atmo_observer_radius; + vec3 origin = vec3(0.0, 0.0, radius); + float cosviewzenith = viewdirection.y; + // angle between the view and the sun around the zenith + vec2 viewflat = viewdirection.xz; + vec2 sunflat = sundirection.xz; + float lengths = length(viewflat) * length(sunflat); + float coslightview = (lengths > 1e-6) ? clamp(dot(viewflat, sunflat) / lengths, -1.0, 1.0) : 1.0; + vec3 localview = vec3(sqrt(max(1.0 - cosviewzenith * cosviewzenith, 0.0)), 0.0, cosviewzenith); + bool intersectsground = atmo_ray_sphere_nearest(origin, localview, atmo_bottom_radius) >= 0.0; + return atmo_skyview_uv(intersectsground, cosviewzenith, coslightview, radius); +} diff --git a/shaders/atmosphere_integrate.glsl b/shaders/atmosphere_integrate.glsl new file mode 100644 index 00000000..596ea1ab --- /dev/null +++ b/shaders/atmosphere_integrate.glsl @@ -0,0 +1,105 @@ +// scattering along a ray through the atmosphere, see atmosphere_common.glsl. the includer declares the samplers +// atmo_transmittance_lut and (when multiple scattering is used) atmo_multiscattering_lut + +// above the ground by this much (km), so the test for the shadow of the planet does not hit the point itself +const float ATMO_PLANET_RADIUS_OFFSET = 0.01; + +struct atmo_scattering_s +{ + // light scattered towards the origin + vec3 luminance; + // transmittance from the origin to the end of the ray + vec3 transmittance; + // light scattered once more, as if the light arriving at every point was 1 (multiple scattering table) + vec3 multiscattering_as_1; +}; + +vec3 atmo_transmittance_to_top(float radius, float cossunzenith) +{ + return texture(atmo_transmittance_lut, atmo_transmittance_uv(radius, cossunzenith)).rgb; +} + +// Origin and Direction (unit) in the frame of the planet. with Ground the light the ground reflects at the end of the ray +// is included. Physicalphase: rayleigh and mie phase functions, otherwise isotropic (multiple scattering table). +// Multiscattering: light scattered more than once comes from the multiple scattering table +atmo_scattering_s atmo_integrate(vec3 origin, vec3 direction, vec3 sundirection, bool ground, int samples, bool physicalphase, bool multiscattering) +{ + atmo_scattering_s result; + result.luminance = vec3(0.0); + result.transmittance = vec3(1.0); + result.multiscattering_as_1 = vec3(0.0); + + float tbottom = atmo_ray_sphere_nearest(origin, direction, atmo_bottom_radius); + float ttop = atmo_ray_sphere_nearest(origin, direction, atmo_top_radius); + float tmax; + if (tbottom < 0.0) + { + if (ttop < 0.0) + return result; + tmax = ttop; + } + else + { + tmax = (ttop > 0.0) ? min(ttop, tbottom) : tbottom; + } + + float cosangle = dot(direction, sundirection); + const float uniformphase = 1.0 / (4.0 * ATMO_PI); + float miephase = physicalphase ? atmo_mie_phase(cosangle) : uniformphase; + float rayleighphase = physicalphase ? atmo_rayleigh_phase(cosangle) : uniformphase; + + vec3 throughput = vec3(1.0); + float invsamples = 1.0 / float(samples); + for (int s = 0; s < samples; ++s) + { + // the steps grow with the distance: the medium near the observer matters most + float t0 = float(s) * invsamples; + float t1 = float(s + 1) * invsamples; + t0 = t0 * t0 * tmax; + t1 = t1 * t1 * tmax; + float dt = t1 - t0; + float t = t0 + dt * 0.3; + + vec3 position = origin + t * direction; + float radius = length(position); + vec3 up = position / radius; + atmo_medium_s medium = atmo_sample_medium(radius); + vec3 extinction = max(medium.extinction, vec3(1e-7)); + vec3 sampletransmittance = exp(-extinction * dt); + + float cossunzenith = dot(sundirection, up); + vec3 suntransmittance = atmo_transmittance_to_top(radius, cossunzenith); + float earthshadow = (atmo_ray_sphere_nearest(position + up * ATMO_PLANET_RADIUS_OFFSET, sundirection, atmo_bottom_radius) >= 0.0) ? 0.0 : 1.0; + + vec3 multiscatteredluminance = vec3(0.0); +#ifdef ATMO_MULTISCATTERING_LUT + if (multiscattering) + multiscatteredluminance = texture(atmo_multiscattering_lut, atmo_multiscattering_uv(radius, cossunzenith)).rgb; +#endif + + vec3 phasescattering = medium.mie_scattering * miephase + medium.rayleigh_scattering * rayleighphase; + vec3 S = earthshadow * suntransmittance * phasescattering + multiscatteredluminance * medium.scattering; + // analytic integral over the step of the light scattered at the sample, attenuated on the way to its start + vec3 Sint = (S - S * sampletransmittance) / extinction; + result.luminance += throughput * Sint; + + vec3 MSint = (medium.scattering - medium.scattering * sampletransmittance) / extinction; + result.multiscattering_as_1 += throughput * MSint; + + throughput *= sampletransmittance; + } + + if (ground && tbottom > 0.0 && tmax == tbottom) + { + vec3 position = origin + tbottom * direction; + float radius = length(position); + vec3 up = position / radius; + float cossunzenith = dot(sundirection, up); + vec3 suntransmittance = atmo_transmittance_to_top(radius, cossunzenith); + float NdotL = clamp(dot(up, sundirection), 0.0, 1.0); + result.luminance += suntransmittance * throughput * NdotL * atmo_ground.rgb / ATMO_PI; + } + + result.transmittance = throughput; + return result; +} diff --git a/shaders/atmosphere_multiscattering.comp b/shaders/atmosphere_multiscattering.comp new file mode 100644 index 00000000..9b69abdc --- /dev/null +++ b/shaders/atmosphere_multiscattering.comp @@ -0,0 +1,69 @@ +// light scattered more than once, for the multiple scattering table (Hillaire 2020, section 5.5): for a point at a height +// lit by the sun at an angle, the second order scattering from 64 directions around it, summed as a geometric series of +// all the orders. one work group per texel, one invocation per direction. run after the transmittance table + +#include +#include + +layout(binding = ATMOSPHERE_TRANSMITTANCE_UNIT) uniform sampler2D atmo_transmittance_lut; +#include + +layout(local_size_x = 64) in; + +layout(binding = ATMOSPHERE_MULTISCATTERING_UNIT, rgba16f) writeonly uniform image2D multiscattering_target; + +const int MULTISCATTERING_SAMPLES = 20; +const int SQRT_DIRECTIONS = 8; + +shared vec3 shared_luminance[64]; +shared vec3 shared_multiscattering[64]; + +void main() +{ + ivec2 texel = ivec2(gl_WorkGroupID.xy); + uint index = gl_LocalInvocationID.x; + + vec2 uv = (vec2(texel) + 0.5) / float(ATMOSPHERE_MULTISCATTERING_SIZE); + uv = vec2(atmo_sub_uv_to_unit(uv.x, float(ATMOSPHERE_MULTISCATTERING_SIZE)), atmo_sub_uv_to_unit(uv.y, float(ATMOSPHERE_MULTISCATTERING_SIZE))); + uv = clamp(uv, 0.0, 1.0); + + float cossunzenith = uv.x * 2.0 - 1.0; + vec3 sundirection = vec3(0.0, sqrt(max(1.0 - cossunzenith * cossunzenith, 0.0)), cossunzenith); + float radius = atmo_bottom_radius + clamp(uv.y + 1e-4, 0.0, 1.0) * (atmo_top_radius - atmo_bottom_radius - 1e-3); + vec3 origin = vec3(0.0, 0.0, radius); + + // directions spread evenly over the sphere + float a = (0.5 + float(index / uint(SQRT_DIRECTIONS))) / float(SQRT_DIRECTIONS); + float b = (0.5 + float(index % uint(SQRT_DIRECTIONS))) / float(SQRT_DIRECTIONS); + float theta = 2.0 * ATMO_PI * a; + float phi = acos(clamp(1.0 - 2.0 * b, -1.0, 1.0)); + vec3 direction = vec3(cos(theta) * sin(phi), sin(theta) * sin(phi), cos(phi)); + + atmo_scattering_s result = atmo_integrate(origin, direction, sundirection, true, MULTISCATTERING_SAMPLES, false, false); + + const float spheresolidangle = 4.0 * ATMO_PI; + const float directions = float(SQRT_DIRECTIONS * SQRT_DIRECTIONS); + shared_luminance[index] = result.luminance * spheresolidangle / directions; + shared_multiscattering[index] = result.multiscattering_as_1 * spheresolidangle / directions; + barrier(); + + for (uint stride = 32U; stride > 0U; stride >>= 1) + { + if (index < stride) + { + shared_luminance[index] += shared_luminance[index + stride]; + shared_multiscattering[index] += shared_multiscattering[index + stride]; + } + barrier(); + } + + if (index != 0U) + return; + + const float isotropicphase = 1.0 / spheresolidangle; + vec3 inscatteredluminance = shared_luminance[0] * isotropicphase; + vec3 multiscatteringas1 = shared_multiscattering[0] * isotropicphase; + // every further order of scattering is the previous one times multiscatteringas1 + vec3 luminance = inscatteredluminance / max(vec3(1.0) - multiscatteringas1, vec3(1e-3)); + imageStore(multiscattering_target, texel, vec4(luminance, 1.0)); +} diff --git a/shaders/atmosphere_skyview.comp b/shaders/atmosphere_skyview.comp new file mode 100644 index 00000000..6adb3c62 --- /dev/null +++ b/shaders/atmosphere_skyview.comp @@ -0,0 +1,39 @@ +// light of the sky seen by the observer in every direction, for the sky-view table (atmosphere_common.glsl), which the +// sky, the ambient light and the fog colour are read from. one invocation per texel. run every frame the sky is drawn with +// the atmosphere, after the transmittance and multiple scattering tables + +#include +#include + +layout(binding = ATMOSPHERE_TRANSMITTANCE_UNIT) uniform sampler2D atmo_transmittance_lut; +layout(binding = ATMOSPHERE_MULTISCATTERING_UNIT) uniform sampler2D atmo_multiscattering_lut; +#define ATMO_MULTISCATTERING_LUT +#include + +layout(local_size_x = 8, local_size_y = 8) in; + +layout(binding = ATMOSPHERE_SKYVIEW_UNIT, rgba16f) writeonly uniform image2D skyview_target; + +const int SKYVIEW_SAMPLES = 32; + +void main() +{ + ivec2 texel = ivec2(gl_GlobalInvocationID.xy); + if (texel.x >= int(ATMOSPHERE_SKYVIEW_WIDTH) || texel.y >= int(ATMOSPHERE_SKYVIEW_HEIGHT)) + return; + + vec2 uv = (vec2(texel) + 0.5) / vec2(ATMOSPHERE_SKYVIEW_WIDTH, ATMOSPHERE_SKYVIEW_HEIGHT); + float radius = atmo_observer_radius; + float cosviewzenith; + float coslightview; + atmo_skyview_params(uv, radius, cosviewzenith, coslightview); + + float cossunzenith = clamp(atmo_sun_direction.y, -1.0, 1.0); + vec3 sundirection = normalize(vec3(sqrt(max(1.0 - cossunzenith * cossunzenith, 0.0)), 0.0, cossunzenith)); + float sinviewzenith = sqrt(max(1.0 - cosviewzenith * cosviewzenith, 0.0)); + vec3 direction = vec3(sinviewzenith * coslightview, sinviewzenith * sqrt(max(1.0 - coslightview * coslightview, 0.0)), cosviewzenith); + vec3 origin = vec3(0.0, 0.0, radius); + + atmo_scattering_s result = atmo_integrate(origin, direction, sundirection, false, SKYVIEW_SAMPLES, true, true); + imageStore(skyview_target, texel, vec4(result.luminance, 1.0)); +} diff --git a/shaders/atmosphere_transmittance.comp b/shaders/atmosphere_transmittance.comp new file mode 100644 index 00000000..cbd5fd81 --- /dev/null +++ b/shaders/atmosphere_transmittance.comp @@ -0,0 +1,40 @@ +// transmittance from a point in the atmosphere to its top, for the transmittance table (atmosphere_common.glsl). +// one invocation per texel. run when the parameters of the atmosphere change + +#include +#include + +layout(local_size_x = 8, local_size_y = 8) in; + +layout(binding = ATMOSPHERE_TRANSMITTANCE_UNIT, rgba16f) writeonly uniform image2D transmittance_target; + +const int TRANSMITTANCE_SAMPLES = 40; + +void main() +{ + ivec2 texel = ivec2(gl_GlobalInvocationID.xy); + if (texel.x >= int(ATMOSPHERE_TRANSMITTANCE_WIDTH) || texel.y >= int(ATMOSPHERE_TRANSMITTANCE_HEIGHT)) + return; + + vec2 uv = (vec2(texel) + 0.5) / vec2(ATMOSPHERE_TRANSMITTANCE_WIDTH, ATMOSPHERE_TRANSMITTANCE_HEIGHT); + float radius; + float cosviewzenith; + atmo_transmittance_params(uv, radius, cosviewzenith); + + vec3 origin = vec3(0.0, 0.0, radius); + vec3 direction = vec3(sqrt(max(1.0 - cosviewzenith * cosviewzenith, 0.0)), 0.0, cosviewzenith); + + float tbottom = atmo_ray_sphere_nearest(origin, direction, atmo_bottom_radius); + float ttop = atmo_ray_sphere_nearest(origin, direction, atmo_top_radius); + float tmax = (tbottom > 0.0) ? ((ttop > 0.0) ? min(ttop, tbottom) : tbottom) : max(ttop, 0.0); + + vec3 opticaldepth = vec3(0.0); + float dt = tmax / float(TRANSMITTANCE_SAMPLES); + for (int s = 0; s < TRANSMITTANCE_SAMPLES; ++s) + { + vec3 position = origin + (float(s) + 0.5) * dt * direction; + opticaldepth += atmo_sample_medium(length(position)).extinction * dt; + } + + imageStore(transmittance_target, texel, vec4(exp(-opticaldepth), 1.0)); +} diff --git a/shaders/color.frag b/shaders/color.frag index 2f7fabc5..3f1d9308 100644 --- a/shaders/color.frag +++ b/shaders/color.frag @@ -10,22 +10,7 @@ layout(location = 0) out vec4 out_color; layout(location = 1) out vec4 out_motion; #endif -// the skydome vertex colours carry Perez luminance scaled by a fixed exposure -// (see CSkyDome::SetExposure), so the circumsolar region and the horizon band -// routinely land well above 1.0. hard-clipping them, as this shader used to do, -// flattened everything within ~20 degrees of the sun - and most of the lower sky - -// into one solid white patch. compress the top end instead: below the knee the -// response is unchanged, above it it rolls off towards SKY_SHOULDER_WHITE, so the -// brightness gradient around the sun survives and the patch keeps a soft edge. -const float SKY_SHOULDER_KNEE = 0.25; -const float SKY_SHOULDER_WHITE = 0.95; - -vec3 sky_shoulder(vec3 color) -{ - const float range = SKY_SHOULDER_WHITE - SKY_SHOULDER_KNEE; - vec3 excess = max(color - vec3(SKY_SHOULDER_KNEE), vec3(0.0)); - return min(color, vec3(SKY_SHOULDER_KNEE)) + range * (vec3(1.0) - exp(-excess / range)); -} +#include void main() { diff --git a/shaders/envmap_prefilter.comp b/shaders/envmap_prefilter.comp new file mode 100644 index 00000000..6e638583 --- /dev/null +++ b/shaders/envmap_prefilter.comp @@ -0,0 +1,100 @@ +// prefilters the environment map with the GGX lobe for rough reflections (split sum approximation, Karis 2013), one level +// of the prefiltered map per dispatch. filtered importance sampling (Krivanek, Colbert 2008): each sample reads the mipmap +// of the source whose texels are about as large as the solid angle the sample stands for, so a few dozen samples give a +// smooth result. run when an update of the environment map is complete + +#include + +layout(local_size_x = ENVMAP_GGX_GROUP_SIZE, local_size_y = ENVMAP_GGX_GROUP_SIZE) in; + +layout(binding = 0, rgba16f) writeonly uniform imageCube prefilter_target; +// the plain map with its box filtered mipmaps, on the unit gl::program binds envmap to +uniform samplerCube envmap; + +// perceptual roughness of the level being written +layout(location = 0) uniform float roughness; +// size of the level being written, and of the first level of the source +layout(location = 1) uniform int target_size; +layout(location = 2) uniform float source_size; +layout(location = 3) uniform int sample_count; + +const float PI = 3.14159265359; + +// direction through the centre of a texel of a cube map face, in the order and orientation of GL_TEXTURE_CUBE_MAP_POSITIVE_X.. +vec3 cube_direction(ivec3 texel, int size) +{ + vec2 uv = (vec2(texel.xy) + 0.5) / float(size) * 2.0 - 1.0; + switch (texel.z) + { + case 0: return vec3(1.0, -uv.y, -uv.x); + case 1: return vec3(-1.0, -uv.y, uv.x); + case 2: return vec3(uv.x, 1.0, uv.y); + case 3: return vec3(uv.x, -1.0, -uv.y); + case 4: return vec3(uv.x, -uv.y, 1.0); + default: return vec3(-uv.x, -uv.y, -1.0); + } +} + +vec2 hammersley(uint i, uint n) +{ + uint bits = bitfieldReverse(i); + return vec2(float(i) / float(n), float(bits) * 2.3283064365386963e-10); +} + +// half vector around N for the GGX distribution of linear roughness a +vec3 importance_sample_ggx(vec2 xi, float a, vec3 N) +{ + float phi = 2.0 * PI * xi.x; + float costheta = sqrt((1.0 - xi.y) / (1.0 + (a * a - 1.0) * xi.y)); + float sintheta = sqrt(max(1.0 - costheta * costheta, 0.0)); + vec3 H = vec3(sintheta * cos(phi), sintheta * sin(phi), costheta); + vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); + vec3 tangentx = normalize(cross(up, N)); + vec3 tangenty = cross(N, tangentx); + return tangentx * H.x + tangenty * H.y + N * H.z; +} + +float distribution_ggx(float NdotH, float a) +{ + float a2 = a * a; + float d = NdotH * NdotH * (a2 - 1.0) + 1.0; + return a2 / (PI * d * d); +} + +void main() +{ + ivec3 texel = ivec3(gl_GlobalInvocationID); + if (texel.x >= target_size || texel.y >= target_size) + return; + + vec3 N = normalize(cube_direction(texel, target_size)); + // the view is taken along the normal, which is what the split sum approximation assumes + vec3 V = N; + float a = max(roughness * roughness, 1e-3); + float texelsolidangle = 4.0 * PI / (6.0 * source_size * source_size); + + vec3 sum = vec3(0.0); + float weight = 0.0; + uint count = uint(sample_count); + for (uint i = 0U; i < count; ++i) + { + vec3 H = importance_sample_ggx(hammersley(i, count), a, N); + float VdotH = dot(V, H); + vec3 L = 2.0 * VdotH * H - V; + float NdotL = dot(N, L); + if (NdotL <= 0.0) + continue; + float NdotH = max(dot(N, H), 0.0); + // pdf of L: D * NdotH / (4 * VdotH) + float pdf = distribution_ggx(NdotH, a) * NdotH / max(4.0 * VdotH, 1e-4); + float samplesolidangle = 1.0 / (float(count) * max(pdf, 1e-6)); + float level = max(0.5 * log2(samplesolidangle / texelsolidangle) + 1.0, 0.0); + vec3 color = textureLod(envmap, L, level).rgb; + if (any(isnan(color)) || any(isinf(color))) + color = vec3(0.0); + sum += max(color, vec3(0.0)) * NdotL; + weight += NdotL; + } + + imageStore(prefilter_target, texel, vec4(sum / max(weight, 1e-4), 1.0)); +} diff --git a/shaders/envmapping.glsl b/shaders/envmapping.glsl index 028f2e70..880f6913 100644 --- a/shaders/envmapping.glsl +++ b/shaders/envmapping.glsl @@ -1,5 +1,7 @@ #if ENVMAP_ENABLED uniform samplerCube envmap; +// the same map prefiltered with the GGX lobe (envmap_prefilter.comp), used when env_ggx is 1 +uniform samplerCube envmap_ggx; #endif vec3 envmap_color( vec3 normal ) @@ -39,3 +41,25 @@ vec3 envmap_color_lod(vec3 fragnormal, float lod) return vec3(0.5); // was vec3(0.0), match the non-LOD fallback #endif } + +// reflection of the surroundings in a surface of the given perceptual roughness (0 mirror, 1 matte) +vec3 envmap_color_rough(vec3 fragnormal, float roughness) +{ + // the plain map, its mipmaps (box filtered) standing for the roughness + const float MAX_REFLECTION_LOD = 8.0; + vec3 boxcolor = envmap_color_lod(fragnormal, roughness * MAX_REFLECTION_LOD); +#if ENVMAP_ENABLED + if (env_ggx == 0U || roughness <= ENVMAP_GGX_ROUGHNESS_MIN) + return boxcolor; + vec3 refvec = reflect(f_pos.xyz, fragnormal); + refvec = vec3(inv_view * vec4(refvec, 0.0)); + float level = (roughness - ENVMAP_GGX_ROUGHNESS_MIN) / (1.0 - ENVMAP_GGX_ROUGHNESS_MIN) * float(ENVMAP_GGX_LEVELS - 1U); + vec3 ggxcolor = textureLod(envmap_ggx, refvec, level).rgb; + if (any(isnan(ggxcolor)) || any(isinf(ggxcolor))) ggxcolor = vec3(0.0); + ggxcolor = max(ggxcolor, vec3(0.0)); + // the prefiltered map takes over over the first tenth of the roughness above its lowest level + return mix(boxcolor, ggxcolor, clamp((roughness - ENVMAP_GGX_ROUGHNESS_MIN) * 10.0, 0.0, 1.0)); +#else + return boxcolor; +#endif +} diff --git a/shaders/exposure_average.comp b/shaders/exposure_average.comp new file mode 100644 index 00000000..946d5e9e --- /dev/null +++ b/shaders/exposure_average.comp @@ -0,0 +1,79 @@ +// exposure from the luminance histogram (exposure_histogram.comp): the average log2 luminance of the pixels between the +// low and high percentile (the darkest and brightest parts of the image do not move the exposure), adapted over time to, +// faster towards the brighter. the exposure brings the adapted luminance to the key, within the limits. one work group, +// one invocation per bin; the histogram is cleared for the next frame + +#include + +layout(local_size_x = EXPOSURE_HISTOGRAM_BINS) in; + +layout(std430, binding = EXPOSURE_SSBO_BINDING) buffer exposure_ssbo +{ + uint histogram[EXPOSURE_HISTOGRAM_BINS]; + float adapted_luminance; + float exposure; + float average_luminance; + uint valid; +}; + +// x: log2 of the lowest luminance binned, y: log2 luminance per bin +layout(location = 0) uniform vec2 log_range; +// x: time since the last frame (s), y: rate of adaptation to a darker image, z: to a brighter one (1/s), w: key +layout(location = 1) uniform vec4 adaptation; +// lowest and highest exposure (multipliers) +layout(location = 2) uniform vec2 exposure_range; + +const float LOW_PERCENTILE = 0.4; +const float HIGH_PERCENTILE = 0.95; + +shared float shared_counts[EXPOSURE_HISTOGRAM_BINS]; + +void main() +{ + uint index = gl_LocalInvocationIndex; + shared_counts[index] = float(histogram[index]); + histogram[index] = 0U; + barrier(); + + if (index != 0U) + return; + + float total = 0.0; + for (uint bin = 1U; bin < EXPOSURE_HISTOGRAM_BINS; ++bin) + total += shared_counts[bin]; + // an all black image (or none) leaves the exposure as it was + if (total < 1.0) + return; + + float low = total * LOW_PERCENTILE; + float high = total * HIGH_PERCENTILE; + float counted = 0.0; + float logsum = 0.0; + float weight = 0.0; + for (uint bin = 1U; bin < EXPOSURE_HISTOGRAM_BINS; ++bin) + { + float count = shared_counts[bin]; + // the part of the bin between the percentiles + float inside = max(min(counted + count, high) - max(counted, low), 0.0); + counted += count; + float binluminance = log_range.x + (float(bin - 1U) + 0.5) * log_range.y; + logsum += inside * binluminance; + weight += inside; + } + float averagelog = (weight > 0.0) ? logsum / weight : log_range.x; + average_luminance = exp2(averagelog); + + if (valid == 0U || !(adapted_luminance > 0.0)) + { + adapted_luminance = average_luminance; + valid = 1U; + } + else + { + float adaptedlog = log2(adapted_luminance); + float rate = (averagelog < adaptedlog) ? adaptation.y : adaptation.z; + adaptedlog = mix(adaptedlog, averagelog, 1.0 - exp(-adaptation.x * rate)); + adapted_luminance = exp2(adaptedlog); + } + exposure = clamp(adaptation.w / adapted_luminance, exposure_range.x, exposure_range.y); +} diff --git a/shaders/exposure_histogram.comp b/shaders/exposure_histogram.comp new file mode 100644 index 00000000..981601f3 --- /dev/null +++ b/shaders/exposure_histogram.comp @@ -0,0 +1,49 @@ +// luminance histogram of the image for the automatic exposure: every second pixel in both directions, binned by the log2 +// of its luminance. bin 0 takes the black pixels, which the average leaves out. exposure_average.comp reads and clears it + +#include + +layout(local_size_x = EXPOSURE_HISTOGRAM_GROUP_SIZE, local_size_y = EXPOSURE_HISTOGRAM_GROUP_SIZE) in; + +layout(binding = 0) uniform sampler2D hdr_image; + +layout(std430, binding = EXPOSURE_SSBO_BINDING) buffer exposure_ssbo +{ + uint histogram[EXPOSURE_HISTOGRAM_BINS]; + float adapted_luminance; + float exposure; + float average_luminance; + uint valid; +}; + +// x: log2 of the lowest luminance binned, y: number of bins per unit of log2 luminance +layout(location = 0) uniform vec2 log_range; + +shared uint shared_histogram[EXPOSURE_HISTOGRAM_BINS]; + +void main() +{ + uint index = gl_LocalInvocationIndex; + shared_histogram[index] = 0U; + barrier(); + + ivec2 pixel = ivec2(gl_GlobalInvocationID.xy) * 2; + ivec2 size = textureSize(hdr_image, 0); + if (pixel.x < size.x && pixel.y < size.y) + { + vec3 color = texelFetch(hdr_image, pixel, 0).rgb; + float luminance = dot(color, vec3(0.2126, 0.7152, 0.0722)); + uint bin = 0U; + // also false for a NaN + if (luminance > 1e-6) + { + float position = clamp((log2(luminance) - log_range.x) * log_range.y, 0.0, float(EXPOSURE_HISTOGRAM_BINS - 2U)); + bin = 1U + uint(position); + } + atomicAdd(shared_histogram[bin], 1U); + } + barrier(); + + if (shared_histogram[index] != 0U) + atomicAdd(histogram[index], shared_histogram[index]); +} diff --git a/shaders/light_common.glsl b/shaders/light_common.glsl index 4df80ad9..72da5e63 100644 --- a/shaders/light_common.glsl +++ b/shaders/light_common.glsl @@ -308,12 +308,39 @@ vec3 material_diffuse() return (value > 0.0 ? diffusecolor * (param[1].x / value) : vec3(0.0)); } +// directional part of the sky ambient light (sky_ssbo, bands 1 and 2 of the spherical harmonics) for a world space direction +vec3 sky_ambient_directional(vec3 n) +{ + return sky_ambient_sh[1].rgb * (0.488603 * n.y) + + sky_ambient_sh[2].rgb * (0.488603 * n.z) + + sky_ambient_sh[3].rgb * (0.488603 * n.x) + + sky_ambient_sh[4].rgb * (1.092548 * n.x * n.y) + + sky_ambient_sh[5].rgb * (1.092548 * n.y * n.z) + + sky_ambient_sh[6].rgb * (0.315392 * (3.0 * n.z * n.z - 1.0)) + + sky_ambient_sh[7].rgb * (1.092548 * n.x * n.z) + + sky_ambient_sh[8].rgb * (0.546274 * (n.x * n.x - n.y * n.y)); +} + +// adds the directional part of the sky light to the ambient light the material starts from (the uniform ambient, or a share +// of it, like the water takes). the same share of the directional part is added, so with the directional part at zero +// (gfx.ambient.sh off) the result is what it always was +vec3 add_sky_ambient(vec3 basecolor, vec3 fragnormal) +{ + if (sky_ambient_factor == 0.0) + return basecolor; + float share = dot(basecolor, vec3(1.0)) / max(dot(ambient, vec3(1.0)), 1e-4); + vec3 worldnormal = normalize(mat3(inv_view) * fragnormal); + return max(basecolor + sky_ambient_directional(worldnormal) * (sky_ambient_factor * share), vec3(0.0)); +} + // [0] - diffuse, [1] - specular // do magic here vec3 apply_lights(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float reflectivity, float specularity, float shadowtone) { // Diffuse colour is applied once, to the albedo; the light terms below are not scaled by it again texturecolor *= material_diffuse(); + // the sky lights the faces turned up and towards the bright part of the sky more than the ones turned to the ground + fragcolor = add_sky_ambient(fragcolor, fragnormal); // Scale ambient before it gets tinted by the texture. // Sun, headlights and emission are added afterwards so they are NOT // attenuated by AMBIENT_SCALE - this only dims the indirect term. @@ -326,9 +353,8 @@ vec3 apply_lights(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float refl vec3 F0 = mix(vec3(0.04), texturecolor, metalic); vec3 fresnel = F0 + (1.0 - F0) * pow(1.0 - NdotV, 5.0); - const float MAX_REFLECTION_LOD = 8.0; float env_roughness = 1.0 - clamp(glossiness / max(abs(param[1].w), 1.0), 0.0, 1.0); - vec3 envcolor = envmap_color_lod(fragnormal, env_roughness * MAX_REFLECTION_LOD); + vec3 envcolor = envmap_color_rough(fragnormal, env_roughness); // Pre-integrated env BRDF: roughness/F0/view-dependent specular scale. // Replaces the old raw `fresnel` weighting so matte surfaces stop diff --git a/shaders/mat_sunlessnormalmap_specgloss.frag b/shaders/mat_sunlessnormalmap_specgloss.frag index b19e9e4d..6659c5fd 100644 --- a/shaders/mat_sunlessnormalmap_specgloss.frag +++ b/shaders/mat_sunlessnormalmap_specgloss.frag @@ -49,9 +49,8 @@ vec3 apply_lights_sunless(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, fl vec3 F0 = mix(vec3(0.04), texturecolor, metalic); vec3 fresnel = F0 + (1.0 - F0) * pow(1.0 - NdotV, 5.0); - const float MAX_REFLECTION_LOD = 8.0; float env_roughness = 1.0 - clamp(glossiness / max(abs(param[1].w), 1.0), 0.0, 1.0); - vec3 envcolor = envmap_color_lod(fragnormal, env_roughness * MAX_REFLECTION_LOD); + vec3 envcolor = envmap_color_rough(fragnormal, env_roughness); // Pre-integrated env BRDF (matches apply_lights): roughness-attenuated // reflection so matte cab surfaces no longer mirror the environment. diff --git a/shaders/postfx_bloom_down.frag b/shaders/postfx_bloom_down.frag new file mode 100644 index 00000000..21888efa --- /dev/null +++ b/shaders/postfx_bloom_down.frag @@ -0,0 +1,77 @@ +// one step down the bloom chain: a 13 tap filter (Jimenez, "Next generation post processing in Call of Duty: Advanced +// Warfare", 2014) from a level twice as large. the first step, from the image itself, weighs the groups of taps by their +// brightness (Karis average) so single bright pixels do not flicker, and applies the threshold. +// param[0].x: 1 for the first step, param[0].y: threshold (0: none), param[0].z: soft knee + +in vec2 f_coords; +layout(location = 0) out vec4 out_color; + +#texture (source_tex, 0, RGB) +uniform sampler2D source_tex; + +#include + +vec3 tap(vec2 uv) +{ + vec3 color = texture(source_tex, uv).rgb; + if (any(isnan(color)) || any(isinf(color))) + return vec3(0.0); + return clamp(color, vec3(0.0), vec3(65000.0)); +} + +float karis_weight(vec3 color) +{ + return 1.0 / (1.0 + dot(color, vec3(0.2126, 0.7152, 0.0722))); +} + +void main() +{ + vec2 texel = 1.0 / vec2(textureSize(source_tex, 0)); + vec2 uv = f_coords; + + vec3 a = tap(uv + texel * vec2(-2.0, 2.0)); + vec3 b = tap(uv + texel * vec2(0.0, 2.0)); + vec3 c = tap(uv + texel * vec2(2.0, 2.0)); + vec3 d = tap(uv + texel * vec2(-2.0, 0.0)); + vec3 e = tap(uv); + vec3 f = tap(uv + texel * vec2(2.0, 0.0)); + vec3 g = tap(uv + texel * vec2(-2.0, -2.0)); + vec3 h = tap(uv + texel * vec2(0.0, -2.0)); + vec3 i = tap(uv + texel * vec2(2.0, -2.0)); + vec3 j = tap(uv + texel * vec2(-1.0, 1.0)); + vec3 k = tap(uv + texel * vec2(1.0, 1.0)); + vec3 l = tap(uv + texel * vec2(-1.0, -1.0)); + vec3 m = tap(uv + texel * vec2(1.0, -1.0)); + + vec3 color; + if (param[0].x > 0.5) + { + vec3 g0 = (j + k + l + m) * 0.25; + vec3 g1 = (a + b + d + e) * 0.25; + vec3 g2 = (b + c + e + f) * 0.25; + vec3 g3 = (d + e + g + h) * 0.25; + vec3 g4 = (e + f + h + i) * 0.25; + float w0 = 0.5 * karis_weight(g0); + float w1 = 0.125 * karis_weight(g1); + float w2 = 0.125 * karis_weight(g2); + float w3 = 0.125 * karis_weight(g3); + float w4 = 0.125 * karis_weight(g4); + color = (g0 * w0 + g1 * w1 + g2 * w2 + g3 * w3 + g4 * w4) / (w0 + w1 + w2 + w3 + w4); + + float threshold = param[0].y; + if (threshold > 0.0) + { + // quadratic soft knee below the threshold + float knee = max(threshold * param[0].z, 1e-4); + float brightness = max(color.r, max(color.g, color.b)); + float soft = clamp(brightness - threshold + knee, 0.0, 2.0 * knee); + soft = soft * soft / (4.0 * knee); + color *= max(soft, brightness - threshold) / max(brightness, 1e-4); + } + } + else + { + color = e * 0.125 + (a + c + g + i) * 0.03125 + (b + d + f + h) * 0.0625 + (j + k + l + m) * 0.125; + } + out_color = vec4(color, 1.0); +} diff --git a/shaders/postfx_bloom_up.frag b/shaders/postfx_bloom_up.frag new file mode 100644 index 00000000..160cf52f --- /dev/null +++ b/shaders/postfx_bloom_up.frag @@ -0,0 +1,29 @@ +// one step up the bloom chain: the level below (half as large, with all the levels under it already added) blurred up with +// a 3x3 tent filter and added to this level of the way down + +in vec2 f_coords; +layout(location = 0) out vec4 out_color; + +#texture (lower_tex, 0, RGB) +uniform sampler2D lower_tex; + +#texture (current_tex, 1, RGB) +uniform sampler2D current_tex; + +void main() +{ + vec2 texel = 1.0 / vec2(textureSize(lower_tex, 0)); + vec2 uv = f_coords; + + vec3 sum = texture(lower_tex, uv).rgb * 4.0; + sum += (texture(lower_tex, uv + texel * vec2(0.0, 1.0)).rgb + + texture(lower_tex, uv + texel * vec2(-1.0, 0.0)).rgb + + texture(lower_tex, uv + texel * vec2(1.0, 0.0)).rgb + + texture(lower_tex, uv + texel * vec2(0.0, -1.0)).rgb) * 2.0; + sum += texture(lower_tex, uv + texel * vec2(-1.0, 1.0)).rgb + + texture(lower_tex, uv + texel * vec2(1.0, 1.0)).rgb + + texture(lower_tex, uv + texel * vec2(-1.0, -1.0)).rgb + + texture(lower_tex, uv + texel * vec2(1.0, -1.0)).rgb; + + out_color = vec4(texture(current_tex, uv).rgb + sum / 16.0, 1.0); +} diff --git a/shaders/postfx_tonemapping.frag b/shaders/postfx_tonemapping.frag index 81b3c3f4..c9e076f4 100644 --- a/shaders/postfx_tonemapping.frag +++ b/shaders/postfx_tonemapping.frag @@ -5,11 +5,35 @@ layout(location = 0) out vec4 out_color; #texture (tex1, 0, RGB) uniform sampler2D tex1; +// glow of the bright parts of the image (postfx_bloom_*.frag) +#texture (bloom_tex, 1, RGB) +uniform sampler2D bloom_tex; + +#include + +// exposure worked out by exposure_average.comp +layout(std430, binding = EXPOSURE_SSBO_BINDING) readonly buffer exposure_ssbo +{ + uint histogram[EXPOSURE_HISTOGRAM_BINS]; + float adapted_luminance; + float exposure; + float average_luminance; + uint valid; +}; + #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) void main() { vec2 texcoord = f_coords; vec3 hdr_color = texture(tex1, texcoord).xyz; - out_color = tonemap(vec4(hdr_color, 1.0)); -} + if (param[0].x > 0.0) + hdr_color = mix(hdr_color, texture(bloom_tex, texcoord).rgb * param[0].w, param[0].x); + float exposurescale = param[0].z; + if (param[0].y > 0.5 && exposure > 0.0) + exposurescale *= exposure; + out_color = tonemap(vec4(hdr_color * exposurescale, 1.0)); +} diff --git a/shaders/sky_atmosphere.frag b/shaders/sky_atmosphere.frag new file mode 100644 index 00000000..4556e72f --- /dev/null +++ b/shaders/sky_atmosphere.frag @@ -0,0 +1,54 @@ +// the sky dome drawn with the light of the physical atmosphere (atmosphere_common.glsl): the sky-view table looked up in +// the direction of each fragment. the colour of the old dome (vertex colours, see color.frag) is kept for the night, +// which the atmosphere does not cover (no moon, no stars), and the two are blended by the height of the sun. +// param[0].x: share of the atmosphere, param[0].y: brightness of the atmosphere, param[0].z: how much the overcast flattens +// the sky, param[0].w: luminance the flattened sky tends to. param[1].rgb: white balance of the atmosphere + +in vec3 f_color; +in vec4 f_pos; + +#include +#include +#include +#include +#include + +layout(binding = SKY_VIEW_TEX) uniform sampler2D sky_view_lut; + +layout(location = 0) out vec4 out_color; +#if MOTIONBLUR_ENABLED +layout(location = 1) out vec4 out_motion; +#endif + +// the brightest the sky gets, a guard against the circumsolar peak of the mie scattering blowing up the bloom +const float SKY_RADIANCE_MAX = 64.0; + +void main() +{ + vec3 domecolor = sky_shoulder(pow(max(f_color.rgb, vec3(0.0)), vec3(2.2))); + + // the dome is drawn around the camera with the rotation of the view only, so the view space position is the direction + vec3 direction = normalize(mat3(inv_view) * f_pos.xyz); + // the bottom of the dome reaches a little under the horizon, where the table holds the dark air in front of the ground; + // it gets the light of the horizon instead, as the old dome did + direction = normalize(vec3(direction.x, max(direction.y, 0.002), direction.z)); + vec3 sundirection = normalize(atmo_sun_direction.xyz); + vec3 atmosphere = textureLod(sky_view_lut, atmo_skyview_uv_world(direction, sundirection), 0.0).rgb; + atmosphere *= param[1].rgb * param[0].y; + // an overcast sky is greyer and more even than the clear one the atmosphere describes + vec3 overcast = vec3(param[0].w) * (0.75 + 0.5 * clamp(direction.y, 0.0, 1.0)); + atmosphere = mix(atmosphere, overcast, param[0].z); + atmosphere = min(atmosphere, vec3(SKY_RADIANCE_MAX)); + + vec3 col = mix(domecolor, atmosphere, param[0].x); + if (any(isnan(col)) || any(isinf(col))) + col = domecolor; +#if POSTFX_ENABLED + out_color = vec4(apply_fog(col), 1.0f); +#else + out_color = tonemap(vec4(apply_fog(col), 1.0f)); +#endif +#if MOTIONBLUR_ENABLED + out_motion = vec4(0.0f); +#endif +} diff --git a/shaders/sky_shoulder.glsl b/shaders/sky_shoulder.glsl new file mode 100644 index 00000000..6d045316 --- /dev/null +++ b/shaders/sky_shoulder.glsl @@ -0,0 +1,16 @@ +// the skydome vertex colours carry Perez luminance scaled by a fixed exposure +// (see CSkyDome::SetExposure), so the circumsolar region and the horizon band +// routinely land well above 1.0. hard-clipping them, as this shader used to do, +// flattened everything within ~20 degrees of the sun - and most of the lower sky - +// into one solid white patch. compress the top end instead: below the knee the +// response is unchanged, above it it rolls off towards SKY_SHOULDER_WHITE, so the +// brightness gradient around the sun survives and the patch keeps a soft edge. +const float SKY_SHOULDER_KNEE = 0.25; +const float SKY_SHOULDER_WHITE = 0.95; + +vec3 sky_shoulder(vec3 color) +{ + const float range = SKY_SHOULDER_WHITE - SKY_SHOULDER_KNEE; + vec3 excess = max(color - vec3(SKY_SHOULDER_KNEE), vec3(0.0)); + return min(color, vec3(SKY_SHOULDER_KNEE)) + range * (vec3(1.0) - exp(-excess / range)); +} diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 71995935..c5e3cd21 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1534,6 +1534,10 @@ 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 (ConfigParse_gfx_sky(Parser, Token)) + { + // parsed + } else { tokenparsed = false; @@ -1542,6 +1546,43 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token return tokenparsed; } +bool +global_settings::ConfigParse_gfx_sky( cParser &Parser, std::string_view const Token ) { + + // switches take yes/no (or true/false, 1/0), scales take a number. a missing entry keeps the default + auto const readswitch = [&]( bool &Value ) { + Parser.getTokens( 1 ); + Parser >> Value; }; + auto const readvalue = [&]( float &Value, float const Min, float const Max ) { + Parser.getTokens( 1 ); + Parser >> Value; + Value = std::clamp( Value, Min, Max ); }; + + if( Token == "gfx.ambient.sh" ) { readswitch( gfx_ambient_sh ); } + else if( Token == "gfx.ambient.sh.strength" ) { readvalue( gfx_ambient_sh_strength, 0.f, 2.f ); } + else if( Token == "gfx.ambient.groundalbedo" ) { readvalue( gfx_ambient_groundalbedo, 0.f, 1.f ); } + else if( Token == "gfx.envmap.ggx" ) { readswitch( gfx_envmap_ggx ); } + else if( Token == "gfx.atmosphere" ) { readswitch( gfx_atmosphere ); } + else if( Token == "gfx.atmosphere.sky" ) { readvalue( gfx_atmosphere_sky, 0.f, 16.f ); } + else if( Token == "gfx.atmosphere.ambient" ) { readvalue( gfx_atmosphere_ambient, 0.f, 16.f ); } + else if( Token == "gfx.atmosphere.sun" ) { readvalue( gfx_atmosphere_sun, 0.f, 16.f ); } + else if( Token == "gfx.atmosphere.haze" ) { readvalue( gfx_atmosphere_haze, 0.f, 32.f ); } + else if( Token == "gfx.atmosphere.altitude" ) { readvalue( gfx_atmosphere_altitude, 0.f, 9000.f ); } + else if( Token == "gfx.postfx.autoexposure" ) { readswitch( gfx_postfx_autoexposure ); } + else if( Token == "gfx.postfx.autoexposure.key" ) { readvalue( gfx_postfx_autoexposure_key, 0.001f, 100.f ); } + else if( Token == "gfx.postfx.autoexposure.min" ) { readvalue( gfx_postfx_autoexposure_min, -16.f, 0.f ); } + else if( Token == "gfx.postfx.autoexposure.max" ) { readvalue( gfx_postfx_autoexposure_max, 0.f, 16.f ); } + else if( Token == "gfx.postfx.autoexposure.speed.dark" ) { readvalue( gfx_postfx_autoexposure_speed_dark, 0.01f, 100.f ); } + else if( Token == "gfx.postfx.autoexposure.speed.bright" ) { readvalue( gfx_postfx_autoexposure_speed_bright, 0.01f, 100.f ); } + else if( Token == "gfx.postfx.autoexposure.compensation" ) { readvalue( gfx_postfx_autoexposure_compensation, -16.f, 16.f ); } + 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 { return false; } + + return true; +} + void global_settings::export_as_text( std::ostream &Output ) const { @@ -1778,6 +1819,26 @@ 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.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 ); + export_as_text( Output, "gfx.envmap.ggx", gfx_envmap_ggx ); + export_as_text( Output, "gfx.atmosphere", gfx_atmosphere ); + export_as_text( Output, "gfx.atmosphere.sky", gfx_atmosphere_sky ); + export_as_text( Output, "gfx.atmosphere.ambient", gfx_atmosphere_ambient ); + export_as_text( Output, "gfx.atmosphere.sun", gfx_atmosphere_sun ); + export_as_text( Output, "gfx.atmosphere.haze", gfx_atmosphere_haze ); + export_as_text( Output, "gfx.atmosphere.altitude", gfx_atmosphere_altitude ); + export_as_text( Output, "gfx.postfx.autoexposure", gfx_postfx_autoexposure ); + export_as_text( Output, "gfx.postfx.autoexposure.key", gfx_postfx_autoexposure_key ); + export_as_text( Output, "gfx.postfx.autoexposure.min", gfx_postfx_autoexposure_min ); + export_as_text( Output, "gfx.postfx.autoexposure.max", gfx_postfx_autoexposure_max ); + export_as_text( Output, "gfx.postfx.autoexposure.speed.dark", gfx_postfx_autoexposure_speed_dark ); + export_as_text( Output, "gfx.postfx.autoexposure.speed.bright", gfx_postfx_autoexposure_speed_bright ); + export_as_text( Output, "gfx.postfx.autoexposure.compensation", gfx_postfx_autoexposure_compensation ); + 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, "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 9dd768e7..05b98121 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -302,6 +302,29 @@ struct global_settings { float gfx_shadow_angle_min { -0.2f }; int gfx_shadow_rank_cutoff { 3 }; + // 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) + bool gfx_ambient_sh { true }; // the ambient light depends on where a surface faces: the sky above, the ground below + float gfx_ambient_sh_strength { 1.f }; // 0 = uniform ambient light as before, 1 = full directional difference + float gfx_ambient_groundalbedo { 0.1f }; // how much light the ground below the horizon gives back + bool gfx_envmap_ggx { true }; // rough reflections from the environment map prefiltered with the GGX lobe + bool gfx_atmosphere { true }; // sky, sun, ambient light and fog colour from the physical atmosphere (Hillaire 2020) + float gfx_atmosphere_sky { 1.f }; // brightness of the drawn sky and of the fog + float gfx_atmosphere_ambient { 1.f }; // brightness of the ambient light + float gfx_atmosphere_sun { 1.f }; // brightness of the sunlight + float gfx_atmosphere_haze { 1.f }; // amount of aerosols (mie scattering) on a clear day; grows with the overcast + float gfx_atmosphere_altitude { 200.f }; // height of the scenery above the sea level, in metres + bool gfx_postfx_autoexposure { true }; // the exposure follows the brightness of the image, within limits + float gfx_postfx_autoexposure_key { 0.3f }; // average luminance of the image the exposure is set for + float gfx_postfx_autoexposure_min { -1.f }; // lowest exposure correction, EV (darkening of bright images) + float gfx_postfx_autoexposure_max { 0.5f }; // highest exposure correction, EV (brightening of dark images, tunnels) + float gfx_postfx_autoexposure_speed_dark { 1.f }; // rate of adaptation to a darker image, 1/s + float gfx_postfx_autoexposure_speed_bright { 3.f }; // rate of adaptation to a brighter image, 1/s + float gfx_postfx_autoexposure_compensation { 0.f }; // fixed exposure correction, EV, applied with and without the adaptation + 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 + float ui_fontsize = 13.0f; float ui_scale = 1.0f; bool ui_viewports = true; // panels of the ui can be dragged out of the simulator window, into windows of their own @@ -375,6 +398,8 @@ 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*) + 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 export_as_text( std::ostream &Output ) const;