From c48c58776a5334a12b1c07c52e7a83211769820d Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sun, 25 Oct 2026 03:06:01 +0100 Subject: [PATCH] =?UTF-8?q?Vulkan:=20parity=20with=20GL=20(P1=E2=80=93P7),?= =?UTF-8?q?=20shared=20GPU=20culling=20records=20(N6c),=20threaded=20comma?= =?UTF-8?q?nd=20recording=20and=20render=20thread=20(M1,=20M4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 23 + rendering/common/exposure.cpp | 95 ++ rendering/common/exposure.h | 44 + rendering/common/gpuparticles.cpp | 321 ++++++ rendering/common/gpuparticles.h | 185 +++ rendering/common/grassfield.cpp | 269 +++++ rendering/common/grassfield.h | 133 +++ rendering/common/instancecull.cpp | 389 +++++++ rendering/common/instancecull.h | 303 +++++ rendering/common/localshadows.cpp | 648 +++++++++++ rendering/common/localshadows.h | 288 +++++ rendering/common/scenewalk.h | 216 +++- rendering/common/scenewalkindirect.cpp | 829 ++++++++++++++ rendering/common/shadowfit.cpp | 273 +++++ rendering/common/shadowfit.h | 116 ++ rendering/common/skylight.cpp | 289 +++++ rendering/common/skylight.h | 131 +++ rendering/common/terrainmaterials.cpp | 373 ++++++ rendering/common/terrainmaterials.h | 142 +++ rendering/common/weatherstate.cpp | 343 ++++++ rendering/common/weatherstate.h | 168 +++ rendering/opengl46/opengl46exposure.cpp | 53 +- rendering/opengl46/opengl46gpuparticles.cpp | 336 +----- rendering/opengl46/opengl46gpuparticles.h | 101 +- rendering/opengl46/opengl46grass.cpp | 257 +---- rendering/opengl46/opengl46grass.h | 45 +- rendering/opengl46/opengl46instancecull.cpp | 344 +----- rendering/opengl46/opengl46instancecull.h | 252 +---- rendering/opengl46/opengl46localshadows.cpp | 526 +-------- rendering/opengl46/opengl46localshadows.h | 233 +--- rendering/opengl46/opengl46renderer.cpp | 920 +-------------- rendering/opengl46/opengl46renderer.h | 185 +-- rendering/opengl46/opengl46shadowfit.cpp | 56 +- rendering/opengl46/opengl46shadowfit.h | 45 +- rendering/opengl46/opengl46sky.cpp | 248 +--- rendering/opengl46/opengl46sky.h | 44 +- rendering/opengl46/opengl46terrain.cpp | 461 ++------ rendering/opengl46/opengl46terrain.h | 103 +- rendering/opengl46/opengl46weather.cpp | 333 +----- rendering/opengl46/opengl46weather.h | 55 +- rendering/vulkan14/vulkan14ao.cpp | 214 ++++ rendering/vulkan14/vulkan14bindings.h | 35 +- rendering/vulkan14/vulkan14commands.cpp | 1004 +++++++++++++++++ rendering/vulkan14/vulkan14commands.h | 186 +++ rendering/vulkan14/vulkan14common.cpp | 9 +- rendering/vulkan14/vulkan14common.h | 7 +- rendering/vulkan14/vulkan14compute.cpp | 395 +++++++ rendering/vulkan14/vulkan14compute.h | 156 +++ rendering/vulkan14/vulkan14context.h | 272 ++++- rendering/vulkan14/vulkan14cull.cpp | 604 ++++++++++ rendering/vulkan14/vulkan14cull.h | 196 ++++ rendering/vulkan14/vulkan14debug.cpp | 440 ++++++++ rendering/vulkan14/vulkan14descriptors.cpp | 22 +- rendering/vulkan14/vulkan14descriptors.h | 8 +- rendering/vulkan14/vulkan14frame.cpp | 1066 ++++++++++++++---- rendering/vulkan14/vulkan14grass.cpp | 157 +++ rendering/vulkan14/vulkan14grass.h | 60 + rendering/vulkan14/vulkan14imgui.cpp | 15 +- rendering/vulkan14/vulkan14imgui.h | 2 +- rendering/vulkan14/vulkan14indirect.cpp | 822 ++++++++++++++ rendering/vulkan14/vulkan14lamps.cpp | 359 ++++++ rendering/vulkan14/vulkan14memory.cpp | 21 +- rendering/vulkan14/vulkan14memory.h | 2 +- rendering/vulkan14/vulkan14particles.cpp | 342 ++++++ rendering/vulkan14/vulkan14particles.h | 105 ++ rendering/vulkan14/vulkan14passes.cpp | 31 +- rendering/vulkan14/vulkan14passes.h | 6 +- rendering/vulkan14/vulkan14renderer.cpp | 318 +++++- rendering/vulkan14/vulkan14renderer.h | 166 +++ rendering/vulkan14/vulkan14scene.cpp | 748 ++++++++++-- rendering/vulkan14/vulkan14shaders.cpp | 94 +- rendering/vulkan14/vulkan14shaders.h | 9 +- rendering/vulkan14/vulkan14shadows.cpp | 167 +++ rendering/vulkan14/vulkan14shadows.h | 113 ++ rendering/vulkan14/vulkan14sky.cpp | 482 ++++++++ rendering/vulkan14/vulkan14swapchain.cpp | 11 +- rendering/vulkan14/vulkan14swapchain.h | 2 +- rendering/vulkan14/vulkan14terrain.cpp | 361 ++++++ rendering/vulkan14/vulkan14terrain.h | 85 ++ rendering/vulkan14/vulkan14texture.cpp | 48 +- rendering/vulkan14/vulkan14texture.h | 11 +- rendering/vulkan14/vulkan14weather.cpp | 665 +++++++++++ shaders/vk14/apply_fog.glsl | 21 +- shaders/vk14/atmosphere_ambient.comp | 130 +++ shaders/vk14/atmosphere_common.glsl | 194 ++++ shaders/vk14/atmosphere_integrate.glsl | 105 ++ shaders/vk14/atmosphere_multiscattering.comp | 72 ++ shaders/vk14/atmosphere_skyview.comp | 42 + shaders/vk14/atmosphere_transmittance.comp | 43 + shaders/vk14/clouds_common.glsl | 291 +++++ shaders/vk14/clouds_march.comp | 295 +++++ shaders/vk14/clouds_noise.comp | 141 +++ shaders/vk14/clouds_shadow.comp | 34 + shaders/vk14/clouds_temporal.comp | 72 ++ shaders/vk14/cluster_heatmap.frag | 48 + shaders/vk14/common.glsl | 120 +- shaders/vk14/depthprepass.frag | 10 + shaders/vk14/depthprepass.vert | 26 + shaders/vk14/depthprepass_alpha.frag | 18 + shaders/vk14/envmap_prefilter.comp | 107 ++ shaders/vk14/envmapping.glsl | 25 +- shaders/vk14/exposure_average.comp | 86 ++ shaders/vk14/exposure_histogram.comp | 56 + shaders/vk14/fog_horizon.comp | 49 + shaders/vk14/fog_inject.comp | 323 ++++++ shaders/vk14/fog_integrate.comp | 91 ++ shaders/vk14/fog_volume.glsl | 73 ++ shaders/vk14/grass.frag | 49 + shaders/vk14/grass.vert | 247 ++++ shaders/vk14/gtao_denoise.comp | 58 + shaders/vk14/gtao_main.comp | 160 +++ shaders/vk14/hiz_build.comp | 46 + shaders/vk14/instance_cull.comp | 291 +++++ shaders/vk14/instance_cull_commands.comp | 69 ++ shaders/vk14/instance_cull_offsets.comp | 62 + shaders/vk14/light_common.glsl | 231 +++- shaders/vk14/mat_terrain.frag | 4 + shaders/vk14/mat_terrain.glsl | 158 +++ shaders/vk14/mat_terrain_holes.frag | 12 + shaders/vk14/particle_common.glsl | 96 ++ shaders/vk14/particle_light.glsl | 147 +++ shaders/vk14/particle_soft.glsl | 24 + shaders/vk14/particles_precipitation.comp | 84 ++ shaders/vk14/particles_precipitation.frag | 54 + shaders/vk14/particles_precipitation.vert | 61 + shaders/vk14/particles_smoke.frag | 50 + shaders/vk14/particles_smoke.vert | 40 + shaders/vk14/particles_smoke_prepare.comp | 65 ++ shaders/vk14/particles_smoke_simulate.comp | 102 ++ shaders/vk14/postfx_bloom_down.frag | 80 ++ shaders/vk14/postfx_bloom_up.frag | 33 + shaders/vk14/postfx_ssao_apply.frag | 19 +- shaders/vk14/postfx_tonemapping.frag | 5 +- shaders/vk14/quad.vert | 3 + shaders/vk14/shadow_fit_bounds.comp | 133 +++ shaders/vk14/shadow_fit_depth.comp | 72 ++ shaders/vk14/shadowcube.vert | 31 + shaders/vk14/simpleuv.vert | 4 + shaders/vk14/sky_atmosphere.frag | 63 ++ shaders/vk14/sky_weather.frag | 64 ++ shaders/vk14/stars.frag | 35 + shaders/vk14/stars.vert | 43 + shaders/vk14/vertex.vert | 4 + shaders/vk14/weather_common.glsl | 172 +++ utilities/Globals.cpp | 15 + utilities/Globals.h | 5 + 146 files changed, 20748 insertions(+), 4606 deletions(-) create mode 100644 rendering/common/exposure.cpp create mode 100644 rendering/common/exposure.h create mode 100644 rendering/common/gpuparticles.cpp create mode 100644 rendering/common/gpuparticles.h create mode 100644 rendering/common/grassfield.cpp create mode 100644 rendering/common/grassfield.h create mode 100644 rendering/common/instancecull.cpp create mode 100644 rendering/common/instancecull.h create mode 100644 rendering/common/localshadows.cpp create mode 100644 rendering/common/localshadows.h create mode 100644 rendering/common/scenewalkindirect.cpp create mode 100644 rendering/common/shadowfit.cpp create mode 100644 rendering/common/shadowfit.h create mode 100644 rendering/common/skylight.cpp create mode 100644 rendering/common/skylight.h create mode 100644 rendering/common/terrainmaterials.cpp create mode 100644 rendering/common/terrainmaterials.h create mode 100644 rendering/common/weatherstate.cpp create mode 100644 rendering/common/weatherstate.h create mode 100644 rendering/vulkan14/vulkan14ao.cpp create mode 100644 rendering/vulkan14/vulkan14commands.cpp create mode 100644 rendering/vulkan14/vulkan14commands.h create mode 100644 rendering/vulkan14/vulkan14compute.cpp create mode 100644 rendering/vulkan14/vulkan14compute.h create mode 100644 rendering/vulkan14/vulkan14cull.cpp create mode 100644 rendering/vulkan14/vulkan14cull.h create mode 100644 rendering/vulkan14/vulkan14debug.cpp create mode 100644 rendering/vulkan14/vulkan14grass.cpp create mode 100644 rendering/vulkan14/vulkan14grass.h create mode 100644 rendering/vulkan14/vulkan14indirect.cpp create mode 100644 rendering/vulkan14/vulkan14lamps.cpp create mode 100644 rendering/vulkan14/vulkan14particles.cpp create mode 100644 rendering/vulkan14/vulkan14particles.h create mode 100644 rendering/vulkan14/vulkan14shadows.cpp create mode 100644 rendering/vulkan14/vulkan14shadows.h create mode 100644 rendering/vulkan14/vulkan14sky.cpp create mode 100644 rendering/vulkan14/vulkan14terrain.cpp create mode 100644 rendering/vulkan14/vulkan14terrain.h create mode 100644 rendering/vulkan14/vulkan14weather.cpp create mode 100644 shaders/vk14/atmosphere_ambient.comp create mode 100644 shaders/vk14/atmosphere_common.glsl create mode 100644 shaders/vk14/atmosphere_integrate.glsl create mode 100644 shaders/vk14/atmosphere_multiscattering.comp create mode 100644 shaders/vk14/atmosphere_skyview.comp create mode 100644 shaders/vk14/atmosphere_transmittance.comp create mode 100644 shaders/vk14/clouds_common.glsl create mode 100644 shaders/vk14/clouds_march.comp create mode 100644 shaders/vk14/clouds_noise.comp create mode 100644 shaders/vk14/clouds_shadow.comp create mode 100644 shaders/vk14/clouds_temporal.comp create mode 100644 shaders/vk14/cluster_heatmap.frag create mode 100644 shaders/vk14/depthprepass.frag create mode 100644 shaders/vk14/depthprepass.vert create mode 100644 shaders/vk14/depthprepass_alpha.frag create mode 100644 shaders/vk14/envmap_prefilter.comp create mode 100644 shaders/vk14/exposure_average.comp create mode 100644 shaders/vk14/exposure_histogram.comp create mode 100644 shaders/vk14/fog_horizon.comp create mode 100644 shaders/vk14/fog_inject.comp create mode 100644 shaders/vk14/fog_integrate.comp create mode 100644 shaders/vk14/fog_volume.glsl create mode 100644 shaders/vk14/grass.frag create mode 100644 shaders/vk14/grass.vert create mode 100644 shaders/vk14/gtao_denoise.comp create mode 100644 shaders/vk14/gtao_main.comp create mode 100644 shaders/vk14/hiz_build.comp create mode 100644 shaders/vk14/instance_cull.comp create mode 100644 shaders/vk14/instance_cull_commands.comp create mode 100644 shaders/vk14/instance_cull_offsets.comp create mode 100644 shaders/vk14/mat_terrain.frag create mode 100644 shaders/vk14/mat_terrain.glsl create mode 100644 shaders/vk14/mat_terrain_holes.frag create mode 100644 shaders/vk14/particle_common.glsl create mode 100644 shaders/vk14/particle_light.glsl create mode 100644 shaders/vk14/particle_soft.glsl create mode 100644 shaders/vk14/particles_precipitation.comp create mode 100644 shaders/vk14/particles_precipitation.frag create mode 100644 shaders/vk14/particles_precipitation.vert create mode 100644 shaders/vk14/particles_smoke.frag create mode 100644 shaders/vk14/particles_smoke.vert create mode 100644 shaders/vk14/particles_smoke_prepare.comp create mode 100644 shaders/vk14/particles_smoke_simulate.comp create mode 100644 shaders/vk14/postfx_bloom_down.frag create mode 100644 shaders/vk14/postfx_bloom_up.frag create mode 100644 shaders/vk14/shadow_fit_bounds.comp create mode 100644 shaders/vk14/shadow_fit_depth.comp create mode 100644 shaders/vk14/shadowcube.vert create mode 100644 shaders/vk14/sky_atmosphere.frag create mode 100644 shaders/vk14/sky_weather.frag create mode 100644 shaders/vk14/stars.frag create mode 100644 shaders/vk14/stars.vert create mode 100644 shaders/vk14/weather_common.glsl diff --git a/CMakeLists.txt b/CMakeLists.txt index 872e3b25..01ec878a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -310,7 +310,17 @@ set(SOURCES "gl/glsl_common.cpp" "rendering/common/shadersource.cpp" "rendering/common/scenewalk.cpp" +"rendering/common/scenewalkindirect.cpp" +"rendering/common/instancecull.cpp" +"rendering/common/shadowfit.cpp" +"rendering/common/localshadows.cpp" +"rendering/common/terrainmaterials.cpp" +"rendering/common/grassfield.cpp" +"rendering/common/gpuparticles.cpp" "rendering/common/locallights.cpp" +"rendering/common/skylight.cpp" +"rendering/common/weatherstate.cpp" +"rendering/common/exposure.cpp" "gl/buffer.cpp" "gl/fence.cpp" "gl/pbo.cpp" @@ -430,6 +440,7 @@ if (WITH_VULKAN) "rendering/vulkan14/vulkan14vma.cpp" "rendering/vulkan14/vulkan14shaders.cpp" "rendering/vulkan14/vulkan14descriptors.cpp" + "rendering/vulkan14/vulkan14compute.cpp" "rendering/vulkan14/vulkan14passes.cpp" "rendering/vulkan14/vulkan14texture.cpp" "rendering/vulkan14/vulkan14imgui.cpp" @@ -437,6 +448,18 @@ if (WITH_VULKAN) "rendering/vulkan14/vulkan14geometry.cpp" "rendering/vulkan14/vulkan14scene.cpp" "rendering/vulkan14/vulkan14frame.cpp" + "rendering/vulkan14/vulkan14sky.cpp" + "rendering/vulkan14/vulkan14weather.cpp" + "rendering/vulkan14/vulkan14ao.cpp" + "rendering/vulkan14/vulkan14cull.cpp" + "rendering/vulkan14/vulkan14indirect.cpp" + "rendering/vulkan14/vulkan14shadows.cpp" + "rendering/vulkan14/vulkan14lamps.cpp" + "rendering/vulkan14/vulkan14terrain.cpp" + "rendering/vulkan14/vulkan14grass.cpp" + "rendering/vulkan14/vulkan14particles.cpp" + "rendering/vulkan14/vulkan14debug.cpp" + "rendering/vulkan14/vulkan14commands.cpp" "ref/volk/volk.c") set(DEFINITIONS ${DEFINITIONS} "WITH_VULKAN") diff --git a/rendering/common/exposure.cpp b/rendering/common/exposure.cpp new file mode 100644 index 00000000..e8dc3c12 --- /dev/null +++ b/rendering/common/exposure.cpp @@ -0,0 +1,95 @@ +/* +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/common/exposure.h" + +#include "gl/ubo.h" +#include "utilities/Globals.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}; +// soft knee of the threshold, a fraction of it +float const BLOOM_KNEE{0.5f}; + +} // namespace + +namespace gfx +{ + +glm::vec2 exposure_settings::histogram_range() +{ + auto const bins{static_cast(gl::EXPOSURE_HISTOGRAM_BINS - 2)}; + return glm::vec2{HISTOGRAM_LOG_MIN, bins / (HISTOGRAM_LOG_MAX - HISTOGRAM_LOG_MIN)}; +} + +glm::vec2 exposure_settings::average_range() +{ + auto const bins{static_cast(gl::EXPOSURE_HISTOGRAM_BINS - 2)}; + return glm::vec2{HISTOGRAM_LOG_MIN, (HISTOGRAM_LOG_MAX - HISTOGRAM_LOG_MIN) / bins}; +} + +glm::vec4 exposure_settings::adaptation(double const Deltatime) +{ + return glm::vec4{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}; +} + +glm::vec2 exposure_settings::exposure_range() +{ + return glm::vec2{std::exp2(Global.gfx_postfx_autoexposure_min), std::exp2(Global.gfx_postfx_autoexposure_max)}; +} + +std::vector exposure_settings::bloom_sizes(int const Width, int const Height) +{ + std::vector sizes; + 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; + sizes.push_back(size); + } + return sizes; +} + +glm::vec4 exposure_settings::bloom_params(std::size_t const Level) +{ + return glm::vec4{(Level == 0 ? 1.f : 0.f), Global.gfx_postfx_bloom_threshold, BLOOM_KNEE, 0.f}; +} + +glm::vec4 exposure_settings::tonemap_params(std::size_t const Bloomlevels) +{ + 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}; +} + +void exposure_settings::draw_debug_ui(float const *Readback) +{ + 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)", Readback[2], Readback[0], Readback[1], std::log2(std::max(Readback[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); +} + +} // namespace gfx diff --git a/rendering/common/exposure.h b/rendering/common/exposure.h new file mode 100644 index 00000000..54269074 --- /dev/null +++ b/rendering/common/exposure.h @@ -0,0 +1,44 @@ +/* +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/. +*/ + +// what the automatic exposure and the bloom of the renderers share (the part of the stage S3 of +// claude/gl46-renderer-features-plan.md which isn't the work of the gpu, taken out of opengl46_exposure for the vulkan renderer, +// stage P1 of claude/vulkan14-renderer-plan.md): the parameters of the histogram and of the adaptation +// (exposure_histogram.comp, exposure_average.comp), the sizes of the levels of the bloom chain and the parameters of its steps +// (postfx_bloom_*.frag), the parameters of the tonemapping and the controls of the debug window + +#pragma once + +#include +#include + +namespace gfx +{ + +struct exposure_settings +{ + // x: log2 of the lowest luminance binned, y: bins per unit of log2 luminance (exposure_histogram.comp) + static glm::vec2 histogram_range(); + // x: log2 of the lowest luminance binned, y: log2 luminance per bin (exposure_average.comp) + static glm::vec2 average_range(); + // x: time since the last measurement (Deltatime, s), y, z: rates of the adaptation to a darker and a brighter image, w: key + static glm::vec4 adaptation(double const Deltatime); + // the lowest and the highest exposure (multipliers) + static glm::vec2 exposure_range(); + // the sizes of the levels of the bloom chain of an image, half the size each, as many as there are (empty: no chain) + static std::vector bloom_sizes(int const Width, int const Height); + // param[0] of a step down the chain (postfx_bloom_down.frag): Level 0 is the first one, from the image itself + static glm::vec4 bloom_params(std::size_t const Level); + // parameters of the tonemapping (postfx_tonemapping.frag), with bloom from a chain of Bloomlevels levels (0: none) + static glm::vec4 tonemap_params(std::size_t const Bloomlevels); + // controls of the debug window; Readback: the adapted luminance, the exposure and the average luminance the gpu worked out + static void draw_debug_ui(float const *Readback); +}; + +} // namespace gfx diff --git a/rendering/common/gpuparticles.cpp b/rendering/common/gpuparticles.cpp new file mode 100644 index 00000000..d0c6243c --- /dev/null +++ b/rendering/common/gpuparticles.cpp @@ -0,0 +1,321 @@ +/* +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/common/gpuparticles.h" + +#include "rendering/particles.h" +#include "simulation/simulationenvironment.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/Timer.h" + +namespace gfx +{ + +namespace +{ + +// places of the smoke at most, all sources together (48 bytes each, as many again for the draw); the sources past it get none +std::uint32_t constexpr MAX_SMOKE_PLACES{1u << 19}; +// the origin of the smoke moves by whole steps when the camera gets this far from it +double constexpr ORIGIN_STEP{256.0}; +double constexpr ORIGIN_REACH{1024.0}; +// how much the smoke scatters the light onwards (Henyey-Greenstein) +float constexpr SMOKE_FORWARD{0.3f}; + +// the boxes of the rain and snow around the camera: their sizes (metres) and the shares of the particles in each +std::array constexpr BOX_SIZE{10.f, 24.f, 60.f}; +std::array constexpr BOX_SHARE{0.2f, 0.35f, 0.45f}; + +// what the rain and the snow are like: the speed of the fall (m/s), how long a streak lasts (s, the eye's or the camera's +// exposure), opacity, how much a drop or a flake scatters the light onwards and how bright it is against a white surface +struct precipitation_kind +{ + float fall; + float exposure; + float opacity; + float forward; + float brightness; +}; +precipitation_kind constexpr RAIN{7.f, 0.025f, 0.5f, 0.6f, 0.6f}; +precipitation_kind constexpr SNOW{1.1f, 0.01f, 0.85f, 0.25f, 0.9f}; +// the share of the wind of the environment the drops and flakes drift with (as the old cylinder leant with it) +double constexpr WIND_SHARE{0.5}; +// the camera faster than this from a frame to the next was moved, not driven or flown +double constexpr CAMERA_JUMP{120.0}; + +} // namespace + +bool gpu_particles::init() +{ + if (m_state != 0) + return m_state > 0; + m_state = -1; + if (false == gpu_init()) + return false; + m_state = 1; + return true; +} + +bool gpu_particles::smoke_available() +{ + return init(); +} + +bool gpu_particles::precipitation_available() +{ + return init(); +} + +void gpu_particles::clear() +{ + m_pool.clear(); + m_requested.clear(); + m_poolfull = false; + m_sourcerecords.clear(); + m_places = 0; + m_storage = false; + gpu_clear(); + m_precipitationcapacity = 0; + m_originset = false; + m_lastupdate = 0; +} + +void gpu_particles::layout(particle_manager &Particles) +{ + auto const &sources{Particles.sequence()}; + // the sources already laid out which asked for the same places since + std::size_t kept{0}; + while ((kept < m_requested.size()) && (kept < sources.size()) && (m_requested[kept] == sources[kept].max_particles())) + ++kept; + if ((kept == m_requested.size()) && (kept == sources.size()) && m_storage) + return; + if (kept < m_requested.size()) + { + // a source went away or changed: all anew + kept = 0; + m_pool.clear(); + m_requested.clear(); + m_places = 0; + m_poolfull = false; + } + auto const oldplaces{m_places}; + + auto first{m_places}; + m_pool.resize(sources.size()); + m_requested.resize(sources.size()); + for (std::size_t i = kept; i < sources.size(); ++i) + { + m_requested[i] = sources[i].max_particles(); + auto const size{static_cast(std::min(m_requested[i], MAX_SMOKE_PLACES))}; + m_pool[i] = pool_source{}; + if (first + size > MAX_SMOKE_PLACES) + { + if (false == m_poolfull) + WriteLog("particles: the smoke of some sources left out, over " + std::to_string(MAX_SMOKE_PLACES) + " places of particles"); + m_poolfull = true; + continue; + } + m_pool[i].first = first; + m_pool[i].size = size; + first += size; + } + m_places = first; + + std::vector owners(std::max(m_places, 1), 0); + for (std::size_t i = 0; i < m_pool.size(); ++i) + std::fill_n(owners.begin() + m_pool[i].first, m_pool[i].size, static_cast(i)); + + // the particles of the places kept go over to the new storage, the new places are dead + auto const places{std::max(m_places, 1)}; + gpu_layout(places, (m_storage && (oldplaces > 0) && (kept > 0)) ? oldplaces : 0, owners, sources.size()); + m_storage = true; + m_sourcerecords.resize(sources.size(), gl::particle_smoke_source{}); +} + +void gpu_particles::simulate_smoke(particle_manager &Particles, glm::dvec3 const &Camera) +{ + if (false == init()) + return; + layout(Particles); + if (Particles.updates() == m_lastupdate) + return; + m_lastupdate = Particles.updates(); + if (m_places == 0) + return; + + // the origin of the positions near the camera; when it moves, the particles move by as much the other way + glm::vec3 shift{0.f}; + auto const snapped{glm::floor(Camera / ORIGIN_STEP) * ORIGIN_STEP}; + if (false == m_originset) + { + m_origin = snapped; + m_originset = true; + } + else if (glm::length(Camera - m_origin) > ORIGIN_REACH) + { + shift = glm::vec3{m_origin - snapped}; + m_origin = snapped; + } + + // the light of the smoke as the cpu works it out, for gfx.smoke.lighting off + auto const oldlight{(glm::vec3{Global.DayLight.ambient} + 0.35f * glm::vec3{Global.DayLight.diffuse}) * simulation::Environment.light_intensity()}; + auto const &sources{Particles.sequence()}; + m_smokealive = 0; + for (std::size_t i = 0; i < sources.size(); ++i) + { + auto const &state{sources[i].gpu()}; + auto &pool{m_pool[i]}; + auto &record{m_sourcerecords[i]}; + auto const spawns{(pool.size > 0) ? std::min(state.spawns, pool.size) : 0u}; + record.pool = glm::uvec4{pool.first, pool.size, pool.head, spawns}; + record.location = glm::vec4{glm::vec3{state.location - m_origin}, 0.f}; + record.previous = glm::vec4{glm::vec3{state.previous - m_origin}, 0.f}; + record.launch = glm::vec4{state.inclination[0], state.inclination[1], state.velocity[0], state.velocity[1]}; + record.initial = glm::vec4{state.size[0], state.size[1], state.opacity[0], state.opacity[1]}; + record.sizechange = glm::vec4{state.sizechange[0], state.sizechange[1], state.sizechange[2], state.spread}; + record.opacitychange = glm::vec4{state.opacitychange[0], state.opacitychange[1], state.opacitychange[2], 0.f}; + record.motion = glm::vec4{state.rise, state.damping, state.risemin, 0.f}; + record.settling = glm::vec4{state.settling, state.drag, state.settles ? 1.f : 0.f, state.fadein}; + record.color = glm::vec4{glm::clamp(state.color, glm::vec3{0.f}, glm::vec3{1.f}), 0.f}; + record.oldcolor = glm::vec4{glm::clamp(state.color * oldlight, glm::vec3{0.f}, glm::vec3{1.f}), 0.f}; + if (pool.size > 0) + { + pool.head = (pool.head + spawns) % pool.size; + if (spawns > 0) + m_smokealive += pool.size; + } + } + + smoke_simulation simulation; + simulation.places = m_places; + simulation.step = glm::vec4{static_cast(Particles.timedelta()), shift.x, shift.y, shift.z}; + auto const wind{glm::vec3{simulation::Environment.wind()}}; + simulation.wind = glm::vec4{wind.x, wind.y, wind.z, 0.f}; + simulation.seed = static_cast(m_lastupdate); + gpu_simulate(simulation, m_sourcerecords); +} + +std::array gpu_particles::planes(view const &View) +{ + // the sides and the near side of the clip space, w >= 0 (the far plane is at infinity) + auto const matrix{glm::transpose(View.projection * View.modelview)}; + std::array result{matrix[3] + matrix[0], matrix[3] - matrix[0], matrix[3] + matrix[1], matrix[3] - matrix[1], matrix[3]}; + for (auto &plane : result) + { + auto const length{glm::length(glm::vec3{plane})}; + if (length > 0.f) + plane /= length; + } + return result; +} + +std::size_t gpu_particles::render_smoke(view const &View) +{ + if ((false == init()) || (m_places == 0)) + return 0; + smoke_draw draw; + draw.places = m_places; + draw.origin = glm::vec3{m_origin - View.camera}; + draw.view = View.modelview; + draw.options = glm::vec4{Global.gfx_smoke_lighting ? 1.f : 0.f, SMOKE_FORWARD, static_cast(m_places), 0.f}; + draw.planes = planes(View); + draw.soft = glm::vec4{View.soft ? Global.gfx_particles_soft : 0.f, 0.f, 0.f, 0.f}; + gpu_render_smoke(draw, m_smokedrawn); + return m_smokedrawn; +} + +std::size_t gpu_particles::render_precipitation(view const &View, bool const Snow, float const Intensity, unsigned int const Frame) +{ + if (false == init()) + return 0; + gpu_collect_precipitation(m_precipitationdrawn); + + auto const &kind{Snow ? SNOW : RAIN}; + // once per frame: the time moves on, the particles drift with the wind and fall, the camera has its velocity + if ((false == m_framestarted) || (Frame != m_frame)) + { + m_framestarted = true; + m_frame = Frame; + auto const seconds{Timer::GetDeltaTime()}; + if (seconds > 0.0) + { + auto const wind{glm::dvec3{simulation::Environment.wind()} * WIND_SHARE}; + for (std::size_t box = 0; box < BOX_SIZE.size(); ++box) + { + auto const size{static_cast(BOX_SIZE[box])}; + m_drift[box] = glm::mod(m_drift[box] + wind * seconds, size); + m_fall[box] = std::fmod(m_fall[box] + kind.fall * seconds, 16.0 * size); + } + m_time = std::fmod(m_time + seconds, 3600.0); + if (m_camerastarted) + { + auto const velocity{(View.camera - m_lastcamera) / seconds}; + if (glm::length(velocity) < CAMERA_JUMP) + m_cameravelocity = glm::mix(m_cameravelocity, velocity, 1.0 - std::exp(-10.0 * seconds)); + } + } + m_lastcamera = View.camera; + m_camerastarted = true; + } + + // the particles, more of them in heavier rain or snow + auto const most{static_cast(std::max(Global.gfx_precipitation_particles, 0))}; + auto const count{static_cast(most * std::clamp(0.15f + 0.85f * Intensity, 0.f, 1.f))}; + m_precipitationcount = count; + m_precipitationsnow = Snow; + if (count == 0) + return 0; + if (m_precipitationcapacity < count) + { + m_precipitationcapacity = std::max(count, most); + gpu_precipitation_storage(m_precipitationcapacity); + } + + precipitation_draw draw; + draw.count = count; + auto const second{static_cast(count * BOX_SHARE[0])}; + auto const third{static_cast(count * (BOX_SHARE[0] + BOX_SHARE[1]))}; + draw.counts = glm::uvec4{second, third, count, Snow ? 1u : 0u}; + for (std::size_t box = 0; box < BOX_SIZE.size(); ++box) + { + auto const size{static_cast(BOX_SIZE[box])}; + draw.offsets[box] = glm::vec4{glm::vec3{glm::mod(m_drift[box] - glm::mod(View.camera, size), size)}, static_cast(m_fall[box])}; + } + draw.sizes = glm::vec4{BOX_SIZE[0], BOX_SIZE[1], BOX_SIZE[2], 0.f}; + auto const velocity{glm::vec3{glm::dvec3{simulation::Environment.wind()} * WIND_SHARE - m_cameravelocity}}; + draw.velocity = glm::vec4{velocity.x, velocity.y, velocity.z, kind.fall}; + draw.timing = glm::vec4{static_cast(m_time), kind.exposure, kind.opacity, static_cast(m_precipitationcapacity)}; + draw.view = View.modelview; + draw.options = glm::vec4{1.f, kind.forward, kind.brightness, 0.f}; + draw.planes = planes(View); + // the width of a pixel a metre in front of the camera, for the least width of a streak + auto const pixel{(View.height > 0) && (View.projection[1][1] != 0.f) ? 2.f / (std::abs(View.projection[1][1]) * static_cast(View.height)) : 0.f}; + draw.soft = glm::vec4{View.soft ? 0.25f * Global.gfx_particles_soft : 0.f, 0.f, 0.f, 0.f}; + draw.pixel = glm::vec4{pixel, 0.f, 0.f, 0.f}; + gpu_render_precipitation(draw); + return m_precipitationdrawn; +} + +std::string gpu_particles::info() const +{ + if (m_state <= 0) + return {}; + std::string text; + if (m_places > 0) + text += " smoke of the gpu: " + std::to_string(m_smokedrawn) + " drawn, " + std::to_string(m_places) + " places of " + std::to_string(m_pool.size()) + " sources (" + + std::to_string(m_smokealive) + " of the ones which bore lately)" + (m_poolfull ? ", some sources left out" : "") + "\n"; + if (m_precipitationcount > 0) + text += std::string(" ") + (m_precipitationsnow ? "snow" : "rain") + " of the gpu: " + std::to_string(m_precipitationdrawn) + " of " + std::to_string(m_precipitationcount) + + " drawn\n"; + return text; +} + +} // namespace gfx diff --git a/rendering/common/gpuparticles.h b/rendering/common/gpuparticles.h new file mode 100644 index 00000000..42b41f33 --- /dev/null +++ b/rendering/common/gpuparticles.h @@ -0,0 +1,185 @@ +/* +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" + +class particle_manager; + +namespace gfx +{ + +// particles of the gpu, whichever api draws them: the smoke and the rain or snow. +// +// smoke: the sources (particle_manager in its gpu mode) work out on the cpu only how many particles they bear in an update of +// the simulation and what these start from; the particles are kept in a storage buffer, a ring of places for each source, and +// simulated by a compute pass once per update (particles_smoke_simulate.comp). in every colour pass which draws them another +// compute pass (particles_smoke_prepare.comp) leaves out the dead ones and the ones out of the view, lights the rest with the +// lights of the pass and adds them to an indirect draw of billboards (particles_smoke.vert/.frag). +// +// rain and snow: particles in three boxes around the camera, each repeated along the world axes, which keep no state: their +// place follows from their number, how far the wind took them and how far they fell (particles_precipitation.comp), drawn as +// streaks of their motion against the camera (particles_precipitation.vert/.frag). +// +// the positions of the smoke are kept from an origin near the camera, moved by whole steps as the camera goes, so they keep +// their precision far from the centre of the scenery. what the passes are given is worked out here; the buffers, the compute +// passes and the draws are the renderer's (the gpu_ functions) +class gpu_particles +{ + public: + gpu_particles() = default; + virtual ~gpu_particles() = default; + gpu_particles(gpu_particles const &) = delete; + gpu_particles &operator=(gpu_particles const &) = delete; + + // what a colour pass draws with + struct view + { + glm::mat4 modelview; // world axes around the camera to the view space of the pass + glm::mat4 projection; + glm::dvec3 camera; + int height{0}; // of the target, pixels + bool soft{false}; // the resolved depth of the scene drawn so far is bound to the depth of the scene of the pass + }; + + // whether the programs could be made; without them the smoke stays on the cpu and the rain is the old cylinder + bool smoke_available(); + bool precipitation_available(); + // once per update of the particle manager (in its gpu mode): the births and the change of the smoke. Camera: of the view + void simulate_smoke(particle_manager &Particles, glm::dvec3 const &Camera); + // draws the smoke the view sees, with the texture bound to unit 0. returns: particles drawn, as read back a few frames later + std::size_t render_smoke(view const &View); + // draws the rain (Snow false) or the snow of the intensity 0-1. Frame: number of the frame, the time moves on once per frame. + // returns: particles drawn, read back + std::size_t render_precipitation(view const &View, bool Snow, float Intensity, unsigned int Frame); + // drops what is on the gpu, e.g. when the context goes + void clear(); + + std::string info() const; + + protected: + // the uniforms of particles_smoke_simulate.comp + struct smoke_simulation + { + std::uint32_t places{0}; + glm::vec4 step{0.f}; // x: seconds of the update, yzw: added to the positions, as the origin moved + glm::vec4 wind{0.f}; + std::uint32_t seed{0}; + }; + // the uniforms of particles_smoke_prepare.comp and of the draw + struct smoke_draw + { + std::uint32_t places{0}; + glm::vec3 origin{0.f}; // of the particles less the camera + glm::mat4 view{1.f}; + glm::vec4 options{0.f}; // x: lit by the lights of the pass, y: scattering onwards, z: places for the draws + std::array planes{}; + glm::vec4 soft{0.f}; // of particles_smoke.vert + }; + // the uniforms of particles_precipitation.comp and of the draw + struct precipitation_draw + { + glm::uvec4 counts{0u}; + std::array offsets{}; + glm::vec4 sizes{0.f}; + glm::vec4 velocity{0.f}; + glm::vec4 timing{0.f}; + glm::mat4 view{1.f}; + glm::vec4 options{0.f}; + std::array planes{}; + glm::vec4 soft{0.f}; // of particles_precipitation.vert + glm::vec4 pixel{0.f}; + std::uint32_t count{0}; // particles, the threads of the compute pass + }; + + // the programs; false: the particles of the gpu can't be used + virtual bool gpu_init() = 0; + // new storage of the smoke: Places places (at least 1) of particles, all dead but the first Kept of the storage before, which + // keep what they had; the storage of the draws for as many particles, the source of every place (Owners) and room for the + // records of Sources sources + virtual void gpu_layout(std::uint32_t const Places, std::uint32_t const Kept, std::vector const &Owners, std::size_t const Sources) = 0; + // the records of the sources (when there are any) and the compute pass of the simulation + virtual void gpu_simulate(smoke_simulation const &Simulation, std::vector const &Records) = 0; + // the compute pass and the draw of the smoke the view sees; Drawn: the particles drawn, as the last read back gives them (left + // as it is when nothing came back yet) + virtual void gpu_render_smoke(smoke_draw const &Draw, std::size_t &Drawn) = 0; + // the particles of the rain or snow drawn, as the last read back gives them + virtual void gpu_collect_precipitation(std::size_t &Drawn) = 0; + // room for the draws of Capacity particles of the rain or snow + virtual void gpu_precipitation_storage(std::uint32_t const Capacity) = 0; + // the compute pass and the draw of the rain or snow + virtual void gpu_render_precipitation(precipitation_draw const &Draw) = 0; + // the storage goes (the draw commands and the read backs stay) + virtual void gpu_clear() = 0; + + std::uint32_t places() const + { + return m_places; + } + std::uint32_t precipitation_count() const + { + return m_precipitationcount; + } + bool precipitation_snow() const + { + return m_precipitationsnow; + } + + private: + struct pool_source + { + std::uint32_t first{0}; + std::uint32_t size{0}; + std::uint32_t head{0}; + }; + + bool init(); + // the places of the sources in the storage: sources added after the others get theirs after the places already given, and + // the particles stay; otherwise all is laid out anew + void layout(particle_manager &Particles); + // the planes of the view of the pass, for the culling of the compute passes + static std::array planes(view const &View); + + int m_state{0}; // 0: not set up yet, 1: ready, -1: can't be used + + // smoke + std::vector m_pool; + std::vector m_requested; // the sizes the sources asked for + std::vector m_sourcerecords; + std::uint32_t m_places{0}; + bool m_poolfull{false}; // some sources got no places + bool m_storage{false}; // the storage of the smoke was made + std::uint64_t m_lastupdate{0}; + glm::dvec3 m_origin{0.0}; + bool m_originset{false}; + std::size_t m_smokedrawn{0}; + std::size_t m_smokealive{0}; // places of the sources which bore particles lately + + // rain and snow + std::uint32_t m_precipitationcapacity{0}; + unsigned int m_frame{0}; + bool m_framestarted{false}; + std::array m_drift{}; // how far the wind took the particles of each box, within the box + std::array m_fall{}; // how far they fell, within 16 sizes of the box + double m_time{0.0}; // seconds, for the flutter of the flakes + glm::dvec3 m_lastcamera{0.0}; + glm::dvec3 m_cameravelocity{0.0}; + bool m_camerastarted{false}; + std::size_t m_precipitationdrawn{0}; + std::uint32_t m_precipitationcount{0}; + bool m_precipitationsnow{false}; +}; + +} // namespace gfx diff --git a/rendering/common/grassfield.cpp b/rendering/common/grassfield.cpp new file mode 100644 index 00000000..e0a628c5 --- /dev/null +++ b/rendering/common/grassfield.cpp @@ -0,0 +1,269 @@ +/* +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/common/grassfield.h" + +#include "simulation/simulationenvironment.h" +#include "simulation/simulationtime.h" +#include "utilities/Globals.h" +#include "utilities/Timer.h" + +namespace gfx +{ + +namespace +{ + +int constexpr max_blades{65536}; // drawn in a tile at most +float constexpr blade_reach{1.5f}; // metres the bounds of a tile reach above its ground, for the blades + +// the share of the blades drawn at specified distance: fewer further away, for speed (from Thinning), and fewer and fewer from +// Fade to Reach, where the grass ends; the same as lod_share() of grass.vert +float lod_share(float const Distance, float const Thinning, float const Fade, float const Reach) +{ + auto const share{Distance <= Thinning ? 1.f : std::pow(Thinning / Distance, 1.5f)}; + auto const t{std::clamp((Distance - Fade) / std::max(Reach - Fade, 1e-3f), 0.f, 1.f)}; + return share * (1.f - t * t * (3.f - 2.f * t)); +} + +// the grass through the year, by the day of the year: x how dry it is, y how fresh (the green of spring), z how grown (share +// of the height). the turns follow the seasons of the textures (world_environment::compute_season(): winter till day 65, +// spring till 158, summer till 252, autumn till 341): dormant and dry in winter, greening up and growing through spring, +// fresh in May, drying out from August, the dry grass lying lower towards winter +glm::vec3 grass_season(int const Yearday) +{ + struct point + { + float day; + glm::vec3 value; + }; + static point const year[]{{1.f, {0.80f, 0.f, 0.55f}}, {65.f, {0.80f, 0.f, 0.55f}}, {100.f, {0.45f, 0.3f, 0.70f}}, {135.f, {0.05f, 1.f, 0.90f}}, + {170.f, {0.f, 0.4f, 1.f}}, {200.f, {0.10f, 0.f, 1.f}}, {240.f, {0.35f, 0.f, 1.f}}, {280.f, {0.50f, 0.f, 0.95f}}, + {320.f, {0.65f, 0.f, 0.80f}}, {341.f, {0.80f, 0.f, 0.60f}}, {367.f, {0.80f, 0.f, 0.55f}}}; + auto const day{std::clamp(static_cast(Yearday), 1.f, 366.f)}; + for (std::size_t i = 1; i < std::size(year); ++i) + if (day <= year[i].day) + { + auto const t{(day - year[i - 1].day) / (year[i].day - year[i - 1].day)}; + return glm::mix(year[i - 1].value, year[i].value, t * t * (3.f - 2.f * t)); + } + return year[std::size(year) - 1].value; +} + +} // namespace + +void grass_field::release(std::uint64_t const Id, chunk &Chunk) +{ + if (Chunk.uploaded) + gpu_release(Id); + Chunk.uploaded = false; +} + +void grass_field::clear() +{ + for (auto &entry : m_chunks) + release(entry.first, entry.second); + m_chunks.clear(); +} + +void grass_field::upload(std::uint64_t const Id, chunk &Chunk, gfx::grass_chunk const &Source) +{ + release(Id, Chunk); + Chunk.version = Source.version; + Chunk.corner = Source.corner; + Chunk.size = Source.size; + Chunk.cells = Source.cells; + Chunk.samples = Source.samples; + Chunk.kinds = Source.kinds; + gpu_upload(Id, Source); + Chunk.uploaded = true; + + auto const side{Source.cells + 1}; + auto const samples{Source.samples + 1}; + auto const plane{static_cast(samples) * samples}; + std::size_t kinds{0}; + while (kinds < Source.kinds.size() && Source.kinds[kinds] >= 0) + ++kinds; + + // the ground and the densest place of every kind of each tile, to skip the empty tiles and to draw as many blades as needed + auto const spacing{static_cast(Source.size) / Source.cells}; + auto const step{static_cast(Source.size) / Source.samples}; + auto const tilesize{static_cast(Source.size) / tiles}; + for (int tz = 0; tz < tiles; ++tz) + for (int tx = 0; tx < tiles; ++tx) + { + auto &part{Chunk.parts[static_cast(tz) * tiles + tx]}; + auto const range = [](double const From, double const To, double const Step, int const Last) { + return std::make_pair(std::clamp(static_cast(std::floor(From / Step)), 0, Last), std::clamp(static_cast(std::ceil(To / Step)), 0, Last)); + }; + auto const [gx0, gx1]{range(tx * tilesize, (tx + 1) * tilesize, spacing, Source.cells)}; + auto const [gz0, gz1]{range(tz * tilesize, (tz + 1) * tilesize, spacing, Source.cells)}; + part.low = std::numeric_limits::max(); + part.high = -std::numeric_limits::max(); + for (int z = gz0; z <= gz1; ++z) + for (int x = gx0; x <= gx1; ++x) + { + auto const height{Source.heights[static_cast(z) * side + x]}; + part.low = std::min(part.low, height); + part.high = std::max(part.high, height); + } + part.most.fill(0.f); + auto const [sx0, sx1]{range(tx * tilesize, (tx + 1) * tilesize, step, Source.samples)}; + auto const [sz0, sz1]{range(tz * tilesize, (tz + 1) * tilesize, step, Source.samples)}; + for (std::size_t kind = 0; kind < kinds; ++kind) + { + std::uint8_t most{0}; + for (int z = sz0; z <= sz1; ++z) + for (int x = sx0; x <= sx1; ++x) + most = std::max(most, Source.density[kind * plane + static_cast(z) * samples + x]); + part.most[kind] = most / 255.f; + } + } +} + +void grass_field::update(std::vector const &Kinds, std::vector const &Chunks) +{ + m_kinds = Kinds; + for (auto &entry : m_chunks) + entry.second.listed = false; + if (false == Global.gfx_grass) + { + clear(); + return; + } + for (auto const &source : Chunks) + { + if (source.heights == nullptr || source.density == nullptr || source.cells <= 0 || source.samples <= 0) + continue; + auto &entry{m_chunks[source.id]}; + entry.listed = true; + if (false == entry.uploaded || entry.version != source.version) + upload(source.id, entry, source); + } + // what's no longer near goes + for (auto it = m_chunks.begin(); it != m_chunks.end();) + { + if (it->second.listed) + { + ++it; + continue; + } + release(it->first, it->second); + it = m_chunks.erase(it); + } +} + +bool grass_field::prepare(glm::dvec3 const &Camera, std::function const &Visible) +{ + if (false == Global.gfx_grass || m_chunks.empty() || m_kinds.empty()) + return false; + + m_chunkdraws.clear(); + m_tiledraws.clear(); + m_triangles = 0; + // whole up to the distance, thinning out to the fading distance + auto const fade{std::max(5.f, Global.gfx_grass_distance)}; + auto const reach{std::max(fade + 1.f, Global.gfx_grass_fadedistance)}; + auto const thinning{std::max(3.f, fade * 0.2f)}; + auto const multiplier{std::max(0.f, Global.gfx_grass_density)}; + auto const camera{Camera}; + + m_frame.lod = glm::vec4{thinning, fade, reach, 0.f}; + auto const season{grass_season(simulation::Time.year_day())}; + m_frame.season = glm::vec4{season.x, season.y, season.z, 0.f}; + // the wind sways the blades along it, harder in the gusts + auto const wind{glm::vec3{simulation::Environment.wind()}}; + auto const windspeed{glm::length(glm::vec2{wind.x, wind.z})}; + auto const winddirection{windspeed > 0.01f ? glm::vec2{wind.x, wind.z} / windspeed : glm::vec2{1.f, 0.f}}; + m_frame.wind = glm::vec4{winddirection.x, winddirection.y, std::clamp(windspeed / 15.f, 0.f, 1.f), static_cast(std::fmod(Timer::GetTime(), 3600.0))}; + + for (auto &entry : m_chunks) + { + auto &chunk{entry.second}; + // the kinds of the chunk, and the share of the blades of each against its densest + std::array base{}, tip{}, shape{}, dry{}; + glm::vec4 share{0.f}; + float densest{0.f}; + for (std::size_t i = 0; i < chunk.kinds.size(); ++i) + if (chunk.kinds[i] >= 0 && chunk.kinds[i] < static_cast(m_kinds.size())) + densest = std::max(densest, m_kinds[chunk.kinds[i]].density); + if (densest <= 0.f) + continue; + for (std::size_t i = 0; i < chunk.kinds.size(); ++i) + { + if (chunk.kinds[i] < 0 || chunk.kinds[i] >= static_cast(m_kinds.size())) + continue; + auto const &kind{m_kinds[chunk.kinds[i]]}; + base[i] = glm::vec4{kind.base, 0.f}; + tip[i] = glm::vec4{kind.tip, 0.f}; + shape[i] = glm::vec4{kind.height, kind.variation, kind.width, kind.bend}; + dry[i] = glm::vec4{kind.dry, kind.seasonal}; + share[i] = kind.density / densest; + } + auto const tallest{std::max({shape[0].x, shape[1].x, shape[2].x, shape[3].x}) * 1.5f + blade_reach}; + + auto const tilesize{chunk.size / tiles}; + bool set{false}; + for (int tz = 0; tz < tiles; ++tz) + for (int tx = 0; tx < tiles; ++tx) + { + auto const &part{chunk.parts[static_cast(tz) * tiles + tx]}; + auto const fill{std::min(1.f, glm::dot(glm::vec4{part.most[0], part.most[1], part.most[2], part.most[3]}, share))}; + if (fill <= 0.f) + continue; + // distance to the box of the tile + glm::dvec3 const low{chunk.corner.x + tx * tilesize, part.low, chunk.corner.y + tz * tilesize}; + glm::dvec3 const high{low.x + tilesize, part.high + tallest, low.z + tilesize}; + auto const nearest{glm::clamp(camera, low, high)}; + auto const distance{static_cast(glm::length(nearest - camera))}; + if (distance > reach) + continue; + if (false == Visible(scene::bounding_area{(low + high) * 0.5, static_cast(glm::length(high - low) * 0.5)})) + continue; + auto const full{tilesize * tilesize * densest * multiplier * fill}; + auto const count{std::min(max_blades, static_cast(std::ceil(full * lod_share(distance, thinning, fade, reach))))}; + if (count <= 0) + continue; + if (false == set) + { + // the chunk is drawn relative to its corner, the heights as they are + set = true; + chunk_draw draw; + draw.id = entry.first; + draw.offset = glm::dvec3{chunk.corner.x - camera.x, -camera.y, chunk.corner.y - camera.z}; + draw.chunk = glm::vec4{chunk.size, static_cast(chunk.cells), static_cast(chunk.samples), 0.f}; + draw.camera = glm::vec4{static_cast(camera.x - chunk.corner.x), static_cast(camera.y), static_cast(camera.z - chunk.corner.y), 0.f}; + draw.base = base; + draw.tip = tip; + draw.shape = shape; + draw.share = share; + draw.dry = dry; + draw.first = m_tiledraws.size(); + m_chunkdraws.push_back(draw); + } + // fewer segments further away + auto const segments{distance < thinning * 1.5f ? 3 : distance < fade * 0.5f ? 2 : 1}; + // the number of the tile in the world, the seed of its blades + auto const seedx{static_cast(std::floor(chunk.corner.x / tilesize + 0.5) + tx)}; + auto const seedz{static_cast(std::floor(chunk.corner.y / tilesize + 0.5) + tz)}; + tile_draw draw; + draw.tile = glm::vec4{tx * tilesize, tz * tilesize, tilesize, std::max(1.f, full)}; + draw.part = glm::vec4{fill, static_cast(segments), seedx, seedz}; + draw.count = count; + draw.segments = segments; + m_tiledraws.push_back(draw); + ++m_chunkdraws.back().count; + m_triangles += static_cast(count) * (segments * 2 - 1); + } + } + return true; +} + +} // namespace gfx diff --git a/rendering/common/grassfield.h b/rendering/common/grassfield.h new file mode 100644 index 00000000..edc100c8 --- /dev/null +++ b/rendering/common/grassfield.h @@ -0,0 +1,133 @@ +/* +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 + +#include "rendering/renderer.h" +#include "scene/scenenode.h" + +namespace gfx +{ + +// grass of the heightmap terrain, whichever api draws it: blades made up by the vertex shader (grass.vert) where the grass is +// painted, in the chunks near the camera. +// +// a chunk is cut into tiles; each tile visible within the reach of the grass is drawn as instances of a blade, as many as +// its densest kind and its distance ask for. a blade finds its place, its kind and its shape from its number, so the same +// blades stand at the same places in every frame. further away the blades thin out and grow wider, and fade out at the end +// of the reach. the heights, the densities and the holes of a chunk are kept in textures, made by the renderer (the gpu_ +// functions) when the chunk comes near; what is drawn in a frame is worked out here (prepare()) +class grass_field +{ + public: + virtual ~grass_field() = default; + + // see gfx_renderer::Terrain_Grass() + void update(std::vector const &Kinds, std::vector const &Chunks); + // drops everything made on the gpu, e.g. when the context goes + void clear(); + bool empty() const + { + return m_chunks.empty(); + } + + static int constexpr tiles{16}; // along a side of a chunk + + // what a frame draws (the uniforms of grass.vert): the same for all draws + struct frame_draw + { + glm::vec4 lod{0.f}; // grass_lod + glm::vec4 season{0.f}; // grass_season + glm::vec4 wind{0.f}; // grass_wind + }; + // a chunk with tiles seen, drawn relative to its corner + struct chunk_draw + { + std::uint64_t id{0}; // of the chunk, as the renderer got it in gpu_upload() + glm::dvec3 offset{0.0}; // of the corner from the camera, the heights as they are + glm::vec4 chunk{0.f}; // grass_chunk + glm::vec4 camera{0.f}; // grass_camera + std::array base{}, tip{}, shape{}, dry{}; + glm::vec4 share{0.f}; + std::size_t first{0}; // tiles drawn, in tile_draws() + std::size_t count{0}; + }; + // a tile drawn: Count instances of a strip of 2 * Segments + 1 vertices + struct tile_draw + { + glm::vec4 tile{0.f}; // grass_tile + glm::vec4 part{0.f}; // grass_part + int count{0}; + int segments{0}; + }; + // the draws of the grass seen from Camera: false when there is nothing to draw (grass off, no chunks or kinds), the draws of + // the last call are left then. Visible: whether the bounds of a tile are seen + bool prepare(glm::dvec3 const &Camera, std::function const &Visible); + frame_draw const &frame() const + { + return m_frame; + } + std::vector const &chunk_draws() const + { + return m_chunkdraws; + } + std::vector const &tile_draws() const + { + return m_tiledraws; + } + // the triangles of the draws prepared + std::size_t triangles() const + { + return m_triangles; + } + + protected: + // the textures of the chunk Id made of Source: heights (R32F, the grid points, cells + 1 along a side), densities (RGBA8, the + // densities of the kinds side by side, samples + 1 along a side), holes (R8, a single texel of 0 without holes) + virtual void gpu_upload(std::uint64_t const Id, gfx::grass_chunk const &Source) = 0; + // the textures of the chunk go; nothing when it has none + virtual void gpu_release(std::uint64_t const Id) = 0; + + private: + struct tile + { + float low{0.f}, high{0.f}; // of the ground + std::array most{}; // the highest density of each kind of the chunk within the tile, 0..1 + }; + struct chunk + { + std::uint64_t version{0}; + glm::dvec2 corner{0.0}; + float size{0.f}; + int cells{0}; + int samples{0}; + std::array kinds{-1, -1, -1, -1}; + std::array parts; + bool uploaded{false}; // has its textures + bool listed{false}; // in the last list given + }; + + void upload(std::uint64_t const Id, chunk &Chunk, gfx::grass_chunk const &Source); + void release(std::uint64_t const Id, chunk &Chunk); + + std::vector m_kinds; + std::unordered_map m_chunks; + frame_draw m_frame; + std::vector m_chunkdraws; + std::vector m_tiledraws; + std::size_t m_triangles{0}; +}; + +} // namespace gfx diff --git a/rendering/common/instancecull.cpp b/rendering/common/instancecull.cpp new file mode 100644 index 00000000..5625b03c --- /dev/null +++ b/rendering/common/instancecull.cpp @@ -0,0 +1,389 @@ +/* +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/common/instancecull.h" + +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "rendering/frustum.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" + +namespace gfx +{ + +namespace +{ + +constexpr std::int64_t RECORD_SIZE{sizeof(scene::basic_cell::instance_record)}; + +} // namespace + +instance_cull::instance_cull() : m_records{std::make_shared()} +{ + m_readbackdata.resize(gl::INSTANCE_COUNTER_HEADER + READBACK_ENTRIES); +} + +void instance_cull::begin_pass(pass_setup const &Setup) +{ + collect(); + + m_inpass = true; + m_ran = true; + m_dispatched = false; + m_setup = Setup; + m_buckets.clear(); + m_joblist.clear(); + m_grouplist.clear(); + m_drawgroups.clear(); + m_lodlist.clear(); + m_groupmap.clear(); + m_sleepergroupmap.clear(); + m_cpucounts.clear(); + m_candidates = 0; + m_entries = 0; + m_sleepertracks = 0; + m_sleepers = 0; + m_models = 0; + m_counts = {}; +} + +void instance_cull::add_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area) +{ + add(Key, Bucket, Area, false); +} + +void instance_cull::skip_bucket(scene::basic_cell::instance_bucket const &Bucket) +{ + if (!m_inpass) + return; + ++m_counts.skippedbuckets; + m_counts.skippedinstances += Bucket.records.size(); +} + +void instance_cull::add_sleepers(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area) +{ + add(Key, Bucket, Area, true); +} + +void instance_cull::add(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area, bool const Sleepers) +{ + if (!m_inpass || Bucket.records.empty() || Key.pModel == nullptr) + return; + + // range of the squared distances the instances of the bucket can be at, scaled as in the culling, with some slack for + // its single precision: the draws are made only for the lod bands within it. sleepers go by the plain distance from the + // camera of the pass + auto const &distancecamera{Sleepers ? m_setup.camera : m_setup.distancecamera}; + auto const distancescale{Sleepers ? 1.0 : static_cast(m_setup.distancescale)}; + auto const distanceoffset{Sleepers ? 0.0 : static_cast(m_setup.distanceoffset)}; + auto const centerdistance{glm::length(Area.center - distancecamera)}; + auto const nearest{std::max(0.0, centerdistance - Area.radius)}; + auto const farthest{centerdistance + Area.radius}; + auto const distancemin{static_cast(0.999 * (nearest * nearest * distancescale + distanceoffset))}; + auto const distancemax{static_cast(1.001 * (farthest * farthest * distancescale + distanceoffset))}; + + auto &groupmap{Sleepers ? m_sleepergroupmap : m_groupmap}; + auto lookup{groupmap.find(Key)}; + if (lookup == groupmap.end()) + { + // lod bounds of the model, the same set Render_Instanced() splits the distance into bands with, found once for the model + auto const lodfirst{static_cast(m_lodlist.size())}; + auto const &lodbounds{Key.pModel->lod_bounds()}; + m_lodlist.insert(m_lodlist.end(), lodbounds.begin(), lodbounds.end()); + auto const lodcount{static_cast(lodbounds.size())}; + + group_data const group{static_cast(m_grouplist.size()), m_entries, lodcount + 1, distancemin, distancemax}; + m_grouplist.push_back({lodfirst, lodcount, m_entries, 0}); + m_entries += group.entrycount; + lookup = groupmap.emplace(Key, group).first; + if (m_setup.drawing) + { + // any instance of the group serves; it's only prepared and asked for its material + TAnimModel *instance{nullptr}; + for (auto *candidate : Bucket.instances) + { + if (candidate != nullptr) + { + instance = candidate; + break; + } + } + m_drawgroups.push_back({&lookup->first, instance, Sleepers, lodfirst, lodcount, group.entryfirst, distancemin, distancemax}); + } + } + else + { + lookup->second.distancemin = std::min(lookup->second.distancemin, distancemin); + lookup->second.distancemax = std::max(lookup->second.distancemax, distancemax); + if (m_setup.drawing) + { + auto &drawgroup{m_drawgroups[lookup->second.index]}; + drawgroup.distancemin = lookup->second.distancemin; + drawgroup.distancemax = lookup->second.distancemax; + if (drawgroup.instance == nullptr) + { + for (auto *candidate : Bucket.instances) + { + if (candidate != nullptr) + { + drawgroup.instance = candidate; + break; + } + } + } + } + } + + place(Bucket); + + gl::instance_cull_job job{}; + job.origin = packed_vec3{Bucket.recordorigin - m_setup.camera}; + job.first = Bucket.gpufirst; + job.distanceorigin = packed_vec3{Bucket.recordorigin - m_setup.distancecamera}; + job.count = static_cast(Bucket.records.size()); + job.group = lookup->second.index; + job.candidatestart = m_candidates; + job.flags = (Sleepers ? gl::INSTANCE_JOB_SLEEPERS : 0u); + m_joblist.push_back(job); + m_buckets.push_back(&Bucket); + m_candidates += job.count; + if (Sleepers) + { + ++m_sleepertracks; + m_sleepers += job.count; + } + else if (Key.instance != nullptr) + { + ++m_models; + } +} + +void instance_cull::place(scene::basic_cell::instance_bucket const &Bucket) +{ + auto &shared{*m_records}; + auto const count{static_cast(Bucket.records.size())}; + auto const current{Bucket.gpugeneration == shared.recordgeneration}; + if (current && !Bucket.gpustale) + return; + if (current && count <= Bucket.gpucapacity) + { + send_records(Bucket.gpufirst, Bucket.records.data(), count); + m_counts.sentrecords += count; + Bucket.gpustale = false; + return; + } + + // a new place, with some room for instances the editor adds + auto const allocate = [this, &shared](scene::basic_cell::instance_bucket const &Target) { + auto const records{static_cast(Target.records.size())}; + Target.gpufirst = shared.recordfill; + Target.gpucapacity = records + records / 4; + Target.gpugeneration = shared.recordgeneration; + Target.gpustale = false; + shared.recordfill += Target.gpucapacity; + send_records(Target.gpufirst, Target.records.data(), records); + m_counts.sentrecords += records; + }; + + auto const capacity{static_cast(record_buffer_size() / RECORD_SIZE)}; + if (shared.recordfill + count + count / 4 > capacity) + { + // the buffer is started over, and only the buckets of this pass get places again. the places left behind by + // buckets which were made anew or are out of sight are dropped this way. the passes of a frame queue different + // buckets: when those of all of them don't fit in the buffer together, some pass would start it over in every frame, + // so a buffer started over again in the same or the next frame grows + std::int64_t live{count + count / 4}; + for (auto const *bucket : m_buckets) + live += bucket->records.size() + bucket->records.size() / 4; + auto const again{shared.restarted && (m_setup.frame - shared.restartframe <= 1)}; + if ((2 * live * RECORD_SIZE > record_buffer_size()) || again) + resize_record_buffer(std::max(2 * live * RECORD_SIZE, 2 * record_buffer_size())); + else + restart_records(); + shared.restarted = true; + shared.restartframe = m_setup.frame; + m_counts.restarted = true; + ++shared.recordgeneration; + shared.recordfill = 0; + for (std::size_t job = 0; job < m_buckets.size(); ++job) + { + allocate(*m_buckets[job]); + m_joblist[job].first = m_buckets[job]->gpufirst; + } + } + allocate(Bucket); +} + +bool instance_cull::dispatch_begin() +{ + m_occlusion = false; + m_twophase = false; + return (m_inpass && !m_joblist.empty()); +} + +void instance_cull::frustum_planes(glm::vec4 (&Planes)[6]) const +{ + auto const &frustumplanes{m_setup.frustum->planes()}; + for (int plane = 0; plane < 6; ++plane) + { + auto const *source{frustumplanes[plane]}; + Planes[plane] = glm::vec4{source[0], source[1], source[2], + static_cast(source[0] * m_setup.camera.x + source[1] * m_setup.camera.y + source[2] * m_setup.camera.z + source[3])}; + } +} + +void instance_cull::dispatch_end() +{ + m_cpucounts.assign(m_entries, 0); + m_dispatched = true; +} + +void instance_cull::draw_result(std::uint32_t const Draws, std::uint32_t const Prepassdraws, std::uint32_t const Calls, std::uint32_t const Prepasscalls, double const Cputime) +{ + m_draws = Draws; + m_prepassdraws = Prepassdraws; + m_calls = Calls; + m_prepasscalls = Prepasscalls; + m_cputime = Cputime; +} + +void instance_cull::cpu_result(scene::basic_cell::instance_bucket_key const &Key, std::vector const &Bandcounts, bool const Sleepers) +{ + if (!m_dispatched) + return; + auto const &groupmap{Sleepers ? m_sleepergroupmap : m_groupmap}; + auto const lookup{groupmap.find(Key)}; + if (lookup == groupmap.end()) + return; + auto const &group{lookup->second}; + for (std::size_t band = 0; band < std::min(Bandcounts.size(), group.entrycount); ++band) + m_cpucounts[group.entryfirst + band] += Bandcounts[band]; +} + +void instance_cull::cpu_instance(scene::basic_cell::instance_bucket_key const &Key, float const Distancesquared) +{ + if (!m_dispatched) + return; + auto const lookup{m_groupmap.find(Key)}; + if (lookup == m_groupmap.end()) + return; + auto const &group{lookup->second}; + auto const &lods{m_grouplist[group.index]}; + // the band the culling gives the distance: the number of bounds it reached + auto const first{m_lodlist.begin() + lods.lodfirst}; + auto const band{static_cast(std::upper_bound(first, first + lods.lodcount, Distancesquared) - first)}; + if (band < group.entrycount) + ++m_cpucounts[group.entryfirst + band]; +} + +void instance_cull::end_pass() +{ + // the counts of both phases, for the panel and the comparison + if (m_dispatched) + push_counts(); + // the cpu counts are there to compare with only when the cpu drew + if (m_dispatched && !m_setup.drawing) + { + std::uint64_t total{0}; + for (auto const count : m_cpucounts) + total += count; + m_cpupasses.push_back({m_setup.frame, m_cpucounts, total}); + // the gpu is never this far behind + while (m_cpupasses.size() > 8) + m_cpupasses.pop_front(); + } + m_lastsleepertracks = m_sleepertracks; + m_lastsleepers = m_sleepers; + m_lastmodels = m_models; + m_counts.jobs = static_cast(m_joblist.size()); + m_counts.candidates = m_candidates; + m_lastcounts = m_counts; + m_inpass = false; + m_dispatched = false; +} + +void instance_cull::collect() +{ + if (!pop_counts(m_readbackdata.data())) + return; + + auto const frame{m_readbackdata[0]}; + comparison result; + result.frame = frame; + result.occluded = m_readbackdata[3]; + result.gputotal = m_readbackdata[1] - std::min(m_readbackdata[1], result.occluded); + result.entries = m_readbackdata[2]; + result.first = m_readbackdata[4]; + while (!m_cpupasses.empty() && m_cpupasses.front().frame != frame) + m_cpupasses.pop_front(); + if (m_cpupasses.empty()) + { + // drawn from the gpu: nothing to compare with + m_last = result; + return; + } + + auto const &cpu{m_cpupasses.front()}; + result.cputotal = cpu.total; + result.compared = std::min({result.entries, READBACK_ENTRIES, static_cast(cpu.counts.size())}); + for (std::uint32_t entry = 0; entry < result.compared; ++entry) + { + auto const gpu{m_readbackdata[gl::INSTANCE_COUNTER_HEADER + entry]}; + auto const difference{gpu > cpu.counts[entry] ? gpu - cpu.counts[entry] : cpu.counts[entry] - gpu}; + if (difference > 0) + { + ++result.mismatched; + result.largestdifference = std::max(result.largestdifference, difference); + } + } + m_cpupasses.pop_front(); + + m_last = result; + ++m_comparedframes; + if ((result.mismatched > 0) || (result.gputotal != result.cputotal) || (result.entries != cpu.counts.size())) + ++m_mismatchedframes; +} + +std::string instance_cull::info(std::string const &Label, bool const Occlusion) +{ + if (Global.gfx_gpucull == 0) + return {}; + // the sleepers are among the instances counted + auto sleepers{m_lastsleepertracks > 0 ? "\n" + Label + ": " + std::to_string(m_lastsleepers) + " sleepers of " + std::to_string(m_lastsleepertracks) + " tracks queued" : std::string{}}; + // and so are the models drawn on their own + if (m_lastmodels > 0) + sleepers += "\n" + Label + ": " + std::to_string(m_lastmodels) + " models drawn on their own queued"; + // what the pass tested and sent to the gpu + sleepers += "\n" + Label + ": " + std::to_string(m_lastcounts.candidates) + " instances tested in " + std::to_string(m_lastcounts.jobs) + " buckets"; + if (Global.gfx_gpucull_bucketrange) + sleepers += ", " + std::to_string(m_lastcounts.skippedbuckets) + " buckets (" + std::to_string(m_lastcounts.skippedinstances) + " instances) left out before the culling"; + sleepers += ", " + std::to_string(m_lastcounts.sentrecords) + " records sent" + (m_lastcounts.restarted ? ", record buffer started over" : ""); + if (Global.gfx_gpucull == 1) + { + if (m_last.frame == 0) + return "\n" + Label + ": no result yet" + sleepers; + if (false == Occlusion) + return "\n" + Label + ": " + std::to_string(m_last.gputotal) + " instances drawn, " + std::to_string(m_last.entries) + " bands; " + std::to_string(m_draws) + + " indirect draws in " + std::to_string(m_calls) + " calls, cpu " + to_string(m_cputime, 3) + " ms" + sleepers; + return "\n" + Label + ": " + std::to_string(m_last.gputotal) + " instances drawn" + + (m_twophase ? " (" + std::to_string(m_last.first) + " of them first, seen in the last frame)" : std::string{}) + ", " + std::to_string(m_last.occluded) + + " hidden by occlusion" + (m_occlusion ? (m_twophase ? " (also by one another)" : "") : " (test off)") + ", " + std::to_string(m_last.entries) + " bands; " + + std::to_string(m_draws) + " indirect draws in " + std::to_string(m_calls) + " calls (+" + std::to_string(m_prepassdraws) + " depth prepass in " + + std::to_string(m_prepasscalls) + "), cpu " + to_string(m_cputime, 3) + " ms" + sleepers; + } + if (m_comparedframes == 0) + return "\n" + Label + ": no result yet" + sleepers; + return "\n" + Label + ": " + std::to_string(m_last.gputotal) + " instances (cpu " + std::to_string(m_last.cputotal) + "), " + std::to_string(m_last.entries) + " bands, " + + std::to_string(m_last.mismatched) + " differ (by up to " + std::to_string(m_last.largestdifference) + "); frames differing " + std::to_string(m_mismatchedframes) + " of " + + std::to_string(m_comparedframes) + sleepers; +} + +} // namespace gfx diff --git a/rendering/common/instancecull.h b/rendering/common/instancecull.h new file mode 100644 index 00000000..d63147b7 --- /dev/null +++ b/rendering/common/instancecull.h @@ -0,0 +1,303 @@ +/* +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 +#include + +#include "gl/ubo.h" +#include "scene/scene.h" + +class cFrustum; +class TAnimModel; + +namespace gfx +{ + +// the cpu side of the culling of the model instances of a render pass in compute passes, shared by the renderers (stage N6c of +// claude/vulkan14-renderer-plan.md); the backend (opengl46_instance_cull, vk14 instance_cull) keeps the buffers and runs the passes: +// - the records of the instances (scene::basic_cell::instance_record) are kept in one buffer, each bucket of a scene cell +// in its own place, and sent again only when the bucket made them anew +// - every bucket queued in the pass becomes a job; the buckets of one model and one set of skins make a group, whose +// lod bands are its entries +// - the passes count the instances which pass per entry, turn the counts into places in the output and write the +// modelview matrices of the instances there +// with Global.gfx_gpucull 1 the instances are drawn from the result: the renderer turns each lod band of a group into a +// list of indirect draw commands (one per submodel drawn in the band), whose instances a fourth pass fills in from the +// counts (instance_cull_commands.comp), and draws them with the output bound as the instance buffer. an occlusion test +// against a depth pyramid of what the pass drew before can leave out more instances. in two phases the instances seen in the +// previous frame are culled and drawn first, then the pyramid is made again with them in it and the others are tested +// against it, so the instances hide one another too. the command buffer holds the commands three times over: the instances +// drawn first, all of them, and the ones drawn second, each copy command_count() commands long (draw_command_offset()) +// with Global.gfx_gpucull 2 the cpu still draws, and the result is only compared with its own: the counts per entry are +// read back a few frames later and set against the counts Render_Instanced() reported for the same frame +// each render pass culled on the gpu (the main view, the cascades of the sun shadows, the faces of the environment map, the +// shadow maps of headlights and the scenery parts of those of lamps) has a culling of its own, with its own buffers, while the +// records and the programs are shared by all of them: the records of a bucket go to the gpu once and serve every pass which +// queues the bucket +// the sleepers of tracks (TTrack::m_sleeper_local_transforms) are culled the same way, each track with them a bucket of its +// own, and drawn together with the instances; their groups are kept apart from those of the model instances +// so are the model instances drawn on their own (not instanceable): each one a bucket and a group of its own (the instance in +// its key), whose submodel tree the renderer animates and walks for it alone +class instance_cull +{ + public: + // what a pass needs for the tests, the way Render_Instanced() takes it + struct pass_setup + { + unsigned int frame{0}; + glm::dvec3 camera{0.0}; // camera of the pass: frustum and placement + glm::dvec3 distancecamera{0.0}; // the distance is measured from it + cFrustum const *frustum{nullptr}; // of the pass camera, in world coordinates + float distancescale{1.f}; // of the squared distance + float distanceoffset{0.f}; // added to the scaled squared distance past the range test + float distancerange{0.f}; // squared range of the scaled distance before the offset + float drawrange{0.f}; // squared draw range + float zoom{1.f}; + bool drawing{false}; // the instances are drawn from the result (Global.gfx_gpucull 1), not compared + }; + // a group of instances (model and skins) of the pass, for the renderer to make the draw commands of + struct draw_group + { + scene::basic_cell::instance_bucket_key const *key; + TAnimModel *instance; // one of the group, prepares the model and gives the material. none for sleepers + bool sleepers; // the skins come from the key, as Render_Sleepers() sets them + std::uint32_t lodfirst; // in lod_bounds() + std::uint32_t lodcount; + std::uint32_t entryfirst; + float distancemin; // squared distances (scaled as the culling does) the instances of the group can be at + float distancemax; + }; + // copies of the commands in the command buffer + enum class command_set : std::uint32_t + { + first = 0, // instances which passed the first dispatch: in one phase all of them + all = 1, // after the second phase + second = 2 // the ones which passed the second phase + }; + + instance_cull(); + instance_cull(const instance_cull &) = delete; + instance_cull &operator=(const instance_cull &) = delete; + virtual ~instance_cull() = default; + + // starts collecting the buckets of a pass + void begin_pass(pass_setup const &Setup); + bool in_pass() const + { + return m_inpass; + } + bool drawing() const + { + return m_inpass && m_setup.drawing; + } + // adds the records of a bucket (made already) to the pass, sending them to the gpu when they changed. + // Area: bounds which hold the locations of all its visible instances: their own (scene_walk::Bucket_Area()) or of the cell + // holding the bucket + void add_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area); + // adds the sleepers of a track, their records made already as a bucket with no instances (only the records and the place + // in the record buffer count). Area: bounds of the locations of the sleepers. the range of the whole track is tested by + // the caller, the culling tests each sleeper against the frustum and gives it the lod band of its plain distance from the + // camera of the pass (Render_Sleepers()) + void add_sleepers(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area); + // a bucket the renderer left out of the pass, as none of its instances can pass the culling (Global.gfx_gpucull_bucketrange): only counted, + // for the panel + void skip_bucket(scene::basic_cell::instance_bucket const &Bucket); + // the last dispatch works in two phases + bool two_phase() const + { + return m_twophase; + } + // groups of the pass and the lod bounds they refer to, valid from the dispatch to end_pass() + std::vector const &draw_groups() const + { + return m_drawgroups; + } + std::vector const &lod_bounds() const + { + return m_lodlist; + } + // offset of command Index of a copy in the command buffer + std::intptr_t draw_command_offset(command_set const Set, std::size_t const Index) const + { + return static_cast((static_cast(Set) * m_commandcount + Index) * sizeof(gl::instance_draw_command)); + } + // counts of the last result which came back from the gpu, with the draws of the pass and the calls which made them (fewer + // with multi draws), for the debug panel + void draw_result(std::uint32_t const Draws, std::uint32_t const Prepassdraws, std::uint32_t const Calls, std::uint32_t const Prepasscalls, double const Cputime); + // what draw_result() was given last + std::uint32_t prepass_draws() const + { + return m_prepassdraws; + } + std::uint32_t prepass_calls() const + { + return m_prepasscalls; + } + double cpu_time() const + { + return m_cputime; + } + // instances which passed in the last result which came back from the gpu, a few frames old + std::uint32_t last_count() const + { + return static_cast(m_last.gputotal); + } + // counts per lod band of the instances Render_Instanced() drew for a model and its skins in this pass, or of the sleepers + // Render_Sleepers() drew. needs the dispatch before it + void cpu_result(scene::basic_cell::instance_bucket_key const &Key, std::vector const &Bandcounts, bool const Sleepers = false); + // the cpu drew the instance of a group of its own (Key with the instance) at a squared distance scaled as the culling + // scales it: counted in the lod band the distance falls in. needs the dispatch before it + void cpu_instance(scene::basic_cell::instance_bucket_key const &Key, float const Distancesquared); + // ends the pass: its cpu counts are kept for the comparison + void end_pass(); + // takes the newest counts of the gpu which arrived and compares them with the ones of the cpu. done when a pass begins; + // for the panel also by the culls of passes which aren't made every frame + void collect(); + // a pass was culled since the start + bool ran() const + { + return m_ran; + } + // lines for the debug panel, Label naming the pass. Occlusion: the pass may test occlusion (the main view) + std::string info(std::string const &Label = "gpu cull", bool const Occlusion = true); + + protected: + // what the cullings of all passes share of the record buffer: records placed so far, the generation which tells current places + // from the ones of a replaced buffer, the frame of the last time the buffer was started over, and whether it was + struct record_state + { + std::uint32_t recordfill{0}; + unsigned int recordgeneration{1}; + unsigned int restartframe{0}; + bool restarted{false}; + }; + // entries read back for the comparison; with more of them only the totals and these are compared + static constexpr std::uint32_t READBACK_ENTRIES{16384}; + + // what the backend does with the buffers + // size of the record buffer, bytes, and a new one of the given size (the contents are dropped) + virtual std::int64_t record_buffer_size() const = 0; + virtual void resize_record_buffer(std::int64_t const Size) = 0; + // sends records to their place in the record buffer + virtual void send_records(std::uint32_t const First, scene::basic_cell::instance_record const *Records, std::uint32_t const Count) = 0; + // the buffer is started over without growing: the records go to the places of the buffer from its start again + virtual void restart_records() {} + // the counters of the pass (INSTANCE_COUNTER_HEADER + READBACK_ENTRIES words) go back to the cpu, and the oldest of them which + // arrived: false when none did + virtual void push_counts() = 0; + virtual bool pop_counts(std::uint32_t *Counts) = 0; + + // the start of the dispatch of the first phase: false when there's nothing to cull + bool dispatch_begin(); + // planes of the frustum of the pass moved to its camera, in double precision: the cpu tests world coordinates + void frustum_planes(glm::vec4 (&Planes)[6]) const; + // the end of the dispatch of the first phase + void dispatch_end(); + // with a culling made for another pass: shares the records with it + void share_records(instance_cull const &Shared) + { + m_records = Shared.m_records; + } + + struct group_data + { + std::uint32_t index; + std::uint32_t entryfirst; + std::uint32_t entrycount; + float distancemin; + float distancemax; + }; + // counts of the cpu of a pass, waiting for those of the gpu + struct cpu_pass + { + unsigned int frame; + std::vector counts; + std::uint64_t total; + }; + struct comparison + { + unsigned int frame{0}; + std::uint64_t gputotal{0}; + std::uint64_t cputotal{0}; + std::uint32_t entries{0}; + std::uint32_t compared{0}; // entries compared, at most READBACK_ENTRIES + std::uint32_t mismatched{0}; + std::uint32_t largestdifference{0}; + std::uint32_t occluded{0}; // left out by the occlusion test + std::uint32_t first{0}; // drawn first, seen in the previous frame (two phases) + }; + + using group_map = std::unordered_map; + + // add_bucket() and add_sleepers() + void add(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area, bool const Sleepers); + // finds a place for the records of a bucket in the record buffer and sends them + void place(scene::basic_cell::instance_bucket const &Bucket); + + std::shared_ptr m_records; + + bool m_inpass{false}; + bool m_ran{false}; + pass_setup m_setup; + std::vector m_buckets; // of the jobs, in their order + std::vector m_joblist; + std::vector m_grouplist; + std::vector m_drawgroups; + std::vector m_lodlist; + group_map m_groupmap; + group_map m_sleepergroupmap; // groups of the sleepers + std::uint32_t m_candidates{0}; + std::uint32_t m_entries{0}; + // tracks with sleepers and their sleepers queued in the pass, and in the last pass, for the panel + std::uint32_t m_sleepertracks{0}; + std::uint32_t m_sleepers{0}; + std::uint32_t m_lastsleepertracks{0}; + std::uint32_t m_lastsleepers{0}; + // model instances drawn on their own queued in the pass, and in the last pass, for the panel + std::uint32_t m_models{0}; + std::uint32_t m_lastmodels{0}; + // what the pass held and sent, and the same of the last pass, for the panel: jobs (buckets queued), buckets left out beyond + // their range and their instances, records sent to the gpu, the record buffer started over + struct pass_counts + { + std::uint32_t jobs{0}; + std::uint32_t candidates{0}; + std::uint32_t skippedbuckets{0}; + std::uint64_t skippedinstances{0}; + std::uint64_t sentrecords{0}; + bool restarted{false}; + }; + pass_counts m_counts; + pass_counts m_lastcounts; + bool m_dispatched{false}; + + std::vector m_cpucounts; // of the current pass, by entry + std::deque m_cpupasses; + std::vector m_readbackdata; + comparison m_last; + std::uint64_t m_comparedframes{0}; + std::uint64_t m_mismatchedframes{0}; + // draws of the last pass drawn from the gpu + std::uint32_t m_draws{0}; + std::uint32_t m_prepassdraws{0}; + std::uint32_t m_calls{0}; + std::uint32_t m_prepasscalls{0}; + std::uint32_t m_commandcount{0}; + double m_cputime{0.0}; + bool m_occlusion{false}; // the last dispatch tested occlusion + bool m_twophase{false}; // in two phases +}; + +} // namespace gfx diff --git a/rendering/common/localshadows.cpp b/rendering/common/localshadows.cpp new file mode 100644 index 00000000..e7785c42 --- /dev/null +++ b/rendering/common/localshadows.cpp @@ -0,0 +1,648 @@ +/* +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/common/localshadows.h" + +#include "rendering/openglcamera.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/utilities.h" +#include "vehicle/DynObj.h" +#include "vehicle/Train.h" + +namespace gfx +{ + +namespace +{ + +// a lamp which moved farther than this (an animated submodel) has its scenery drawn anew, metres +double const LAMP_MOVED{0.01}; +// lamps with a shorter reach aren't worth a map, metres +float const LAMP_RANGE_MIN{1.f}; + +} // namespace + +local_shadows::level_setup local_shadows::setup(int const Level) +{ + switch (Level) + { + case 1: + return {512, 4, 0.f, 1, 1024, 2, 8}; + case 2: + return {1024, 2, 1.f, 1, 1024, 1, 4}; + case 3: + return {1024, 1, 2.f, 2, 2048, 1, 2}; + default: + return {0, 0, 0.f, 0, 0, 0, 0}; + } +} + +void local_shadows::allocate(int const Size) +{ + release(); + if (Size <= 0) + return; + if (false == gpu_allocate(Size)) + { + ErrorLog("lamp shadow map framebuffer setup failed, the lamps have no shadows"); + release(); + Global.gfx_lights_shadows = 0; + return; + } + m_size = Size; + WriteLog("lamp shadow maps: " + std::to_string(Size) + " x " + std::to_string(Size) + " cube"); +} + +void local_shadows::release() +{ + gpu_release(); + m_size = 0; + m_valid = false; +} + +void local_shadows::allocate_lamps(int const Count, int const Size) +{ + release_lamps(); + if ((Count <= 0) || (Size <= 0)) + return; + // the scenery parts and the working copies + if (false == gpu_allocate_lamps(Count, Size)) + { + ErrorLog("scenery lamp shadow map framebuffer setup failed, the scenery lamps have no shadows"); + release_lamps(); + Global.gfx_lights_shadows_lamps = 0; + return; + } + m_lampssize = Size; + m_slots.assign(static_cast(Count), lamp_slot{}); + WriteLog("scenery lamp shadow maps: " + std::to_string(Count) + " x " + std::to_string(Size) + " x " + std::to_string(Size) + " cube (" + + std::to_string(2 * Count * 6 * Size / 1024 * Size * 4 / 1024) + " MB)"); +} + +void local_shadows::release_lamps() +{ + gpu_release_lamps(); + m_lampssize = 0; + m_slots.clear(); + m_wanted.clear(); +} + +void local_shadows::allocate_headlights(int const Count, int const Size) +{ + release_headlights(); + if ((Count <= 0) || (Size <= 0)) + return; + if (false == gpu_allocate_headlights(Count, Size)) + { + ErrorLog("headlight shadow map framebuffer setup failed, the headlights have no shadows"); + release_headlights(); + Global.gfx_lights_shadows_headlights = 0; + return; + } + m_headlightssize = Size; + m_headlightslots.assign(static_cast(Count), headlight_slot{}); + WriteLog("headlight shadow maps: " + std::to_string(Count) + " x " + std::to_string(Size) + " x " + std::to_string(Size) + " (" + + std::to_string(Count * Size / 1024 * Size * 4 / 1024) + " MB)"); +} + +void local_shadows::release_headlights() +{ + gpu_release_headlights(); + m_headlightssize = 0; + m_headlightslots.clear(); + m_headlightswanted.clear(); +} + +bool local_shadows::update_wanted(TDynamicObject const *Vehicle, TModel3d const *Cab, glm::vec3 const &Lamp, unsigned int const Framestamp) +{ + auto const level{std::clamp(Global.gfx_lights_shadows, 0, 3)}; + if (level != m_level) + { + m_level = level; + auto const size{setup(level).size}; + if (size != m_size) + allocate(size); + m_valid = false; + } + if ((m_size == 0) || (Vehicle == nullptr) || (Cab == nullptr)) + { + // with the lamp off nothing reads the map; once it is on again the cab may have changed + m_valid = false; + return false; + } + if ((false == m_valid) || (Vehicle != m_vehicle) || (Cab != m_cab) || (Lamp != m_lamp)) + { + m_valid = false; + } + // the moving parts of the cab are followed every few frames + if (m_valid && (Framestamp - m_drawnframe < setup(level).interval)) + { + return false; + } + m_vehicle = Vehicle; + m_cab = Cab; + m_lamp = Lamp; + m_drawnframe = Framestamp; + return true; +} + +void local_shadows::begin() +{ + m_cpustart = std::chrono::steady_clock::now(); + gpu_begin_cab(); +} + +void local_shadows::end(std::uint32_t const Drawcalls) +{ + gpu_end_cab(); + m_valid = true; + m_drawcalls = Drawcalls; + ++m_updatecount; + auto const elapsed{std::chrono::duration(std::chrono::steady_clock::now() - m_cpustart).count()}; + m_cputime = (m_cputime == 0.0 ? elapsed : m_cputime * 0.9 + elapsed * 0.1); +} + +glm::vec4 local_shadows::weights() const +{ + // a face spans twice the distance from the lamp + return glm::vec4{static_cast(cab_slot), shadow_near, 2.f / static_cast(std::max(m_size, 1)), setup(m_level).radius}; +} + +void local_shadows::update_lamps(std::vector const &Lights, glm::dvec3 const &Camera, std::uint64_t const Version, unsigned int const Framestamp) +{ + m_frame = Framestamp; + m_wanted.clear(); + auto const level{std::clamp(Global.gfx_lights_shadows, 0, 3)}; + auto const count{level > 0 ? Global.gfx_lights_shadows_lamps : 0}; + auto const size{count > 0 ? Global.gfx_lights_shadows_lamps_size : 0}; + if ((static_cast(count) != m_slots.size()) || (size != m_lampssize)) + { + allocate_lamps(count, size); + } + if (m_slots.empty()) + { + return; + } + m_sceneryleft = setup(level).sceneryperframe; + auto const reach{static_cast(Global.gfx_lights_shadows_lamps_range)}; + // the lights come the most important first: the first ones within reach get the slots + std::vector picked; + for (auto const &light : Lights) + { + if (picked.size() >= m_slots.size()) + { + break; + } + if ((light.owner == nullptr) || (light.range < LAMP_RANGE_MIN) || (glm::length(light.position - Camera) > reach)) + { + continue; + } + picked.emplace_back(&light); + } + // lamps which already have a slot keep it + std::vector unplaced; + for (auto const *light : picked) + { + auto placed{false}; + for (std::size_t idx = 0; idx < m_slots.size(); ++idx) + { + auto &slot{m_slots[idx]}; + if ((slot.owner == light->owner) && (slot.part == light->part) && (slot.wantedframe != Framestamp)) + { + if (glm::length(slot.position - light->position) > LAMP_MOVED) + { + slot.scenery = false; + slot.vehicles = false; + } + slot.position = light->position; + slot.range = light->range; + slot.wantedframe = Framestamp; + m_wanted.emplace_back(idx); + placed = true; + break; + } + } + if (false == placed) + { + unplaced.emplace_back(light); + } + } + // the others take the slots of lamps no longer picked + for (auto const *light : unplaced) + { + for (std::size_t idx = 0; idx < m_slots.size(); ++idx) + { + auto &slot{m_slots[idx]}; + if (slot.wantedframe == Framestamp) + { + continue; + } + slot = lamp_slot{}; + slot.owner = light->owner; + slot.part = light->part; + slot.position = light->position; + slot.range = light->range; + slot.wantedframe = Framestamp; + m_wanted.emplace_back(idx); + break; + } + } + // the scene changed (events, the editor): the scenery parts are out of date + for (auto &slot : m_slots) + { + if (slot.scenery && (slot.version != Version)) + { + slot.scenery = false; + slot.vehicles = false; + } + } +} + +void local_shadows::reset_lamps() +{ + for (auto &slot : m_slots) + { + slot = lamp_slot{}; + } + m_wanted.clear(); +} + +bool local_shadows::scenery_wanted(std::size_t const Slot) +{ + if (m_slots[Slot].scenery || (m_sceneryleft <= 0)) + { + return false; + } + --m_sceneryleft; + return true; +} + +bool local_shadows::vehicles_wanted(std::size_t const Slot, unsigned int const Framestamp) const +{ + auto const &slot{m_slots[Slot]}; + return slot.scenery && ((false == slot.vehicles) || (Framestamp - slot.vehiclesframe >= setup(m_level).interval)); +} + +void local_shadows::begin_lamp(std::size_t const Slot, bool const Vehicles, std::uint64_t const Version, unsigned int const Framestamp) +{ + auto &slot{m_slots[Slot]}; + auto const scenerycube{static_cast(Slot)}; + auto const workingcube{static_cast(m_slots.size() + Slot)}; + if (Vehicles) + { + // the vehicles go over the scenery + slot.vehicles = true; + slot.vehiclesframe = Framestamp; + ++m_lampsvehicles; + } + else + { + slot.scenery = true; + slot.version = Version; + slot.vehicles = false; + ++m_lampsscenery; + } + gpu_begin_lamp(scenerycube, workingcube, Vehicles); +} + +void local_shadows::end_lamp(std::uint32_t const Drawcalls) +{ + gpu_end_lamp(); + m_lampsframedrawcalls += Drawcalls; +} + +void local_shadows::lamps_timer_begin() +{ + m_lampsframedrawcalls = 0; + m_cpustart = std::chrono::steady_clock::now(); + gpu_lamps_timer(true); + m_lampstiming = true; +} + +void local_shadows::lamps_timer_end() +{ + if (false == m_lampstiming) + { + return; + } + gpu_lamps_timer(false); + m_lampstiming = false; + m_lampsdrawcalls = m_lampsframedrawcalls; + auto const elapsed{std::chrono::duration(std::chrono::steady_clock::now() - m_cpustart).count()}; + m_lampscputime = (m_lampscputime == 0.0 ? elapsed : m_lampscputime * 0.9 + elapsed * 0.1); +} + +local_shadows::lamp_slot const *local_shadows::lamp(gfx::local_lights::light const &Light, std::size_t &Slot) const +{ + if (Light.owner == nullptr) + { + return nullptr; + } + for (auto const idx : m_wanted) + { + auto const &slot{m_slots[idx]}; + if ((slot.owner == Light.owner) && (slot.part == Light.part)) + { + if (false == slot.scenery) + { + return nullptr; + } + Slot = idx; + return &slot; + } + } + return nullptr; +} + +glm::vec4 local_shadows::lamp_weights(std::size_t const Slot) const +{ + auto const cube{m_slots[Slot].vehicles ? m_slots.size() + Slot : Slot}; + return glm::vec4{static_cast(cube), shadow_near, 2.f / static_cast(std::max(m_lampssize, 1)), setup(m_level).radius}; +} + +std::vector const &local_shadows::lamp_vehicles(glm::dvec3 const &Camera) +{ + m_lampvehicles.clear(); + auto const vehiclesrange{static_cast(Global.gfx_lights_shadows_vehicles_range)}; + if (vehiclesrange > 0.0) + { + for (auto *vehicle : simulation::Vehicles.sequence()) + { + if (glm::length(vehicle->GetPosition() - Camera) <= vehiclesrange + vehicle->radius()) + m_lampvehicles.emplace_back(vehicle); + } + } + return m_lampvehicles; +} + +std::vector const &local_shadows::slot_vehicles(lamp_slot const &Slot) +{ + m_slotvehicles.clear(); + for (auto *vehicle : m_lampvehicles) + { + if (glm::length(vehicle->GetPosition() - Slot.position) <= Slot.range + vehicle->radius()) + m_slotvehicles.emplace_back(vehicle); + } + return m_slotvehicles; +} + +std::vector const &local_shadows::pick_headlights(light_array const &Lights, opengl_camera const &Camera) +{ + m_headlightpicked.clear(); + auto const count{(Global.gfx_lights_headlights && (std::clamp(Global.gfx_lights_shadows, 0, 3) > 0)) ? std::clamp(Global.gfx_lights_shadows_headlights, 0, static_cast(gl::MAX_HEADLIGHT_SHADOWS)) : 0}; + if (count > 0) + { + m_headlightcandidates.clear(); + auto const camera{Camera.position()}; + auto const range{static_cast(Global.gfx_lights_shadows_headlights_range)}; + for (auto const &light : Lights.data) + { + if (light.intensity == 0.f) + continue; + // as in the light buffers: by day the light around takes the beam down to nothing + auto luminance{static_cast(Global.fLuminance)}; + if (light.owner->fShade > 0.f) + luminance *= light.owner->fShade; + auto const beam{light.color - glm::vec3{luminance}}; + if (std::max(beam.r, std::max(beam.g, beam.b)) <= 0.f) + continue; + // the part of the beam the shadows are cast in: from the end of the vehicle to the reach ahead of it + auto const &dimensions{light.owner->MoverParameters->Dim}; + auto const radius{range * 0.5 + std::max(dimensions.W, dimensions.H) + 2.0}; + auto const centre{light.position + glm::dvec3{light.direction} * (range * 0.5 + 2.0)}; + if (glm::length(centre - camera) - radius > range) + continue; + if (false == Camera.visible(scene::bounding_area{centre, static_cast(radius)})) + continue; + m_headlightcandidates.emplace_back(static_cast(glm::length2(camera - light.position)) / light.intensity, &light); + } + auto const picked{std::min(m_headlightcandidates.size(), static_cast(count))}; + std::partial_sort(std::begin(m_headlightcandidates), std::begin(m_headlightcandidates) + picked, std::end(m_headlightcandidates), + [](auto const &Left, auto const &Right) { return Left.first < Right.first; }); + for (std::size_t idx = 0; idx < picked; ++idx) + { + m_headlightpicked.emplace_back(m_headlightcandidates[idx].second); + } + } + return m_headlightpicked; +} + +void local_shadows::update_headlights(std::vector const &Lights, unsigned int const Framestamp) +{ + m_headlightswanted.clear(); + auto const level{std::clamp(Global.gfx_lights_shadows, 0, 3)}; + auto const count{level > 0 ? std::clamp(Global.gfx_lights_shadows_headlights, 0, static_cast(gl::MAX_HEADLIGHT_SHADOWS)) : 0}; + auto const size{count > 0 ? setup(level).headlightsize : 0}; + if ((static_cast(count) != m_headlightslots.size()) || (size != m_headlightssize)) + { + allocate_headlights(count, size); + } + if (m_headlightslots.empty()) + { + return; + } + // sets which already have a slot keep it + std::vector unplaced; + for (auto const *light : Lights) + { + if (m_headlightswanted.size() + unplaced.size() >= m_headlightslots.size()) + { + break; + } + auto placed{false}; + for (std::size_t idx = 0; idx < m_headlightslots.size(); ++idx) + { + auto &slot{m_headlightslots[idx]}; + if ((slot.owner == light->owner) && (slot.index == light->index) && (slot.wantedframe != Framestamp)) + { + slot.wantedframe = Framestamp; + m_headlightswanted.emplace_back(idx); + placed = true; + break; + } + } + if (false == placed) + { + unplaced.emplace_back(light); + } + } + // the others take the slots of sets no longer picked + for (auto const *light : unplaced) + { + for (std::size_t idx = 0; idx < m_headlightslots.size(); ++idx) + { + auto &slot{m_headlightslots[idx]}; + if (slot.wantedframe == Framestamp) + { + continue; + } + slot = headlight_slot{}; + slot.owner = light->owner; + slot.index = light->index; + slot.wantedframe = Framestamp; + m_headlightswanted.emplace_back(idx); + break; + } + } +} + +void local_shadows::reset_headlights() +{ + for (auto &slot : m_headlightslots) + { + slot = headlight_slot{}; + } + m_headlightswanted.clear(); +} + +bool local_shadows::headlight_wanted(std::size_t const Slot, bool const Moved, unsigned int const Framestamp) const +{ + auto const &slot{m_headlightslots[Slot]}; + auto const level{setup(m_level)}; + return (false == slot.drawn) || (Framestamp - slot.drawnframe >= (Moved ? level.headlightmoving : level.headlightstill)); +} + +void local_shadows::begin_headlight(std::size_t const Slot, glm::dvec3 const &Projector, glm::mat3 const &Rotation, glm::mat4 const &Projection, glm::vec3 const &Direction, unsigned int const Framestamp) +{ + auto &slot{m_headlightslots[Slot]}; + slot.drawn = true; + slot.drawnframe = Framestamp; + slot.projector = Projector; + slot.rotation = Rotation; + slot.projection = Projection; + slot.direction = Direction; + ++m_headlightsdrawn; + gpu_begin_headlight(Slot); +} + +void local_shadows::end_headlight(std::uint32_t const Drawcalls) +{ + gpu_end_headlight(); + m_headlightsframedrawcalls += Drawcalls; +} + +void local_shadows::headlights_timer_begin() +{ + m_headlightsframedrawcalls = 0; + m_headlightscpustart = std::chrono::steady_clock::now(); + gpu_headlights_timer(true); + m_headlightstiming = true; +} + +void local_shadows::headlights_timer_end() +{ + if (false == m_headlightstiming) + { + return; + } + gpu_headlights_timer(false); + m_headlightstiming = false; + m_headlightsdrawcalls = m_headlightsframedrawcalls; + auto const elapsed{std::chrono::duration(std::chrono::steady_clock::now() - m_headlightscpustart).count()}; + m_headlightscputime = (m_headlightscputime == 0.0 ? elapsed : m_headlightscputime * 0.9 + elapsed * 0.1); +} + +int local_shadows::headlight_map(TDynamicObject const *Owner, int const Index) const +{ + for (auto const idx : m_headlightswanted) + { + auto const &slot{m_headlightslots[idx]}; + if ((slot.owner == Owner) && (slot.index == Index)) + { + return (slot.drawn ? static_cast(idx) : -1); + } + } + return -1; +} + +glm::vec4 local_shadows::headlight_params(std::size_t const Slot) const +{ + // a map spans 2 tan(fov / 2) one metre from the projector, and the projection holds 1 / tan(fov / 2) + auto const &projection{m_headlightslots[Slot].projection}; + return glm::vec4{2.f / (std::max(projection[1][1], 1e-6f) * static_cast(std::max(m_headlightssize, 1))), setup(m_level).radius, 0.f, 0.f}; +} + +TDynamicObject *local_shadows::shadow_cab() +{ + if (FreeFlyModeFlag || (false == Global.render_cab) || (simulation::Train == nullptr)) + return nullptr; + auto *cab{simulation::Train->Dynamic()}; + if ((cab != nullptr) && ((false == cab->bDisplayCab) || (cab->mdKabina == nullptr) || (cab->mdKabina == cab->mdModel))) + cab = nullptr; + return cab; +} + +std::string local_shadows::info() const +{ + std::string text; + if (m_size != 0) + { + text += "\nlamp shadows: " + std::to_string(m_size) + " cube, " + (m_valid ? "cab lamp" : "none") + ", " + std::to_string(m_drawcalls) + " drawcalls, cpu " + to_string(m_cputime, 2) + + " ms"; + } + if (false == m_slots.empty()) + { + auto withvehicles{0}; + auto ready{0}; + for (auto const idx : m_wanted) + { + ready += (m_slots[idx].scenery ? 1 : 0); + withvehicles += (m_slots[idx].vehicles ? 1 : 0); + } + text += "\nscenery lamp shadows: " + std::to_string(m_wanted.size()) + " of " + std::to_string(m_slots.size()) + " slots (" + std::to_string(ready) + " ready, " + + std::to_string(withvehicles) + " with vehicles), drawn " + std::to_string(m_lampsscenery) + " scenery + " + std::to_string(m_lampsvehicles) + " vehicles, " + + std::to_string(m_lampsdrawcalls) + " drawcalls, cpu " + to_string(m_lampscputime, 2) + " ms"; + } + if (false == m_headlightslots.empty()) + { + auto ready{0}; + for (auto const idx : m_headlightswanted) + { + ready += (m_headlightslots[idx].drawn ? 1 : 0); + } + text += "\nheadlight shadows: " + std::to_string(m_headlightswanted.size()) + " of " + std::to_string(m_headlightslots.size()) + " slots (" + std::to_string(ready) + " ready), " + + std::to_string(m_headlightssize) + " map, drawn " + std::to_string(m_headlightsdrawn) + " times, " + std::to_string(m_headlightsdrawcalls) + " drawcalls, cpu " + + to_string(m_headlightscputime, 2) + " ms"; + } + return text; +} + +void local_shadows::draw_debug_ui() +{ + char const *levels[]{"no", "low (512, every 4th frame)", "medium (1024, every 2nd frame)", "high (1024, every frame, softer)"}; + ImGui::Combo("lamp shadows (gfx.lights.shadows)", &Global.gfx_lights_shadows, levels, IM_ARRAYSIZE(levels)); + if (m_size != 0) + { + ImGui::Text("cab: cube %d x %d, %s, drawn %u times, %u drawcalls, %.2f ms (cpu)", m_size, m_size, m_valid ? "cab lamp" : "no lamp", m_updatecount, m_drawcalls, m_cputime); + } + ImGui::SliderInt("scenery lamps with shadows (gfx.lights.shadows.lamps)", &Global.gfx_lights_shadows_lamps, 0, 32); + ImGui::SliderFloat("their reach from the camera, m (.lamps.range)", &Global.gfx_lights_shadows_lamps_range, 0.f, 500.f); + ImGui::SliderFloat("vehicles casting them, from the camera, m (.vehicles.range)", &Global.gfx_lights_shadows_vehicles_range, 0.f, 500.f); + if (false == m_slots.empty()) + { + auto const text{info()}; + auto const line{text.find("\nscenery lamp shadows:")}; + auto const end{(line == std::string::npos) ? std::string::npos : text.find('\n', line + 1)}; + if (line != std::string::npos) + ImGui::TextUnformatted(text.substr(line + 1, end == std::string::npos ? std::string::npos : end - line - 1).c_str()); + } + ImGui::SliderInt("headlights with shadows (gfx.lights.shadows.headlights)", &Global.gfx_lights_shadows_headlights, 0, static_cast(gl::MAX_HEADLIGHT_SHADOWS)); + ImGui::SliderFloat("their reach ahead of the vehicle, m (.headlights.range)", &Global.gfx_lights_shadows_headlights_range, 10.f, 1000.f); + if (false == m_headlightslots.empty()) + { + auto const text{info()}; + auto const line{text.rfind('\n')}; + ImGui::TextUnformatted(text.c_str() + (line == std::string::npos ? 0 : line + 1)); + } +} + +} // namespace gfx diff --git a/rendering/common/localshadows.h b/rendering/common/localshadows.h new file mode 100644 index 00000000..9d3c4c52 --- /dev/null +++ b/rendering/common/localshadows.h @@ -0,0 +1,288 @@ +/* +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 + +#include "rendering/common/locallights.h" +#include "rendering/lightarray.h" + +class TDynamicObject; +class TModel3d; +class opengl_camera; + +namespace gfx +{ + +// the shadow maps of the lamps (Global.gfx_lights_shadows), the part shared by the renderers (stage P4 of +// claude/vulkan14-renderer-plan.md): which lamps and headlights get maps, when they are drawn, what the shading reads; the +// backend (opengl46_local_shadows, the vulkan renderer) keeps the maps and draws to them through the gpu_* methods. +// - the lamp of the occupied cab, with the shadows cast by the cab (the model drawn by Render_cab()). the map is kept in the space +// of the cab model, centred on the lamp: it stays right while the vehicle moves and turns, and is drawn anew only every few frames +// (for the moving parts of the cab: levers, doors, wipers) or when the lamp, the cab or the level change; +// - the lamps of scenery models closest to the camera (Global.gfx_lights_shadows_lamps of them, within +// Global.gfx_lights_shadows_lamps_range). the map of such a lamp is kept in world axes, centred on the lamp, and has two parts: +// the scenery (drawn once, when the lamp gets its slot or the scene changes) and a working copy of it, with the vehicles near +// the camera (within Global.gfx_lights_shadows_vehicles_range) and the occupied cab added, made every few frames while there +// are any by the lamp. the shading reads the working copy then, the scenery part otherwise. +// the six faces of a map are drawn at once, so a model costs one draw call per submodel, as in the colour pass. the depth is +// n / d (d the distance along the axis of the face), see local_light_shadow() in light_common.glsl, which reads the map for the +// light with the layer in its headlight_weights.x, from the array of scenery lamps when its linear is 1 and from the one of the +// cab lamp otherwise. +// - the headlights of the vehicles (Global.gfx_lights_shadows_headlights sets of them, the most important ones): a plain map, +// drawn from the projector of the beam with its projection (the beam is a narrow frustum aimed along the vehicle), with what is +// within Global.gfx_lights_shadows_headlights_range ahead of the vehicle, but not the vehicle itself. the map keeps the projector +// it was drawn with, in world space: one a few frames old is still right for what doesn't move. read by headlight_beam_shadow() +// in light_common.glsl, for the headlights with the map in their headlight_weights.w (1 + the layer) and the projection to it in +// gl::scene_ubs::headlight_shadow +class local_shadows +{ + public: + // near plane of the maps, metres. surfaces closer to the lamp (its own fitting) cast no shadow + static constexpr float shadow_near{0.15f}; + // layer of the map of the cab lamp in its array + static constexpr int cab_slot{0}; + + local_shadows() = default; + virtual ~local_shadows() = default; + local_shadows(const local_shadows &) = delete; + local_shadows &operator=(const local_shadows &) = delete; + + // the cab lamp + + // whether the map of the cab lamp is to be drawn in this frame: Vehicle and Cab the occupied vehicle and its cab model, + // Lamp the position of the lamp in the space of the cab model; Vehicle nullptr when the lamp is off. (re)makes the map to + // the size of the level when the level changed + bool update_wanted(TDynamicObject const *Vehicle, TModel3d const *Cab, glm::vec3 const &Lamp, unsigned int const Framestamp); + // sets up the drawing to the map: the target, viewport, cleared faces, the state of the faces. the geometry is to be + // drawn with the programs of the cube maps and a modelview from the space of the model to the space of the map + void begin(); + // ends the drawing to the map + void end(std::uint32_t const Drawcalls); + // whether the map holds the shadows of the lamp of Vehicle, the occupied vehicle + bool ready(TDynamicObject const *Vehicle) const + { + return m_valid && (Vehicle != nullptr) && (Vehicle == m_vehicle); + } + // position of the lamp the map was drawn for, in the space of the cab model + glm::vec3 const &lamp() const + { + return m_lamp; + } + // the headlight_weights of the light which reads the map: layer, near plane, size of a texel one metre from the lamp, + // radius of the filter in texels + glm::vec4 weights() const; + + // lamps of scenery models + + // a lamp with a slot + struct lamp_slot + { + void const *owner{nullptr}; // the light it is, see gfx::local_lights::light + void const *part{nullptr}; + glm::dvec3 position{0.0}; // world space + float range{0.f}; // of the light, metres + unsigned int wantedframe{~0u}; // framestamp it was last among the lamps given shadows + bool scenery{false}; // the scenery part of the map is drawn + std::uint64_t version{0}; // of the scene the scenery part was drawn for + bool vehicles{false}; // the working copy is drawn, with vehicles, and is what the shading reads + unsigned int vehiclesframe{0}; // framestamp the working copy was last drawn in + }; + // picks the lamps which get shadows in this frame from the gathered lights (the most important first): the scenery lamps + // within reach of Camera. a lamp keeps its slot while it is picked; one which got a slot or moved, and all of them when the + // scene changes (Version), need their scenery drawn. (re)makes the maps when their count or size changed + void update_lamps(std::vector const &Lights, glm::dvec3 const &Camera, std::uint64_t const Version, unsigned int const Framestamp); + // forgets the lamps: the scene they come from is about to be replaced (a scenery being loaded) + void reset_lamps(); + // slots of the lamps picked in this frame + std::vector const &lamps() const + { + return m_wanted; + } + lamp_slot const &slot(std::size_t const Slot) const + { + return m_slots[Slot]; + } + // whether the scenery part of the map of the slot is to be drawn in this frame. a few per frame at most, by the level + bool scenery_wanted(std::size_t const Slot); + // whether the working copy of the slot, with vehicles, is to be drawn in this frame (every few frames, by the level) + bool vehicles_wanted(std::size_t const Slot, unsigned int const Framestamp) const; + // the slot has no vehicles by it: the shading reads the scenery part + void no_vehicles(std::size_t const Slot) + { + m_slots[Slot].vehicles = false; + } + // sets up the drawing to the scenery part (cleared) or to the working copy (a copy of the scenery part) of the map of the slot. + // the geometry is to be drawn with the programs of the cube maps and a modelview to world axes centred on the lamp + void begin_lamp(std::size_t const Slot, bool const Vehicles, std::uint64_t const Version, unsigned int const Framestamp); + // ends the drawing to the map of a lamp + void end_lamp(std::uint32_t const Drawcalls); + // the timing of the drawing of the maps of the lamps in this frame + void lamps_timer_begin(); + void lamps_timer_end(); + // the slot of the lamp the light is, if it has its shadows ready, nullptr otherwise + lamp_slot const *lamp(gfx::local_lights::light const &Light, std::size_t &Slot) const; + // the headlight_weights of the light of the lamp in the slot, see weights() + glm::vec4 lamp_weights(std::size_t const Slot) const; + // the vehicles near the camera, which cast the shadows of the lamps (Global.gfx_lights_shadows_vehicles_range), and those by + // the lamp of a slot, within its reach; valid until the next call + std::vector const &lamp_vehicles(glm::dvec3 const &Camera); + std::vector const &slot_vehicles(lamp_slot const &Slot); + std::vector const &slot_vehicles() const + { + return m_slotvehicles; + } + + // headlights of vehicles + + // a set of headlights (one end of a vehicle) with a slot + struct headlight_slot + { + TDynamicObject const *owner{nullptr}; // light_array::light_record::owner and index + int index{-1}; + unsigned int wantedframe{~0u}; // framestamp it was last among the headlights given shadows + bool drawn{false}; // the map holds the shadows of the headlights + unsigned int drawnframe{0}; + // the projector the map was drawn with: world space position, world axes to its axes, projection + glm::dvec3 projector{0.0}; + glm::mat3 rotation{1.f}; + glm::mat4 projection{1.f}; + glm::vec3 direction{0.f}; // of the light when the map was drawn + }; + // the headlights which shine, the most important first (in the order the light buffers take them), whose beam may be seen + // from the camera of the colour pass within the reach of the shadows, as many as there are to be slots; valid until the next call + std::vector const &pick_headlights(light_array const &Lights, opengl_camera const &Camera); + // picks the headlights which get shadows in this frame: Lights, the most important first, as many as there are slots + // (Global.gfx_lights_shadows_headlights). a set keeps its slot while it is picked. (re)makes the maps when their count or size changed + void update_headlights(std::vector const &Lights, unsigned int const Framestamp); + // forgets the headlights: the vehicles may be about to go (a scenery being loaded) + void reset_headlights(); + // slots of the headlights picked in this frame + std::vector const &headlights() const + { + return m_headlightswanted; + } + headlight_slot const &headlight(std::size_t const Slot) const + { + return m_headlightslots[Slot]; + } + // whether the map of the slot is to be drawn in this frame: one never drawn, then every few frames by the level, more often + // while the headlights Moved (with the vehicle) + bool headlight_wanted(std::size_t const Slot, bool const Moved, unsigned int const Framestamp) const; + // sets up the drawing to the map of the slot (cleared), drawn from the projector given (see headlight_slot). the geometry is + // to be drawn by the shadow pass with this projection, back faces only + void begin_headlight(std::size_t const Slot, glm::dvec3 const &Projector, glm::mat3 const &Rotation, glm::mat4 const &Projection, glm::vec3 const &Direction, unsigned int const Framestamp); + // ends the drawing to the map of a set of headlights + void end_headlight(std::uint32_t const Drawcalls); + // the timing of the drawing of the maps of the headlights in this frame + void headlights_timer_begin(); + void headlights_timer_end(); + // the slot of the headlights with their map ready in this frame, -1 if there is none + int headlight_map(TDynamicObject const *Owner, int const Index) const; + // the headlight_shadow_params of the map of the slot, see gl::scene_ubs + glm::vec4 headlight_params(std::size_t const Slot) const; + + // the occupied vehicle whose cab casts the shadows of the lamps and headlights, nullptr when it's not drawn + static TDynamicObject *shadow_cab(); + + // lines for the statistics of the debug panel + std::string info() const; + // controls for the debug window + void draw_debug_ui(); + + protected: + // size, refresh interval (frames), filter radius (texels, 0: the 2x2 filter of the hardware) of a level, the number of + // scenery parts of lamp maps drawn per frame, and the size and refresh intervals (frames, while the vehicle moves and while + // it doesn't) of the maps of headlights + struct level_setup + { + int size; + unsigned int interval; + float radius; + int sceneryperframe; + int headlightsize; + unsigned int headlightmoving; + unsigned int headlightstill; + }; + static level_setup setup(int const Level); + + // what the backend does with the maps: makes them (false: they can't be made) and lets them go; the cab lamp: one cube, the + // lamps: 2 x Count cubes (the scenery parts, then the working copies), the headlights: Count layers + virtual bool gpu_allocate(int const Size) = 0; + virtual void gpu_release() = 0; + virtual bool gpu_allocate_lamps(int const Count, int const Size) = 0; + virtual void gpu_release_lamps() = 0; + virtual bool gpu_allocate_headlights(int const Count, int const Size) = 0; + virtual void gpu_release_headlights() = 0; + // the drawing to the map of the cab lamp, to a cube of the lamps (Vehicles: the working copy, from a copy of the scenery part + // Scenerycube; the scenery part cleared otherwise) and to a layer of the headlights + virtual void gpu_begin_cab() = 0; + virtual void gpu_end_cab() = 0; + virtual void gpu_begin_lamp(int const Scenerycube, int const Workingcube, bool const Vehicles) = 0; + virtual void gpu_end_lamp() = 0; + virtual void gpu_begin_headlight(std::size_t const Slot) = 0; + virtual void gpu_end_headlight() = 0; + // the gpu times of the drawings of a frame + virtual void gpu_lamps_timer(bool const Begin) = 0; + virtual void gpu_headlights_timer(bool const Begin) = 0; + + void allocate(int const Size); + void release(); + void allocate_lamps(int const Count, int const Size); + void release_lamps(); + void allocate_headlights(int const Count, int const Size); + void release_headlights(); + + int m_level{-1}; // level the maps were made for + // cab lamp + int m_size{0}; // of a face of the map, 0 when there is none + bool m_valid{false}; // the map holds the shadows of the lamp of m_vehicle + TDynamicObject const *m_vehicle{nullptr}; + TModel3d const *m_cab{nullptr}; + glm::vec3 m_lamp{0.f}; + unsigned int m_drawnframe{0}; // framestamp the map was last drawn in + // scenery lamps: the scenery parts of the slots first, then their working copies + int m_lampssize{0}; + std::vector m_slots; + std::vector m_wanted; + unsigned int m_frame{0}; // framestamp of the last update_lamps() + int m_sceneryleft{0}; // scenery parts which may still be drawn in this frame + bool m_lampstiming{false}; + std::vector m_lampvehicles; + std::vector m_slotvehicles; + // headlights: a layer per slot + int m_headlightssize{0}; + std::vector m_headlightslots; + std::vector m_headlightswanted; + bool m_headlightstiming{false}; + std::chrono::steady_clock::time_point m_headlightscpustart; + std::vector> m_headlightcandidates; + std::vector m_headlightpicked; + // statistics + std::uint32_t m_drawcalls{0}; // of the last drawing of the cab map + std::uint32_t m_updatecount{0}; // drawings of the cab map so far + double m_cputime{0.0}; // milliseconds, smoothed + std::chrono::steady_clock::time_point m_cpustart; + std::uint32_t m_lampsscenery{0}; // scenery parts drawn so far + std::uint32_t m_lampsvehicles{0}; // working copies drawn so far + std::uint32_t m_lampsdrawcalls{0}; // in the drawings of the lamp maps in the last frame with any + std::uint32_t m_lampsframedrawcalls{0}; + double m_lampscputime{0.0}; // milliseconds per frame, smoothed + std::uint32_t m_headlightsdrawn{0}; // maps of headlights drawn so far + std::uint32_t m_headlightsdrawcalls{0}; // in the drawings of the maps of headlights in the last frame with any + std::uint32_t m_headlightsframedrawcalls{0}; + double m_headlightscputime{0.0}; // milliseconds per frame, smoothed +}; + +} // namespace gfx diff --git a/rendering/common/scenewalk.h b/rendering/common/scenewalk.h index 517c98c1..d172e311 100644 --- a/rendering/common/scenewalk.h +++ b/rendering/common/scenewalk.h @@ -26,7 +26,10 @@ http://mozilla.org/MPL/2.0/. #include "world/TrackSleepers.h" #include "model/material.h" #include "gl/ubo.h" +#include "rendering/common/instancecull.h" +#include +#include #include #include #include @@ -230,32 +233,65 @@ class scene_walk virtual void walk_region_begin() {} // the opaque ground of the colour pass is drawn, the editor takes the position of the cursor from the depth virtual void walk_editor_cursor() {} - // a bucket of instances of a cell: false when the backend took the instances (the walk leaves them out); Area is the cell's - virtual bool walk_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, bool const Bucketrange, scene::bounding_area const &Area) - { - return true; - } + // a bucket of instances of a cell: false when the gpu culling took the instances (the walk leaves them out); Area is the cell's + bool walk_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, bool const Bucketrange, scene::bounding_area const &Area); // a bucket left out of the pass as wholly out of sight - virtual void walk_bucket_culled(scene::basic_cell::instance_bucket const &Bucket) {} - // a model drawn on its own: true when the backend took it. Inrange: the cell of the instance lies within the range of the pass - virtual bool walk_queue_model(TAnimModel *Instance, bool const Inrange) - { - return false; - } - // the sleepers of a track: true when the backend took them - virtual bool walk_queue_sleepers(TTrack *Track) - { - return false; - } + void walk_bucket_culled(scene::basic_cell::instance_bucket const &Bucket); + // a model drawn on its own: true when the gpu culling took it. Inrange: the cell of the instance lies within the range of the pass + bool walk_queue_model(TAnimModel *Instance, bool const Inrange); + // the sleepers of a track: true when the gpu culling took them + bool walk_queue_sleepers(TTrack *Track); // the sleepers the walk drew for a track, by their squared distances, and the lod bounds of their model - virtual void walk_sleepers_drawn(TTrack const &Track, std::vector const &Lodbounds, std::vector> const &Drawn) {} + void walk_sleepers_drawn(TTrack const &Track, std::vector const &Lodbounds, std::vector> const &Drawn); // the instances of the cells are about to be drawn: whether the walk reports what it drew of each bucket (walk_instances_compare()) virtual bool walk_instances_begin() { return false; } - virtual void walk_instances_compare(scene::basic_cell::instance_bucket_key const &Key, std::vector const &Bandcounts) {} + void walk_instances_compare(scene::basic_cell::instance_bucket_key const &Key, std::vector const &Bandcounts); virtual void walk_instances_end() {} + // the geometry of the backend and whether it draws blended now, for the draws recorded for the gpu culling + virtual gfx::geometrybank_manager &walk_geometry() = 0; + virtual bool walk_blending_enabled() const = 0; + + // the gpu culling of the instances (Global.gfx_gpucull, stage N6c): what the walk queues in the culling of the pass and the + // draws it records for the indirect draws made from its result. the backend picks the culling of the pass, runs it and + // makes the draws + // starts the culling of the pass, or none (nullptr). Mainview: the culling of the main view, which takes the sleepers + void Begin_Pass_Cull(gfx::instance_cull *Cull, bool const Mainview); + // with the cpu drawing (Global.gfx_gpucull 2): what it drew of the sleepers and of the models drawn on their own, for the + // comparison with the culling dispatched already + void Compare_Pass_Cull(); + // ends the culling of the pass + void End_Pass_Cull(); + // queues the sleepers of a track in the gpu culling of the pass (Global.gfx_gpucull_sleepers), with the gates of + // Render_Sleepers(), making their records first when they're missing or out of date + void Queue_Sleepers(TTrack *Track); + // queues a model instance drawn on its own in the gpu culling of the pass (Global.gfx_gpucull_models), as a bucket of + // its own, making its record first when it's missing or out of date. Inrange: the cell of the instance lies within the + // range of the pass. returns true when the instance isn't drawn by the cpu in the pass: it's drawn from the result of the + // culling, or it was culled and recorded by the depth pass of the whole scene, or it lies out of range + bool Queue_Model(TAnimModel *Instance, bool const Inrange); + // whether a model with its skins has any part the sun shadows draw: the same test Render_Instanced() makes. the answer + // is kept for the rest of the pass + bool Instances_Cast_Shadow(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket); + // the walk of the submodel trees of the groups culled by Cull, once per lod band, which records their draws + // (m_indirectdraws, m_indirectcommands). Cascade: the pass is one of the cascades of the sun shadows, which share the walks + void Walk_Indirect_Draws(gfx::instance_cull const &Cull, bool const Cascade); + // a draw of the walk of Walk_Indirect_Draws(), with the state the walk left for it (model_ubs with the modelview set) + void Record_Indirect_Draw(gfx::geometry_handle const &Geometry); + // multi draws of the recorded draws (Global.gfx_gpucull_multidraw): puts the draws in an order in which the ones with the + // same state follow one another, the draws of one entry kept in the order of the walk + void Order_Indirect_Draws(); + // whether two recorded draws can go in one multi draw of the colour pass: the same material and texture variant, geometry + // bank, primitive and blending + bool Indirect_Mergeable(std::size_t const First, std::size_t const Other) const; + // the texture the shadow material binds for the alpha test of a material, or null_handle (no test) + texture_handle Indirect_Shadow_Texture(material_handle const Material) const; + // the records of the sleepers and of the models drawn on their own not queued for a while are let go + void Expire_Indirect_Records(); + // a scenery is being loaded: the tracks and the models the records were made for may be gone + void Clear_Indirect_Records(); // members renderpass_config m_renderpass; // parameters for current render pass @@ -320,6 +356,150 @@ class scene_walk scene::basic_cell::instance_bucket_key const *m_modelcomparekey{nullptr}; std::vector> m_modelcpudistances; bool m_indirectchanging{false}; // the walk under way met a submodel which may look different in the next frame + // the gpu culling of the pass under way (Begin_Pass_Cull()), and whether there's one + gfx::instance_cull *m_passcull{nullptr}; + bool m_instancecullpass{false}; + bool m_modelspass{false}; // the models drawn on their own of the pass under way go to its gpu culling + bool m_indirectcolourmodels{false}; // with m_indirectcolourpending: the depth pass culled and recorded them too + bool m_sleeperspass{false}; // the sleepers of the pass under way go to its gpu culling (the one of the main view) + bool m_indirectcoloursleepers{false}; // with m_indirectcolourpending: the depth pass culled and recorded the sleepers too + // the instances were culled in the depth pass of the whole scene, the colour pass draws them from the recorded draws + bool m_indirectcolourpending{false}; + // Instances_Cast_Shadow() of the groups of the shadow pass under way + std::unordered_map m_shadowcasters; + // the sleepers of tracks culled on the gpu with the instances of the main view (Global.gfx_gpucull_sleepers): records made + // from TTrack::m_sleeper_local_transforms when the track is queued first, and again when they or the instance records + // (editor, m_instancerecordsversion) went out of date. the records of tracks not queued for a while are let go + struct sleeper_records + { + scene::basic_cell::instance_bucket bucket; // the records only, no instances + scene::basic_cell::instance_bucket_key key; // the model, and the skin in the slot Render_Sleepers() sets it in + scene::bounding_area area; // of the locations of the sleepers + TModel3d const *model{nullptr}; // what the records were made from + material_handle skin{null_handle}; + glm::mat4 const *transforms{nullptr}; + std::size_t count{0}; + unsigned int frame{0}; // queued last + }; + std::unordered_map m_sleeperrecords; + // the model instances drawn on their own culled on the gpu (Global.gfx_gpucull_models), each one a bucket of its own with + // a group of its own: the record made when the instance is queued first, and again when the instance records went out of + // date (editor, events: m_instancerecordsversion). the records of instances not queued for a while are let go + struct model_records + { + scene::basic_cell::instance_bucket bucket; // the instance alone + scene::basic_cell::instance_bucket_key key; // the model, the skins and the instance + scene::bounding_area area; // the location of the instance + unsigned int frame{0}; // queued last + }; + std::unordered_map m_modelrecords; + // counts per lod band of the sleepers Render_Sleepers() drew in the pass, by model and skin, for the comparison (they're + // drawn before the culling runs) + std::unordered_map, scene::basic_cell::instance_bucket_key_hash> m_sleeperbandcounts; + // a draw recorded from the walk of the submodel tree of a group in one lod band (Walk_Indirect_Draws()), to be replayed once + // the instances of its command are known: the state the walk left for it and what it bound + struct indirect_draw + { + gl::model_ubs ubs; + material_handle material; + TSubModel const *submodel; + gfx::geometry_handle geometry; + unsigned int type; // primitive + bool blending; // drawn with the transparency hack of submodels faded out + std::uint32_t group{0}; // the buffers of the geometry, which the draws of a multi draw share (geometrybank_manager::draw_group()) + }; + std::vector m_indirectdraws; + std::vector m_indirectcommands; // in the order of m_indirectdraws + bool m_indirectrecording{false}; // the draws and the material binds of the backend record instead of drawing + std::uint32_t m_indirectentry{0}; // entry (group and lod band) recorded + material_handle m_indirectmaterial{null_handle}; // what the last material bind noted while recording + TSubModel const *m_indirectsubmodel{nullptr}; + // the walks Walk_Indirect_Draws() made for the cascades of the sun shadows in the frame, for the other cascades to copy: the + // draws and commands recorded for a group (model and skins) in each of its lod bands, first none where no cascade walked it + struct cascade_band + { + std::uint32_t first; + std::uint32_t count; + }; + struct cascade_record + { + unsigned int frame{0}; + std::vector bands; + }; + std::unordered_map m_cascaderecords; + std::vector m_cascadedraws; + std::vector m_cascadecommands; + unsigned int m_cascaderecordframe{0}; + // the walks Walk_Indirect_Draws() made in earlier frames (Global.gfx_gpucull_walkcache). what the walk of a group of instances + // in a lod band records depends on the model with its skins, the band and whether the pass draws shadows, not on the camera, + // so it's kept while the shared state of the model stays the same (TModel3d::state_version()) and used instead of walking the + // tree again. a walk meeting an animated submodel, one lit by the luminance or the stars isn't kept. the models drawn on their + // own (with an instance in the key) are walked every time + struct walk_band + { + std::vector draws; // as recorded + float bandmin{0.f}; + std::uint32_t modelversion{0}; + bool blending{false}; // the state the walk began in + bool valid{false}; + }; + struct walk_record + { + unsigned int frame{0}; // used last + std::array, 2> bands; // of the colour passes (the main view, the environment map) and of the shadows + }; + std::unordered_map m_walkrecords; + int m_walkrankcutoff{0}; // Global.gfx_shadow_rank_cutoff the walks were made with + // what the walks of the frame did, for the panel: walks made, of them not kept, walks taken from earlier frames, and the cpu time + // of the walks and of putting the draws in order with their blocks uploaded + struct walk_counts + { + std::uint32_t walked{0}; + std::uint32_t changing{0}; + std::uint32_t reused{0}; + double walktime{0.0}; + double ordertime{0.0}; + }; + walk_counts m_walkcounts; + walk_counts m_lastwalkcounts; + unsigned int m_walkcountsframe{0}; + // appends the draws of a walk kept from an earlier frame to the recorded draws, with the commands of the entry under way + void Replay_Walk(walk_band const &Walk, bool const Shadows); + // working space of Order_Indirect_Draws(), kept between frames + struct indirect_queue + { + std::uint32_t next; // draw + std::uint32_t end; + std::uint32_t link; // next queue waiting for the same state + }; + struct indirect_state + { + std::uint32_t head; + std::uint32_t tail; + }; + // what a multi draw has in common, and the draw which has it + struct indirect_state_key + { + material_handle material; + std::uint32_t variant; // of the textures of the material + std::uint32_t bank; + unsigned int type; + std::uint32_t indexed; + bool blending; + std::uint32_t draw; + auto tie() const + { + return std::tie(material, variant, bank, type, indexed, blending); + } + }; + std::vector m_indirectstatekeys; + std::vector m_indirectdrawstates; + std::vector m_indirectqueues; + std::vector m_indirectstates; + std::vector m_indirectstateorder; + std::vector m_indirectorder; + std::vector m_indirectdrawsordered; + std::vector m_indirectcommandsordered; // a vehicle drawn in the colour pass of this frame moved (motion blur) bool m_vehiclesmoving{false}; // set while a preview is drawn: submodels faded out entirely are skipped, instead of being drawn invisible over the stencil diff --git a/rendering/common/scenewalkindirect.cpp b/rendering/common/scenewalkindirect.cpp new file mode 100644 index 00000000..95493305 --- /dev/null +++ b/rendering/common/scenewalkindirect.cpp @@ -0,0 +1,829 @@ +/* +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/. +*/ + +// the part of the shared walk of the scene which feeds the gpu culling of the instances and records the draws made from its +// result (stage N6c of claude/vulkan14-renderer-plan.md): the records of the sleepers and of the models drawn on their own, the +// queueing of buckets, the walks of the submodel trees of the culled groups and the order of the recorded draws + +#include "stdafx.h" +#include "rendering/common/scenewalk.h" + +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" + +namespace gfx +{ + +namespace +{ + +// the group of the sleepers of a track in the gpu culling: their model, and the skin in the slot Render_Sleepers() sets it in +scene::basic_cell::instance_bucket_key sleeper_key(TTrack const &Track) +{ + scene::basic_cell::instance_bucket_key key; + key.pModel = Track.m_sleeper_model; + key.skins[1] = Track.m_sleeper_skin; + return key; +} + +} // namespace + +void scene_walk::Begin_Pass_Cull(gfx::instance_cull *Cull, bool const Mainview) +{ + m_passcull = Cull; + m_instancecullpass = false; + if (m_passcull != nullptr) + { + m_instancecullpass = true; + // the same choices as in Render_Instanced() + auto const reflections{m_renderpass.draw_mode == rendermode::reflections}; + gfx::instance_cull::pass_setup setup; + setup.frame = m_framestamp; + setup.camera = m_renderpass.pass_camera.position(); + setup.distancecamera = (m_renderpass.draw_mode == rendermode::shadows ? m_renderpass.viewport_camera.position() : m_renderpass.pass_camera.position()); + setup.frustum = &m_renderpass.pass_camera.frustum(); + setup.distancescale = (reflections ? 1.f : static_cast(1.0 / (static_cast(Global.ZoomFactor) * static_cast(Global.ZoomFactor) * static_cast(Global.fDistanceFactor)))); + setup.distanceoffset = (reflections ? static_cast(sq(EU07_REFLECTIONFIDELITYOFFSET)) : 0.f); + setup.distancerange = (reflections ? static_cast(sq(Global.reflectiontune.range_instances)) : std::numeric_limits::max()); + setup.drawrange = static_cast(sq(m_renderpass.draw_range + 250)); + setup.zoom = Global.ZoomFactor; + setup.drawing = (Global.gfx_gpucull == 1); + m_passcull->begin_pass(setup); + } + // the sleepers of tracks go with the instances of the main view + m_sleeperspass = (Mainview && (m_passcull != nullptr) && Global.gfx_gpucull_sleepers); + if (m_sleeperspass && (false == m_passcull->drawing())) + { + for (auto &counts : m_sleeperbandcounts) + { + counts.second.clear(); + } + } + // the models drawn on their own go with the instances of every pass culled on the gpu + m_modelspass = (m_instancecullpass && Global.gfx_gpucull_models); +} + +void scene_walk::Compare_Pass_Cull() +{ + // the sleepers the cpu drew along the tracks, before the culling ran + if (m_sleeperspass) + { + for (auto const &counts : m_sleeperbandcounts) + { + if (false == counts.second.empty()) + { + m_passcull->cpu_result(counts.first, counts.second, true); + } + } + } + // and the models drawn on their own + for (auto const &model : m_modelcpudistances) + { + m_passcull->cpu_instance(model.first, model.second); + } +} + +void scene_walk::End_Pass_Cull() +{ + if (m_instancecullpass) + { + m_passcull->end_pass(); + m_instancecullpass = false; + m_passcull = nullptr; + } + m_sleeperspass = false; + m_modelspass = false; +} + +bool scene_walk::walk_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, bool const Bucketrange, scene::bounding_area const &Area) +{ + if (m_instancecullpass) + { + // the gpu tests every instance of the bucket, the way Render_Instanced() does. models with nothing to draw in the + // sun shadows are left out of them, as Render_Instanced() leaves them + if ((m_renderpass.draw_mode != rendermode::shadows) || Instances_Cast_Shadow(Key, Bucket)) + { + m_passcull->add_bucket(Key, Bucket, (Bucketrange ? Bucket_Area(Bucket) : Area)); + } + // and when the instances are drawn from its result, the cpu has nothing more to do with them + if (m_passcull->drawing()) + { + return false; + } + } + // the depth pass of the whole scene culled them already, the colour pass draws what it recorded + if (m_indirectcolourpending) + { + return false; + } + return true; +} + +void scene_walk::walk_bucket_culled(scene::basic_cell::instance_bucket const &Bucket) +{ + if (m_instancecullpass) + { + m_passcull->skip_bucket(Bucket); + } +} + +bool scene_walk::walk_queue_model(TAnimModel *Instance, bool const Inrange) +{ + return Queue_Model(Instance, Inrange); +} + +bool scene_walk::walk_queue_sleepers(TTrack *Track) +{ + if (m_sleeperspass) + { + // culled with the instances of the pass and drawn from the result, or drawn by the cpu as well to compare + Queue_Sleepers(Track); + if (m_passcull->drawing()) + { + return true; + } + } + else if (m_indirectcolourpending && m_indirectcoloursleepers) + { + // the depth pass of the whole scene culled and recorded them, the colour pass draws what it recorded + return true; + } + return false; +} + +void scene_walk::walk_sleepers_drawn(TTrack const &Track, std::vector const &Lodbounds, std::vector> const &Drawn) +{ + if (m_sleeperspass && m_instancecullpass && (false == m_passcull->drawing())) + { + // the comparison with the gpu culling: the sleepers drawn by lod band, the bands as the culling makes them, split by + // the bounds of the model past 0 and short of the largest float + auto const counted = [](float const Bound) { + return (Bound > 0.f) && (Bound < std::numeric_limits::max()); + }; + auto &counts{m_sleeperbandcounts[sleeper_key(Track)]}; + auto const bands{static_cast(std::count_if(Lodbounds.begin(), Lodbounds.end(), counted)) + 1}; + if (counts.size() < bands) + { + counts.resize(bands, 0); + } + for (auto const &survivor : Drawn) + { + std::size_t band{0}; + for (auto const bound : Lodbounds) + { + if (counted(bound) && (bound <= survivor.first)) + { + ++band; + } + } + ++counts[band]; + } + } +} + +void scene_walk::walk_instances_compare(scene::basic_cell::instance_bucket_key const &Key, std::vector const &Bandcounts) +{ + m_passcull->cpu_result(Key, Bandcounts); +} + +void scene_walk::Queue_Sleepers(TTrack *Track) +{ + // the gates of Render_Sleepers(); the range is tested for the whole track + if ((Track->m_sleeper_model == nullptr) || Track->m_sleeper_local_transforms.empty() || (Global.SleeperDistance <= 0.f)) + { + return; + } + auto const distsq{glm::length2(Track->location() - m_renderpass.pass_camera.position())}; + auto const cutoffsq{static_cast(Global.SleeperDistance) * static_cast(Global.SleeperDistance)}; + if (distsq > cutoffsq) + { + return; + } + + auto &records{m_sleeperrecords[Track]}; + auto const &transforms{Track->m_sleeper_local_transforms}; + if ((records.bucket.recordsversion != m_instancerecordsversion) || (records.model != Track->m_sleeper_model) || (records.skin != Track->m_sleeper_skin) || + (records.transforms != transforms.data()) || (records.count != transforms.size())) + { + // each sleeper is a record placed and turned by its transform, relative to the origin of the track (the centre of its + // cell), with the sphere Render_Sleepers() tests against the frustum + auto const radius{std::max(Track->m_sleeper_model->bounding_radius(), 2.0f)}; + auto &bucket{records.bucket}; + bucket.records.clear(); + bucket.records.reserve(transforms.size()); + glm::vec3 low{std::numeric_limits::max()}; + glm::vec3 high{std::numeric_limits::lowest()}; + for (auto const &local : transforms) + { + scene::basic_cell::instance_record record; + glm::vec3 const position{local[3]}; + record.position = packed_vec3{position}; + record.radius = radius; + record.rangesquaredmin = 0.f; + record.rangesquaredmax = std::numeric_limits::max(); + record.flags = scene::basic_cell::instance_visible | scene::basic_cell::instance_instanceable; + for (int column = 0; column < 3; ++column) + { + record.rotationscale[column] = local[column]; + } + bucket.records.emplace_back(record); + low = glm::min(low, position); + high = glm::max(high, position); + } + bucket.recordorigin = Track->m_origin; + bucket.lodrangesquared = std::numeric_limits::max(); + bucket.recordsversion = m_instancerecordsversion; + bucket.gpustale = true; + records.key = sleeper_key(*Track); + records.area = scene::bounding_area{Track->m_origin + glm::dvec3{(low + high) * 0.5f}, 0.5f * glm::length(high - low)}; + records.model = Track->m_sleeper_model; + records.skin = Track->m_sleeper_skin; + records.transforms = transforms.data(); + records.count = transforms.size(); + } + records.frame = m_framestamp; + m_passcull->add_sleepers(records.key, records.bucket, records.area); +} + +// queues a model instance drawn on its own in the gpu culling of the pass, as a bucket and a group of its own. the culling +// makes the tests of Render( TAnimModel * ): visibility, frustum, the range of the instance, the draw range, the size on the +// screen, and gives it the lod band of the bounds of its submodels (mindistance/maxdistance) its distance falls in, then +// Walk_Indirect_Draws() animates, prepares and walks its submodel tree from its origin, like it does for a group of +// instanced ones +bool scene_walk::Queue_Model(TAnimModel *Instance, bool const Inrange) +{ + // a billboard is turned to the camera from the modelview of the instance, which a walk from its origin doesn't have + if ((false == Instance->m_cameraindependent) || (Instance->Model() == nullptr)) + { + return false; + } + if (false == m_modelspass) + { + // the depth pass of the whole scene culled and recorded it, the colour pass draws what it recorded + return (m_indirectcolourpending && m_indirectcolourmodels); + } + auto const drawing{m_passcull->drawing()}; + // a model hidden by an event, or in a cell wholly out of the range of the pass, would be left out by the culling + if ((false == Instance->visible()) || (false == Inrange)) + { + return drawing; + } + + auto &records{m_modelrecords[Instance]}; + auto &bucket{records.bucket}; + if ((bucket.recordsversion != m_instancerecordsversion) || (bucket.records.size() != 1)) + { + // the key, the instance and the record anew: the editor may have given the instance another model or skins, or + // another instance may have taken the place of a deleted one + auto &key{records.key}; + key = {}; + key.pModel = Instance->Model(); + if (auto const *material{Instance->Material()}) + { + for (int skin = 0; skin < 5; ++skin) + { + key.skins[skin] = material->replacable_skins[skin]; + } + } + key.instance = Instance; + bucket.instances.assign(1, Instance); + // the location of the instance as the origin of the record, which keeps the precision + Update_Instance_Records(key.pModel, bucket, Instance->location()); + // drawn by its bucket, though it isn't instanceable + bucket.records.front().flags |= scene::basic_cell::instance_instanceable; + records.area = scene::bounding_area{Instance->location(), 0.f}; + } + records.frame = m_framestamp; + if (m_renderpass.draw_mode == rendermode::shadows) + { + // a model with nothing to draw in the shadows is left out of them, like the instances. the answer depends on the + // model and the skins only, and is shared by the instances with them + auto modelkey{records.key}; + modelkey.instance = nullptr; + if (false == Instances_Cast_Shadow(modelkey, bucket)) + { + return drawing; + } + } + // out of the frustum of the pass, beyond its range or too small on the screen by its record: the culling would leave it out, + // and the walk of Walk_Indirect_Draws() isn't made for it + if (Global.gfx_gpucull_bucketrange && (false == EditorModeFlag) && Bucket_Culled(bucket)) + { + m_passcull->skip_bucket(bucket); + return drawing; + } + m_passcull->add_bucket(records.key, bucket, records.area); + if (false == drawing) + { + // the cpu draws it too, and tells what it drew for the comparison + m_modelcomparekey = &records.key; + } + return drawing; +} + +bool scene_walk::Instances_Cast_Shadow(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket) +{ + auto const lookup{m_shadowcasters.find(Key)}; + if (lookup != m_shadowcasters.end()) + { + return lookup->second; + } + + // the skins of the instance Render_Instanced() would take them from + TAnimModel const *instance{nullptr}; + for (auto const *candidate : Bucket.instances) + { + if (candidate != nullptr) + { + instance = candidate; + break; + } + } + auto const *skins{instance != nullptr && instance->Material() != nullptr ? instance->Material()->replacable_skins : nullptr}; + auto castsshadow{false}; + m_instance_lodpending.clear(); + m_instance_lodpending.push_back(Key.pModel != nullptr ? Key.pModel->Root : nullptr); + while ((false == castsshadow) && (false == m_instance_lodpending.empty())) + { + auto const *submodel = m_instance_lodpending.back(); + m_instance_lodpending.pop_back(); + if (submodel == nullptr) + { + continue; + } + if (submodel->eType < TP_ROTATOR) + { + auto const material{submodel->m_material >= 0 ? submodel->m_material : (skins != nullptr ? skins[-submodel->m_material] : null_handle)}; + castsshadow = (walk_material_data(material).shadow_rank <= Global.gfx_shadow_rank_cutoff); + } + m_instance_lodpending.push_back(submodel->Child); + m_instance_lodpending.push_back(submodel->Next); + } + m_shadowcasters.emplace(Key, castsshadow); + return castsshadow; +} + +void scene_walk::Walk_Indirect_Draws(gfx::instance_cull const &Cull, bool const Cascade) +{ + m_indirectdraws.clear(); + m_indirectcommands.clear(); + auto const &lodbounds{Cull.lod_bounds()}; + // the cascades of the sun shadows make the same walks: the mode, the lod band and the state of the models are the same in all + // of them within a frame, only the instances differ, and those the gpu fills in. the walk of a group in a band is made by the + // first cascade which draws it, the others copy its draws and commands with entries of their own + auto const cascade{(m_renderpass.draw_mode == rendermode::shadows) && Cascade}; + auto constexpr nowalk{std::numeric_limits::max()}; + if (cascade && (m_cascaderecordframe != m_framestamp)) + { + m_cascaderecordframe = m_framestamp; + m_cascadedraws.clear(); + m_cascadecommands.clear(); + if ((m_framestamp % 256) == 0) + { + // the groups not drawn for a while + for (auto records{m_cascaderecords.begin()}; records != m_cascaderecords.end();) + { + if (m_framestamp - records->second.frame > 256) + { + records = m_cascaderecords.erase(records); + } + else + { + ++records; + } + } + } + } + if (m_walkcountsframe != m_framestamp) + { + m_walkcountsframe = m_framestamp; + m_lastwalkcounts = m_walkcounts; + m_walkcounts = {}; + } + auto const walkstart{std::chrono::steady_clock::now()}; + // the walks of earlier frames. they go when the switch is off, as the models may change meanwhile without the walks noticing + // it the next time (the season), and when the shadows take other submodels + auto const walkcache{Global.gfx_gpucull_walkcache && (false == m_previewing)}; + if ((false == Global.gfx_gpucull_walkcache) || (m_walkrankcutoff != Global.gfx_shadow_rank_cutoff)) + { + m_walkrecords.clear(); + m_walkrankcutoff = Global.gfx_shadow_rank_cutoff; + } + if (walkcache && ((m_framestamp % 256) == 0)) + { + // the groups not drawn for a while + for (auto records{m_walkrecords.begin()}; records != m_walkrecords.end();) + { + if (m_framestamp - records->second.frame > 256) + { + records = m_walkrecords.erase(records); + } + else + { + ++records; + } + } + } + auto const shadows{m_renderpass.draw_mode == rendermode::shadows}; + m_indirectrecording = true; + for (auto const &group : Cull.draw_groups()) + { + auto *model{group.key->pModel}; + if ((model == nullptr) || (model->Root == nullptr) || ((group.instance == nullptr) && (false == group.sleepers))) + { + continue; + } + // a model drawn on its own is prepared with its lights and animations each time; the others are prepared only for a walk. + // the version is taken before, so a walk prepared by something which changes the model isn't taken up again + walk_record *kept{nullptr}; + if (walkcache && (group.key->instance == nullptr)) + { + kept = &m_walkrecords[*group.key]; + kept->frame = m_framestamp; + auto &bands{kept->bands[shadows ? 1 : 0]}; + if (bands.size() != group.lodcount + 1) + { + bands.clear(); + bands.resize(group.lodcount + 1); + } + } + auto const modelversion{model->state_version()}; + material_handle const *skins{nullptr}; + auto alpha{0x30300030}; + auto prepared{false}; + auto const prepare = [&]() { + prepared = true; + if (group.sleepers) + { + // as Render_Sleepers() sets them up: the skin of the track, if any. like scenery models they pick no texture + // variants of their own + if (group.key->skins[1] != null_handle) + { + skins = group.key->skins.data(); + } + TSubModel::iVariantSeed = 0; + } + else + { + if (group.key->instance != nullptr) + { + // a model drawn on its own: its lights and animations, the way Render( TAnimModel * ) sets them up + group.instance->RaAnimate(m_framestamp); + } + group.instance->RaPrepare(); + auto const *material{group.instance->Material()}; + if (material != nullptr) + { + skins = material->replacable_skins; + alpha = material->textures_alpha; + } + } + alpha ^= 0x0F0F000F; + }; + // the walks of the group the cascades made in this frame. a group whose bands were all walked already isn't prepared + cascade_record *walks{nullptr}; + if (cascade) + { + walks = &m_cascaderecords[*group.key]; + if ((walks->frame != m_framestamp) || (walks->bands.size() != group.lodcount + 1)) + { + walks->frame = m_framestamp; + walks->bands.assign(group.lodcount + 1, cascade_band{nowalk, 0}); + } + } + else if (kept == nullptr) + { + prepare(); + } + for (std::uint32_t band = 0; band <= group.lodcount; ++band) + { + // a band holds the squared distances from its lower bound up to the next one; any of them selects the same lod + auto const bandmin{band == 0 ? 0.f : lodbounds[group.lodfirst + band - 1]}; + auto const bandmax{band == group.lodcount ? std::numeric_limits::max() : lodbounds[group.lodfirst + band]}; + if ((bandmin > group.distancemax) || (bandmax <= group.distancemin)) + { + continue; + } + m_indirectentry = group.entryfirst + band; + if ((walks != nullptr) && (walks->bands[band].first != nowalk)) + { + // walked by another cascade: its draws, for the instances of this entry + auto const &walk{walks->bands[band]}; + m_indirectdraws.insert(m_indirectdraws.end(), m_cascadedraws.begin() + walk.first, m_cascadedraws.begin() + walk.first + walk.count); + for (std::uint32_t draw = 0; draw < walk.count; ++draw) + { + auto &command{m_indirectcommands.emplace_back(m_cascadecommands[walk.first + draw])}; + command.entry = m_indirectentry; + } + // as the walk would count them + m_renderpass.draw_stats.submodels += static_cast(walk.count); + m_renderpass.draw_stats.drawcalls += static_cast(walk.count); + continue; + } + auto const walkfirst{m_indirectdraws.size()}; + auto *keptwalk{kept != nullptr ? &kept->bands[shadows ? 1 : 0][band] : nullptr}; + if ((keptwalk != nullptr) && (keptwalk->valid) && (keptwalk->bandmin == bandmin) && (keptwalk->modelversion == modelversion) && + (keptwalk->blending == walk_blending_enabled())) + { + // walked in an earlier frame, with the model as it is now + Replay_Walk(*keptwalk, shadows); + ++m_walkcounts.reused; + } + else + { + if (false == prepared) + { + prepare(); + } + auto const blending{walk_blending_enabled()}; + m_indirectchanging = false; + ::glPushMatrix(); + ::glLoadIdentity(); + model->Root->fSquareDist = bandmin; + model->Root->ReplacableSet(skins, alpha); + model->Root->pRoot = model; + Render(model->Root); + ::glPopMatrix(); + ++m_walkcounts.walked; + if (m_indirectchanging) + { + ++m_walkcounts.changing; + } + if (keptwalk != nullptr) + { + keptwalk->valid = (false == m_indirectchanging); + if (keptwalk->valid) + { + keptwalk->draws.assign(m_indirectdraws.begin() + walkfirst, m_indirectdraws.end()); + keptwalk->bandmin = bandmin; + keptwalk->modelversion = modelversion; + keptwalk->blending = blending; + } + else + { + keptwalk->draws.clear(); + } + } + } + if (walks != nullptr) + { + walks->bands[band] = cascade_band{static_cast(m_cascadedraws.size()), static_cast(m_indirectdraws.size() - walkfirst)}; + m_cascadedraws.insert(m_cascadedraws.end(), m_indirectdraws.begin() + walkfirst, m_indirectdraws.end()); + m_cascadecommands.insert(m_cascadecommands.end(), m_indirectcommands.begin() + walkfirst, m_indirectcommands.end()); + } + } + } + m_indirectrecording = false; + m_walkcounts.walktime += std::chrono::duration(std::chrono::steady_clock::now() - walkstart).count(); +} + +void scene_walk::Record_Indirect_Draw(gfx::geometry_handle const &Geometry) +{ + // the walk of Walk_Indirect_Draws(): the draw is made later, from the gpu culling, with this state + auto &geometry{walk_geometry()}; + gfx::indirect_source source; + if (geometry.indirect_source(Geometry, source)) + { + m_indirectcommands.push_back({source.count, 0, source.first, static_cast(source.basevertex), 0, m_indirectentry, (source.indexed ? 1u : 0u), 0}); + m_indirectdraws.push_back({model_ubs, m_indirectmaterial, m_indirectsubmodel, Geometry, source.type, walk_blending_enabled(), geometry.draw_group(Geometry)}); + } +} + +void scene_walk::Replay_Walk(walk_band const &Walk, bool const Shadows) +{ + auto &geometry{walk_geometry()}; + for (auto const &walkdraw : Walk.draws) + { + // the place of the chunk in its bank is asked for again: the bank may have been let go since and made anew, which the + // question (as the walk) takes care of, uploading the chunk + gfx::indirect_source source; + if (false == geometry.indirect_source(walkdraw.geometry, source)) + { + continue; + } + m_indirectcommands.push_back({source.count, 0, source.first, static_cast(source.basevertex), 0, m_indirectentry, (source.indexed ? 1u : 0u), 0}); + auto &draw{m_indirectdraws.emplace_back(walkdraw)}; + draw.type = source.type; + draw.group = geometry.draw_group(walkdraw.geometry); + // the walk sets the placement of the submodel, and before each draw of the colour passes the parameters of its material + // (the material bind); the rest of the block is what the pass has at the moment, as the walk would leave it + auto const &walked{walkdraw.ubs}; + draw.ubs = model_ubs; + draw.ubs.modelview = walked.modelview; + draw.ubs.modelviewnormal = walked.modelviewnormal; + if (false == Shadows) + { + std::memcpy(&draw.ubs.param[0], &walked.param[0], sizeof(draw.ubs.param)); + draw.ubs.opacity = walked.opacity; + draw.ubs.alpha_mult = walked.alpha_mult; + } + } + // as the walk would count them + m_renderpass.draw_stats.submodels += static_cast(Walk.draws.size()); + m_renderpass.draw_stats.drawcalls += static_cast(Walk.draws.size()); +} + +texture_handle scene_walk::Indirect_Shadow_Texture(material_handle const Material) const +{ + if (Material == null_handle) + { + return null_handle; + } + return walk_material_data(Material).textures[0]; +} + +// the order of the recorded draws for multi draws. draws go in one multi draw when they follow one another with the same +// state (material and its texture variant, geometry bank, primitive, blending; Indirect_Mergeable()). the draws of one entry, +// the walk of one submodel tree in one lod band, keep their order: submodels of a model may lie on one another and rely on +// the later one being drawn over the earlier one. the draws of different entries are drawn for different instances, which +// the gpu draws in no particular order anyway, so they're free to move. the entries are queues of draws; the draws at the +// heads of the queues which have the state taken next all go, each queue as far as its draws keep that state, then the +// queues wait for the state of their next draw. the states go in the order in which the queues first reach them +void scene_walk::Order_Indirect_Draws() +{ + auto const count{m_indirectdraws.size()}; + if (count < 2) + { + return; + } + + // a number for each state the draws have, by sorting the states (the buffers are kept between frames) + m_indirectstatekeys.clear(); + // the sun shadows draw with the program and the texture of the alpha test, whatever the material + auto const shadows{m_renderpass.draw_mode == rendermode::shadows}; + for (std::size_t index = 0; index < count; ++index) + { + auto const &draw{m_indirectdraws[index]}; + if (shadows) + { + m_indirectstatekeys.push_back({Indirect_Shadow_Texture(draw.material), 0u, draw.group, draw.type, m_indirectcommands[index].indexed, draw.blending, static_cast(index)}); + continue; + } + auto variant{0u}; + if (draw.material != null_handle) + { + auto const &material{walk_material_data(draw.material)}; + if (false == material.texture_variants.empty()) + { + variant = TSubModel::VariantId(draw.submodel) % static_cast(material.texture_variants.size()); + } + } + m_indirectstatekeys.push_back({draw.material, variant, draw.group, draw.type, m_indirectcommands[index].indexed, draw.blending, static_cast(index)}); + } + std::sort(std::begin(m_indirectstatekeys), std::end(m_indirectstatekeys), [](indirect_state_key const &Left, indirect_state_key const &Right) { return Left.tie() < Right.tie(); }); + m_indirectdrawstates.resize(count); + std::uint32_t statecount{0}; + for (std::size_t index = 0; index < count; ++index) + { + if ((index > 0) && (m_indirectstatekeys[index].tie() != m_indirectstatekeys[index - 1].tie())) + { + ++statecount; + } + m_indirectdrawstates[m_indirectstatekeys[index].draw] = statecount; + } + ++statecount; + if (statecount == count) + { + // no two draws share a state, nothing would go together + return; + } + + auto constexpr none{std::numeric_limits::max()}; + m_indirectstates.assign(statecount, indirect_state{none, none}); + m_indirectstateorder.clear(); + m_indirectqueues.clear(); + // the queue waits for the state of its next draw, behind the queues waiting for it already + auto const enqueue = [this, none](std::uint32_t const Queue) { + auto &queue{m_indirectqueues[Queue]}; + auto &state{m_indirectstates[m_indirectdrawstates[queue.next]]}; + queue.link = none; + if (state.head == none) + { + state.head = Queue; + m_indirectstateorder.push_back(m_indirectdrawstates[queue.next]); + } + else + { + m_indirectqueues[state.tail].link = Queue; + } + state.tail = Queue; + }; + for (std::size_t index = 0; index < count;) + { + auto end{index + 1}; + while ((end < count) && (m_indirectcommands[end].entry == m_indirectcommands[index].entry)) + { + ++end; + } + m_indirectqueues.push_back({static_cast(index), static_cast(end), none}); + index = end; + } + for (std::uint32_t queue = 0; queue < m_indirectqueues.size(); ++queue) + { + enqueue(queue); + } + m_indirectorder.clear(); + for (std::size_t taken = 0; taken < m_indirectstateorder.size(); ++taken) + { + auto const stateindex{m_indirectstateorder[taken]}; + auto queueindex{m_indirectstates[stateindex].head}; + m_indirectstates[stateindex] = {none, none}; + while (queueindex != none) + { + auto &queue{m_indirectqueues[queueindex]}; + auto const nextqueue{queue.link}; + while ((queue.next < queue.end) && (m_indirectdrawstates[queue.next] == stateindex)) + { + m_indirectorder.push_back(queue.next); + ++queue.next; + } + if (queue.next < queue.end) + { + enqueue(queueindex); + } + queueindex = nextqueue; + } + } + + m_indirectdrawsordered.clear(); + m_indirectcommandsordered.clear(); + for (auto const index : m_indirectorder) + { + m_indirectdrawsordered.push_back(m_indirectdraws[index]); + m_indirectcommandsordered.push_back(m_indirectcommands[index]); + } + m_indirectdraws.swap(m_indirectdrawsordered); + m_indirectcommands.swap(m_indirectcommandsordered); +} + +bool scene_walk::Indirect_Mergeable(std::size_t const First, std::size_t const Other) const +{ + auto const &first{m_indirectdraws[First]}; + auto const &other{m_indirectdraws[Other]}; + if ((first.material != other.material) || (first.blending != other.blending) || (first.type != other.type) || (first.group != other.group) || + (m_indirectcommands[First].indexed != m_indirectcommands[Other].indexed)) + { + return false; + } + if (first.material == null_handle) + { + return true; + } + // the texture set the material bind binds + auto const &material{walk_material_data(first.material)}; + if (material.texture_variants.empty()) + { + return true; + } + return (&material.GetTextures(TSubModel::VariantId(first.submodel)) == &material.GetTextures(TSubModel::VariantId(other.submodel))); +} + +void scene_walk::Expire_Indirect_Records() +{ + if ((m_framestamp % 256) == 0) + { + // the sleeper records of tracks out of sight for a while. their places in the record buffer of the gpu culling are + // dropped when it starts over + for (auto records{m_sleeperrecords.begin()}; records != m_sleeperrecords.end();) + { + if (m_framestamp - records->second.frame > 256) + { + records = m_sleeperrecords.erase(records); + } + else + { + ++records; + } + } + // and so are the records of the models drawn on their own + for (auto records{m_modelrecords.begin()}; records != m_modelrecords.end();) + { + if (m_framestamp - records->second.frame > 256) + { + records = m_modelrecords.erase(records); + } + else + { + ++records; + } + } + } +} + +void scene_walk::Clear_Indirect_Records() +{ + m_sleeperrecords.clear(); + m_modelrecords.clear(); + m_cascaderecords.clear(); + m_walkrecords.clear(); +} + +} // namespace gfx diff --git a/rendering/common/shadowfit.cpp b/rendering/common/shadowfit.cpp new file mode 100644 index 00000000..0b2bd683 --- /dev/null +++ b/rendering/common/shadowfit.cpp @@ -0,0 +1,273 @@ +/* +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/common/shadowfit.h" + +#include + +#include "rendering/openglcamera.h" +#include "utilities/Globals.h" + +namespace gfx +{ + +namespace +{ + +// the nearest distance the first cascade may start from and the least one the last reaches, metres: the cascades don't +// get any closer to the camera than this, however close the pixels are +float const SPLIT_NEAR_MIN{0.5f}; +float const SPLIT_FAR_MIN{16.f}; +// how far past its distances a cascade takes the pixels, this many metres and this share of the distance: the camera moves +// in the frame or two before the cascades are drawn from the measurement, and a pixel which came into another cascade since +// has to be within its map +float const MARGIN_METRES{1.f}; +float const MARGIN_SHARE{0.05f}; + +} // namespace + +gl::shadow_fit_data shadow_fit::begin_measurement(view const &View, unsigned int const Frame) +{ + // numbers start from 1, 0 is no measurement + if (++m_measurement == 0) + ++m_measurement; + gl::shadow_fit_data data{}; + data.measurement = m_measurement; + data.depthmin = std::numeric_limits::infinity(); + for (std::size_t cascade = 0; cascade < gl::MAX_CASCADES; ++cascade) + { + for (std::size_t axis = 0; axis < 4; ++axis) + { + data.bounds[cascade * 8 + axis] = 0xFFFFFFFFu; + data.bounds[cascade * 8 + 4 + axis] = 0u; + } + } + m_pending[m_measurement % PENDING] = pending{m_measurement, View, Frame}; + return data; +} + +glm::vec4 shadow_fit::bounds_params(view const &View) +{ + auto const farmin{std::min(SPLIT_FAR_MIN, View.range)}; + return glm::vec4{View.range, std::min(SPLIT_NEAR_MIN, farmin * 0.25f), farmin, std::clamp(Global.gfx_shadow_sdsm_split, 0.f, 1.f)}; +} + +glm::vec4 shadow_fit::margin_params() +{ + return glm::vec4{MARGIN_METRES, MARGIN_SHARE, 0.f, 0.f}; +} + +void shadow_fit::arrived(gl::shadow_fit_data const &Data) +{ + auto const &source{m_pending[Data.measurement % PENDING]}; + if ((Data.measurement != 0) && (source.measurement == Data.measurement)) + m_latest = result{Data, source.source, source.frame}; +} + +float shadow_fit::unorder(std::uint32_t const Value) +{ + auto const bits{((Value & 0x80000000u) != 0u) ? (Value & 0x7FFFFFFFu) : ~Value}; + float result; + std::memcpy(&result, &bits, sizeof(result)); + return result; +} + +glm::vec3 shadow_fit::light_vector(glm::vec3 const &Sun) +{ + // cap the vertical angle to keep shadows from getting too long + return glm::normalize(glm::vec3{Sun.x, std::min(Sun.y, Global.gfx_shadow_angle_min), Sun.z}); +} + +glm::mat3 shadow_fit::light_rotation(glm::vec3 const &Sun) +{ + // the rotation of the view of the shadow pass (setup_pass()) + return glm::mat3{glm::lookAt(glm::vec3{0.f}, light_vector(Sun), glm::vec3{0.f, 1.f, 0.f})}; +} + +std::array shadow_fit::fixed_splits() +{ + static_assert(gl::MAX_CASCADES == 3, "the fixed cascades are made for 3 of them"); + std::array splits{0.0f, Global.shadowtune.range / 32, Global.shadowtune.range / 8, Global.shadowtune.range}; + if (Global.shadowtune.map_size > 2048) + { + // increase coverage if our shadow map is large enough to produce decent results + for (std::size_t idx = 1; idx < splits.size(); ++idx) + splits[idx] *= Global.shadowtune.map_size / 2048; + } + return splits; +} + +char const *shadow_fit::unavailable(bool const Postprocessing, bool const Shadow, std::size_t const Shadowviews) +{ + if (false == Global.gfx_shadowmap_enabled) + return "no shadow maps"; + if (Global.bWireFrame) + return "wireframe"; + // the shadows follow the camera, the view shows another one + if (DebugCameraFlag) + return "debug camera"; + if (false == Postprocessing) + return "no post processing"; + if (EditorModeFlag && Global.EditorOrtho) + return "orthographic view"; + if ((false == Shadow) || (Shadowviews != 1)) + return "the shadows cover more than one view"; + return nullptr; +} + +bool shadow_fit::fit(unsigned int const Frame, opengl_camera const &Camera, char const *Unavailable, std::array &Splits, areas &Areas, fit_state &State) const +{ + // a measurement older than this many frames, made with the camera this many metres away or the view turned this many + // degrees from the one now, isn't used + constexpr unsigned int AGE_MAX{6}; + constexpr float SHIFT_MAX{25.f}; + constexpr float TURN_MAX{20.f}; + // metres an area grows by on each side: the vehicles move in the frames since the measurement too + constexpr float MARGIN{0.25f}; + + auto &state{State}; + state = fit_state{}; + if (false == Global.gfx_shadow_sdsm) + return false; + if ((state.reason = Unavailable) != nullptr) + return false; + auto const *latest{this->latest()}; + if (latest == nullptr) + { + state.reason = "no measurement yet"; + return false; + } + auto const &data{latest->data}; + auto const &source{latest->source}; + state.age = Frame - latest->frame; + state.depthmin = data.depthmin; + state.depthmax = data.depthmax; + if (state.age > AGE_MAX) + { + state.reason = "the measurement is too old"; + return false; + } + if (data.samples == 0) + { + state.reason = "only the sky in the view"; + return false; + } + if (source.range != fixed_splits().back()) + { + state.reason = "the range of the shadows changed"; + return false; + } + // the shadows of this frame are made for its colour pass + auto const &camera{Camera}; + auto const shift{glm::vec3{source.camera - camera.position()}}; + if (glm::length(shift) > SHIFT_MAX) + { + state.reason = "the camera jumped"; + return false; + } + auto const turnmatrix{glm::mat3{camera.modelview()} * glm::transpose(source.rotation)}; + auto const turn{std::acos(std::clamp((turnmatrix[0][0] + turnmatrix[1][1] + turnmatrix[2][2] - 1.f) * 0.5f, -1.f, 1.f))}; + state.turn = glm::degrees(turn); + if (state.turn > TURN_MAX) + { + state.reason = "the view turned too fast"; + return false; + } + auto const &projection{camera.projection()}; + if ((std::abs(projection[0][0] - source.projection[0][0]) > 0.02f * std::abs(projection[0][0])) || (std::abs(projection[1][1] - source.projection[1][1]) > 0.02f * std::abs(projection[1][1]))) + { + state.reason = "the field of view changed"; + return false; + } + + // the first cascade from the camera, like the fixed ones. setup_pass() takes distances up to 1 m as shares of the far + // end, so the others start past that + Splits[0] = 0.f; + for (std::size_t split = 1; split < Splits.size(); ++split) + Splits[split] = std::max(data.splits[split], Splits[split - 1] + 1.5f); + if (false == std::isfinite(Splits.back())) + { + state.reason = "invalid measurement"; + return false; + } + + auto const inverselight{glm::transpose(source.light)}; + // the scenery stays where it was while the camera moved, the cab and the train the camera rides with moved along. an area + // covers both + auto const lightshift{source.light * shift}; + auto const chord{2.f * std::sin(turn * 0.5f)}; + for (std::size_t cascade = 0; cascade < gl::MAX_CASCADES; ++cascade) + { + // with nothing of the view in a cascade, its map covers the whole slice + if (data.counts[cascade] == 0) + continue; + auto const *bounds{&data.bounds[cascade * 8]}; + glm::vec3 low{unorder(bounds[0]), unorder(bounds[1]), unorder(bounds[2])}; + glm::vec3 high{unorder(bounds[4]), unorder(bounds[5]), unorder(bounds[6])}; + if ((false == std::isfinite(low.x + low.y + high.x + high.y)) || (low.x > high.x) || (low.y > high.y)) + continue; + low = glm::min(low, low + lightshift); + high = glm::max(high, high + lightshift); + // and what came into the view as it turned, out to the reach of the cascade + auto const reach{Splits[cascade + 1] + static_cast(gl::MAX_CASCADES - cascade)}; + auto const margin{reach * chord + MARGIN + 0.01f * std::max(high.x - low.x, high.y - low.y)}; + low -= glm::vec3{margin}; + high += glm::vec3{margin}; + auto &area{Areas[cascade].emplace()}; + for (int corner = 0; corner < 8; ++corner) + area[corner] = inverselight * glm::vec3{(corner & 1) ? high.x : low.x, (corner & 2) ? high.y : low.y, (corner & 4) ? high.z : low.z}; + } + state.fitted = true; + return true; +} + +float shadow_fit::fit_slice(std::vector &Points, std::array const *Area, glm::mat3 const &Lightrotation, float &Slicesize) +{ + // a cascade fitted to the depth of the view (gfx.shadow.sdsm) covers the area of the pixels it takes, within the slice of the + // view. the areas are compared across the light, the depth along it stays the one of the slice + auto quantizationstep{0.f}; + glm::vec3 slicemin{std::numeric_limits::max()}, slicemax{std::numeric_limits::lowest()}; + for (auto const &point : Points) + { + auto const lightpoint{Lightrotation * glm::vec3{point}}; + slicemin = glm::min(slicemin, lightpoint); + slicemax = glm::max(slicemax, lightpoint); + } + Slicesize = std::max(slicemax.x - slicemin.x, slicemax.y - slicemin.y); + if (Area != nullptr) + { + glm::vec3 areamin{std::numeric_limits::max()}, areamax{std::numeric_limits::lowest()}; + for (auto const &point : *Area) + { + auto const lightpoint{Lightrotation * point}; + areamin = glm::min(areamin, lightpoint); + areamax = glm::max(areamax, lightpoint); + } + auto const fitmin{glm::vec2{glm::max(glm::vec2{slicemin}, glm::vec2{areamin})}}; + auto const fitmax{glm::vec2{glm::min(glm::vec2{slicemax}, glm::vec2{areamax})}}; + if ((fitmin.x < fitmax.x) && (fitmin.y < fitmax.y)) + { + // the size changes in steps of a power of two of about 1/16 of it, so that the texels of the map keep their size until + // the area grows or shrinks by a step + auto const size{std::max(fitmax.x - fitmin.x, fitmax.y - fitmin.y)}; + quantizationstep = std::exp2(std::ceil(std::log2(std::max(size, 1.f) / 16.f))); + Points.clear(); + auto const inverserotation{glm::transpose(Lightrotation)}; + for (int corner = 0; corner < 8; ++corner) + { + auto const lightpoint{glm::vec3{(corner & 1) ? fitmax.x : fitmin.x, (corner & 2) ? fitmax.y : fitmin.y, (corner & 4) ? slicemax.z : slicemin.z}}; + Points.emplace_back(inverserotation * lightpoint, 1.f); + } + } + } + return quantizationstep; +} + +} // namespace gfx diff --git a/rendering/common/shadowfit.h b/rendering/common/shadowfit.h new file mode 100644 index 00000000..9d2bcb1d --- /dev/null +++ b/rendering/common/shadowfit.h @@ -0,0 +1,116 @@ +/* +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" + +class opengl_camera; + +namespace gfx +{ + +// the cpu side of the cascades of the sun shadows fitted to the depth of the main view (sample distribution shadow maps, +// gfx.shadow.sdsm), shared by the renderers (opengl46_shadow_fit, the vulkan renderer). at the end of the colour pass of the main +// view two compute passes go over its resolved depth: the nearest and the farthest distance of the pixels which aren't the sky +// (shadow_fit_depth.comp), then the distances split between the cascades and, for each cascade, the box of the pixels it takes +// in the space of the light (shadow_fit_bounds.comp). the result comes back to the cpu a frame or two later without waiting, +// with the camera and the light it was measured with, and the cascades of the frame cover the boxes (fit()) instead of the +// whole slices of the view between the fixed distances of shadowtune +class shadow_fit +{ + public: + // the view and the light a measurement was made with + struct view + { + glm::dvec3 camera; // position of the camera + glm::mat4 projection; // projection of the view + glm::mat3 rotation; // world to view space, rotation only + glm::mat3 light; // world to the space of the light, rotation only + float range{0.f}; // the farthest distance the cascades may reach + }; + // a measurement which came back + struct result + { + gl::shadow_fit_data data; + view source; + unsigned int frame{0}; // frame of the renderer it was made in + }; + // what the last shadow pass did, for the debug panel + struct fit_state + { + bool fitted{false}; + char const *reason{nullptr}; // why not, with gfx.shadow.sdsm on + unsigned int age{0}; // frames since the measurement + float depthmin{0.f}; + float depthmax{0.f}; + float turn{0.f}; // of the view since the measurement, degrees + std::array splits{}; + // size of the area of the map of each cascade: of the whole slice of the view, and as drawn; metres + std::array slice{}; + std::array drawn{}; + }; + using areas = std::array>, gl::MAX_CASCADES>; + + // a measurement made: its number in the data the passes start from (no pixels, the nearest distance infinite, the bounds + // empty), kept with the view until it comes back + gl::shadow_fit_data begin_measurement(view const &View, unsigned int const Frame); + // the parameters of shadow_fit_bounds.comp: the range, the nearest end of the first split, the least far end of the last + // one, the split between the logarithmic and the even splits; and the margins of the bounds + static glm::vec4 bounds_params(view const &View); + static glm::vec4 margin_params(); + // a measurement which came back from the gpu + void arrived(gl::shadow_fit_data const &Data); + // the newest measurement taken in so far + result const *latest() const + { + return m_latest ? &(*m_latest) : nullptr; + } + // the ends of the bounds, as the passes store them (shadow_fit_order() in shadow_fit_bounds.comp) + static float unorder(std::uint32_t const Value); + + // the cascades of the frame fitted to the latest measurement: the distances they go from and to, and for each cascade the + // corners of the area its map is to cover, camera-relative world space (none: the whole slice of the view). false: the fixed + // cascades. Camera: of the colour pass the shadows are made for; Unavailable: why the cascades can't be fitted, or null + bool fit(unsigned int const Frame, opengl_camera const &Camera, char const *Unavailable, std::array &Splits, areas &Areas, fit_state &State) const; + + // the direction the sun shadows are cast in (the sun, its height capped by gfx.shadow.angle.min), and the rotation from the + // world to the space of the light of the cascades: x and y across the maps, z along the light + static glm::vec3 light_vector(glm::vec3 const &Sun); + static glm::mat3 light_rotation(glm::vec3 const &Sun); + // the distances the fixed cascades of the sun shadows go from and to (shadowtune) + static std::array fixed_splits(); + // why the cascades can't be fitted to the depth of a viewport, null if they can. Postprocessing: the viewport has the images + // of the post-processing (the resolved depth); Shadow: its view is taken into the shadows; Shadowviews: the viewports which are + static char const *unavailable(bool const Postprocessing, bool const Shadow, std::size_t const Shadowviews); + // the points of the slice of the view a shadow pass covers (setup_pass()), in its place the corners of the area fitted to the + // depth, when there is one (and it lies within the slice); Slicesize: the size of the slice across the light. returns the step + // the bounds of the map are quantized to, 0 when the area isn't fitted + static float fit_slice(std::vector &Points, std::array const *Area, glm::mat3 const &Lightrotation, float &Slicesize); + + private: + // a measurement on its way back, kept by its number + struct pending + { + std::uint32_t measurement{0}; // 0: none + view source; + unsigned int frame{0}; + }; + static constexpr std::size_t PENDING = 8; // more than the copies the readbacks keep in flight + + std::array m_pending; + std::uint32_t m_measurement{0}; // number of the last measurement + std::optional m_latest; +}; + +} // namespace gfx diff --git a/rendering/common/skylight.cpp b/rendering/common/skylight.cpp new file mode 100644 index 00000000..794818a6 --- /dev/null +++ b/rendering/common/skylight.cpp @@ -0,0 +1,289 @@ +/* +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/common/skylight.h" + +#include "environment/skydome.h" +#include "utilities/Globals.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 + +namespace gfx +{ + +sky_light::sky_light() +{ + // 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; +} + +sky_light::sh_coefficients sky_light::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 sky_light::begin_update() +{ + m_overcast = std::clamp(Global.Overcast, 0.f, 1.f); + m_weight = 0.f; +} + +bool sky_light::atmosphere_changed(glm::vec3 const &Sundirection, bool &Medium) +{ + // 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}; + Medium = mediumchanged; + if (!mediumchanged && !sunchanged) + return false; + + 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_tables_medium = medium; + m_tables_sun = Sundirection; + return true; +} + +void sky_light::end_update(basic_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}; + + if (Global.gfx_atmosphere && 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 + // with the shadows of the clouds the sun is held back where they are, not everywhere + auto const overcastsun{m_cloud_shadows ? 1.f : (1.f - 0.65f * m_overcast)}; + auto const sun{glm::vec3{result.sun_transmittance} * m_white_balance * (ATMOSPHERE_SUN_SCALE * Global.gfx_atmosphere_sun) * overcastsun}; + 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}; + m_sky_data = skydata; +} + +void sky_light::sky_params(glm::vec4 &Param0, glm::vec4 &Param1) const +{ + // the volumetric clouds make the overcast sky themselves + Param0 = glm::vec4{m_weight, m_sky_scale, m_clouds ? 0.f : 0.75f * m_overcast, m_sky_flat_luminance}; + Param1 = glm::vec4{m_white_balance, 0.f}; +} + +glm::vec3 sky_light::sun_disc_color(glm::vec3 const &Oldcolor) const +{ + return glm::mix(Oldcolor, m_sun_disc_color, m_weight); +} + +void sky_light::draw_debug_ui() const +{ + 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)); +} + +} // namespace gfx diff --git a/rendering/common/skylight.h b/rendering/common/skylight.h new file mode 100644 index 00000000..28b9b402 --- /dev/null +++ b/rendering/common/skylight.h @@ -0,0 +1,131 @@ +/* +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/. +*/ + +// the light of the sky the renderers share (the part of the stage S of claude/gl46-renderer-features-plan.md which isn't the +// work of the gpu, taken out of opengl46_sky for the vulkan renderer, stage P1 of claude/vulkan14-renderer-plan.md): +// - the directional part of the ambient light, as spherical harmonics (gl::sky_data), worked out from the colours of the sky +// dome or from the physical atmosphere +// - the parameters of the physical atmosphere (gl::atmosphere_ubs) and when its look-up tables are to be made again +// - what the result of the atmosphere read back from the gpu (gl::atmosphere_result) makes of the sunlight, the ambient light +// and the fog colour of the frame, the white balance, the share of the atmosphere in the sky and the colour of the sun disc +// switched by Global.gfx_ambient_sh and gfx_atmosphere; off, the light the simulation set is left as it is + +#pragma once + +#include +#include + +#include "gl/ubo.h" +#include "rendering/light.h" + +class CSkyDome; + +namespace gfx +{ + +class sky_light +{ + public: + // the parameters of the atmosphere valid from the start, the tables stay black until they are first made + sky_light(); + + // the first part of the update of a frame: the overcast of the frame, no atmosphere yet + void begin_update(); + // the inputs of the look-up tables of the atmosphere for the frame (Sundirection normalized): true when the tables are to be + // made now, Medium when the transmittance and multiple scattering ones are too (the sky-view one and the ambient light follow + // the sun). the parameters are set up for them, and the tables are taken as made + bool atmosphere_changed(glm::vec3 const &Sundirection, bool &Medium); + // a result of the atmosphere read back from the gpu + void set_result(gl::atmosphere_result const &Result) + { + m_result = Result; + } + // the rest of the update: replaces the colours of Sunlight (diffuse, ambient) and Fogcolor, which come in as the simulation + // set them, with the light of the sky, and works out the directional part of the ambient light. Sundirection and Sunangle + // (degrees) are those of the sun, also when the moon is the key light + void end_update(basic_light &Sunlight, glm::vec3 &Fogcolor, CSkyDome const &Skydome, glm::vec3 const &Sundirection, float const Sunangle); + + gl::atmosphere_ubs const &atmosphere() const + { + return m_atmosphere; + } + // the directional part of the ambient light for the shading + gl::sky_data const &sky_data() const + { + return m_sky_data; + } + // 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; + } + bool has_result() const + { + return m_result.has_value(); + } + glm::vec3 const &white_balance() const + { + return m_white_balance; + } + float sky_scale() const + { + return m_sky_scale; + } + float overcast() const + { + return m_overcast; + } + // the volumetric clouds are drawn over the sky (it doesn't take the overcast look then), and their shadows hold back the + // sunlight (it isn't dimmed by the overcast then) + void set_clouds(bool const Clouds, bool const Shadows) + { + m_clouds = Clouds; + m_cloud_shadows = Shadows; + } + // the shader parameters of the sky drawn with the atmosphere (sky_atmosphere.frag), param[0] and param[1] + void sky_params(glm::vec4 &Param0, glm::vec4 &Param1) 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; + // controls of the debug window + void draw_debug_ui() const; + + 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; + + 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; + std::optional m_result; + gl::sky_data m_sky_data{}; + + // what the 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}; + bool m_clouds{false}; + bool m_cloud_shadows{false}; + 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{}; +}; + +} // namespace gfx diff --git a/rendering/common/terrainmaterials.cpp b/rendering/common/terrainmaterials.cpp new file mode 100644 index 00000000..31393eba --- /dev/null +++ b/rendering/common/terrainmaterials.cpp @@ -0,0 +1,373 @@ +/* +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/common/terrainmaterials.h" + +#include "rendering/common/shadersource.h" +#include "utilities/Logs.h" + +namespace gfx +{ + +namespace +{ + +int constexpr initial_capacity{8}; +int constexpr copy_attempts{600}; // frames to wait for the textures of a layer to load before it's left with what it has +float constexpr default_repeat{8.f}; + +// array textures: diffuse, normal maps, specular/gloss maps +std::array constexpr array_sizes{1024, 512, 256}; + +} // namespace + +bool terrain_materials::init() +{ + if (m_state != 0) + return m_state > 0; + m_state = -1; + if (false == gpu_init()) + return false; + + m_white = make_texture("internal_src:terrain/white", texture_kind::white); + gpu_white(m_textures.texture(m_white)); + char const *names[]{"internal_src:terrain/layers_diffuse", "internal_src:terrain/layers_normal", "internal_src:terrain/layers_specgloss"}; + for (std::size_t i = 0; i < m_arrays.size(); ++i) + { + auto &array{m_arrays[i]}; + array.handle = make_texture(names[i], texture_kind::layers); + array.size = array_sizes[i]; + array.srgb = (i == 0); + gpu_storage(array, initial_capacity, false); + } + m_capacity = initial_capacity; + gpu_changed(); + + // the shader is checked now, so a broken one makes the terrain fall back on plain materials instead of the failure indicator + try + { + m_program = GfxRenderer->Fetch_Shader("terrain"); + } + catch (gfx::shader_exception const &e) + { + ErrorLog("heightmap terrain: can't use the terrain shader: " + std::string(e.what())); + return false; + } + m_state = 1; + return true; +} + +texture_handle terrain_materials::make_texture(std::string const &Name, texture_kind const Kind) +{ + auto const handle{m_textures.create(Name, false, GL_RGBA)}; + auto &texture{m_textures.texture(handle)}; + gpu_texture(texture, Kind); + // the content is supplied here, the texture manager isn't to load, release or replace it + texture.is_ready = true; + texture.is_static = true; + texture.is_rendertarget = true; + texture.is_texstub = false; + texture.data_state = resource_state::good; + return handle; +} + +void terrain_materials::read_material(layer &Layer) +{ + auto const &material{m_materials.material(Layer.material)}; + Layer.repeat = (Layer.size > 0.f ? Layer.size : (material.size.x > 0.f ? material.size.x : default_repeat)); + Layer.normalmap = (material.shader && material.shader->texture_conf.count("normalmap") > 0); + Layer.specgloss = (material.shader && material.shader->texture_conf.count("specgloss") > 0); + Layer.reflection = (std::isfinite(material.params[1].z) ? material.params[1].z : 0.f); +} + +int terrain_materials::layer_index(gfx::terrain_layer const &Layer) +{ + for (auto const &existing : m_layers) + if (existing.material == Layer.material && existing.size == Layer.size) + return existing.index; + + layer entry; + entry.material = Layer.material; + entry.size = Layer.size; + entry.index = static_cast(m_layers.size()); + read_material(entry); + m_layers.push_back(entry); + m_pending = true; + + if (static_cast(m_layers.size()) > m_capacity) + { + // more room: the arrays are made anew, and every layer copied again + auto const capacity{m_capacity * 2}; + for (auto &array : m_arrays) + gpu_storage(array, capacity, true); + m_capacity = capacity; + for (auto &existing : m_layers) + { + existing.copied = false; + existing.attempts = 0; + } + gpu_changed(); + } + return entry.index; +} + +std::size_t terrain_materials::acquire_slot(material_handle const Reuse) +{ + if (Reuse != null_handle) + { + auto const lookup{m_slotof.find(Reuse)}; + if (lookup != m_slotof.end()) + { + m_slots[lookup->second].used = true; + return lookup->second; + } + } + if (false == m_free.empty()) + { + auto const index{m_free.back()}; + m_free.pop_back(); + m_slots[index].used = true; + return index; + } + auto const index{m_slots.size()}; + auto const suffix{std::to_string(index)}; + slot entry; + entry.weights[0] = make_texture("internal_src:terrain/weights0_" + suffix, texture_kind::weights); + entry.weights[1] = make_texture("internal_src:terrain/weights1_" + suffix, texture_kind::weights); + entry.table = make_texture("internal_src:terrain/table_" + suffix, texture_kind::table); + entry.holes = make_texture("internal_src:terrain/holes_" + suffix, texture_kind::holes); + // the textures get their content before the material is made of them, which checks they're there + gpu_blank(entry); + gpu_changed(); + auto const name = [&](texture_handle const Handle) { return "internal_src:" + m_textures.texture(Handle).name; }; + std::string const definition{ + "shader: terrain\n" + "texture1: " + name(m_white) + "\n" + "texture2: " + name(entry.weights[0]) + "\n" + "texture3: " + name(entry.weights[1]) + "\n" + "texture4: " + name(m_arrays[0].handle) + "\n" + "texture5: " + name(m_arrays[1].handle) + "\n" + "texture6: " + name(m_arrays[2].handle) + "\n" + "texture7: " + name(entry.table) + "\n" + "texture8: " + name(entry.holes) + "\n"}; + entry.material = m_materials.create_from_text("internal_src:terrain/chunk_" + suffix, definition); + if (entry.material == null_handle || false == m_materials.material(entry.material).is_good) + { + ErrorLog("heightmap terrain: can't make the material of a chunk"); + m_state = -1; + return static_cast(-1); + } + entry.used = true; + m_slots.push_back(entry); + m_slotof.emplace(entry.material, index); + return index; +} + +material_handle terrain_materials::make(material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement, + int const HoleSamples, std::uint8_t const *Holes) +{ + if (Layers.empty() || false == init()) + return null_handle; + auto const count{std::min(Layers.size(), max_layers)}; + std::array indices{}; + for (std::size_t i = 0; i < count; ++i) + { + if (Layers[i].material == null_handle) + return null_handle; + indices[i] = layer_index(Layers[i]); + } + auto const slotindex{acquire_slot(Reuse)}; + if (slotindex == static_cast(-1)) + return null_handle; + auto &entry{m_slots[slotindex]}; + + // weights, four layers to a texture + auto const painted{Weights != nullptr && Samples > 0 && count > 1}; + auto const side{painted ? Samples + 1 : 1}; + auto const samples{static_cast(side) * side}; + std::vector texels(samples * 4); + for (std::size_t texture = 0; texture < 2; ++texture) + { + std::fill(texels.begin(), texels.end(), 0); + for (std::size_t channel = 0; channel < 4; ++channel) + { + auto const layer{texture * 4 + channel}; + if (layer >= count) + break; + if (false == painted) + { + if (layer == 0) + texels[channel] = 255; + continue; + } + auto const *source{Weights + layer * samples}; + for (std::size_t i = 0; i < samples; ++i) + texels[i * 4 + channel] = source[i]; + } + gpu_weights(entry.weights[texture], side, texels.data()); + } + entry.count = count; + entry.layers = indices; + for (std::size_t i = 0; i < count; ++i) + entry.rotations[i] = Layers[i].rotation; + write_table(entry); + + // the holes: the cells cut out entirely are left out of the mesh, the program of the holes cuts the rest of the openings + auto const cut{Holes != nullptr && HoleSamples > 0}; + { + auto const holeside{cut ? HoleSamples + 1 : 1}; + std::uint8_t const none{0}; + gpu_holes(entry.holes, holeside, cut ? Holes : &none); + gpu_changed(); + } + auto &material{m_materials.material(entry.material)}; + material.params[2] = glm::vec4(Placement.x, Placement.y, 1.f / std::max(1.f, Placement.z), static_cast(side - 1)); + if (m_holesprogram == nullptr && cut) + { + try + { + m_holesprogram = GfxRenderer->Fetch_Shader("terrain_holes"); + } + catch (gfx::shader_exception const &e) + { + ErrorLog("heightmap terrain: can't use the shader of the holes: " + std::string(e.what())); + } + } + // the program of the holes discards fragments, which the depth prepass of the plain one never does + material.shader = (cut && m_holesprogram != nullptr) ? m_holesprogram : m_program; + return entry.material; +} + +void terrain_materials::write_table(slot const &Slot) +{ + // what the layers are: index in the arrays, repeat, which maps they have, reflection + std::array table{}; + for (std::size_t i = 0; i < Slot.count; ++i) + { + auto const &layer{m_layers[Slot.layers[i]]}; + table[i * 4 + 0] = static_cast(layer.index); + table[i * 4 + 1] = 1.f / std::max(0.01f, layer.repeat); + table[i * 4 + 2] = layer.normalmap ? 1.f : 0.f; + table[i * 4 + 3] = layer.specgloss ? 1.f : 0.f; + table[(max_layers + i) * 4 + 0] = layer.reflection; + // the textures turned by the layer of the chunk (the same material can be turned differently in another layer) + auto const angle{glm::radians(static_cast(Slot.rotations[i]))}; + table[(max_layers + i) * 4 + 1] = static_cast(std::cos(angle)); + table[(max_layers + i) * 4 + 2] = static_cast(std::sin(angle)); + } + gpu_table(Slot.table, table.data()); + gpu_changed(); +} + +void terrain_materials::on_environment_change() +{ + if (m_state <= 0) + return; + bool changed{false}; + for (auto &layer : m_layers) + { + auto const &material{m_materials.material(layer.material)}; + if (false == (material.update_on_season_change || material.update_on_weather_change)) + continue; + // the reloaded material can have other textures, other maps and another size + read_material(layer); + layer.copied = false; + layer.attempts = 0; + changed = true; + } + if (false == changed) + return; + m_pending = true; + // the tables of the chunks hold what the layers have, freed slots get theirs anew when they're used again + for (auto const &entry : m_slots) + if (entry.used) + write_table(entry); +} + +void terrain_materials::release(material_handle const Material) +{ + auto const lookup{m_slotof.find(Material)}; + if (lookup == m_slotof.end() || false == m_slots[lookup->second].used) + return; + m_slots[lookup->second].used = false; + m_free.push_back(lookup->second); +} + +bool terrain_materials::copy_layer(layer &Layer) +{ + auto const &material{m_materials.material(Layer.material)}; + auto const slot = [&](char const *Name, int const Fallback) { + if (material.shader) + { + auto const lookup{material.shader->texture_conf.find(Name)}; + if (lookup != material.shader->texture_conf.end()) + return static_cast(lookup->second.id); + return -1; + } + return Fallback; + }; + std::array const slots{slot("diffuse", 0), Layer.normalmap ? slot("normalmap", 1) : -1, Layer.specgloss ? slot("specgloss", 2) : -1}; + // all maps of the layer have to be ready, so they go together + std::array sources{}; + for (std::size_t i = 0; i < slots.size(); ++i) + { + if (slots[i] < 0 || material.textures[slots[i]] == null_handle) + continue; + auto &texture{m_textures.mark_as_used(material.textures[slots[i]])}; + if (false == texture.is_ready) + { + // a texture which is on the card already may have dropped its data after the upload (create() would refuse it then), + // one released by the sweep or never read has to be read first + if (texture.data_state == resource_state::none) + { + texture.reload_on_use = false; + texture.load(); + } + if (texture.data_state == resource_state::failed) + continue; + if (false == texture.create()) + { + if (++Layer.attempts < copy_attempts) + return false; + WriteLog("heightmap terrain: texture \"" + texture.name + "\" isn't ready, the terrain goes without it"); + continue; // waited long enough, the layer goes without it + } + } + sources[i] = &texture; + } + // defaults for maps the layer doesn't have: grey, flat, without specular reflections + std::array, 3> const defaults{{{0.5f, 0.5f, 0.5f, 1.f}, {0.5f, 0.5f, 1.f, 1.f}, {0.f, 1.f, 0.f, 1.f}}}; + for (std::size_t i = 0; i < m_arrays.size(); ++i) + gpu_copy(m_arrays[i], Layer.index, sources[i], defaults[i]); + Layer.copied = true; + return true; +} + +void terrain_materials::update() +{ + if (false == m_pending || m_state <= 0) + return; + + gpu_begin_copies(); + bool copied{false}; + m_pending = false; + for (auto &layer : m_layers) + { + if (layer.copied) + continue; + if (copy_layer(layer)) + copied = true; + else + m_pending = true; + } + gpu_end_copies(copied); +} + +} // namespace gfx diff --git a/rendering/common/terrainmaterials.h b/rendering/common/terrainmaterials.h new file mode 100644 index 00000000..86107078 --- /dev/null +++ b/rendering/common/terrainmaterials.h @@ -0,0 +1,142 @@ +/* +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 "rendering/renderer.h" +#include "model/material.h" +#include "model/Texture.h" + +namespace gfx +{ + +// materials of the heightmap terrain (shader mat_terrain, mat_terrain_holes for chunks with holes), whichever api draws them. +// the textures of the layer materials are copied into texture arrays shared by all chunks; each chunk gets a material +// of its own, with textures holding the weights of its layers and a table of what its layers are. +// what the layers and the chunks are is kept here; the textures are made and filled by the renderer (the gpu_ functions) +class terrain_materials +{ + public: + terrain_materials(material_manager &Materials, texture_bank &Textures) : m_materials(Materials), m_textures(Textures) {} + virtual ~terrain_materials() = default; + + // see gfx_renderer::Terrain_Material. returns null_handle if the terrain shader can't be used + material_handle make(material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement, + int const HoleSamples = 0, std::uint8_t const *Holes = nullptr); + void release(material_handle const Material); + // copies the textures of new layers into the arrays; to be called before the frame is drawn + void update(); + // the layer materials which change with the season or the weather were reloaded, their textures are copied again + void on_environment_change(); + + protected: + static constexpr std::size_t max_layers{8}; + + struct layer + { + material_handle material{null_handle}; + float size{0.f}; // as given by the chunk + float repeat{8.f}; // metres the textures are repeated at + int index{0}; // in the arrays + bool normalmap{false}; + bool specgloss{false}; + float reflection{0.f}; + bool copied{false}; + int attempts{0}; + }; + struct texture_array + { + texture_handle handle{null_handle}; + int size{0}; + bool srgb{false}; + }; + struct slot + { + material_handle material{null_handle}; + std::array weights{null_handle, null_handle}; + texture_handle table{null_handle}; + texture_handle holes{null_handle}; // where the ground is cut, R8; a single texel of 0 without holes + bool used{false}; + // what the table was made of, kept to make it again when a layer changes + std::size_t count{0}; + std::array layers{}; + std::array rotations{}; + }; + // what a texture made here holds + enum class texture_kind + { + white, // a single white texel, rgba8 + layers, // an array of the layer maps, rgba8 (srgb for the diffuse) + weights, // weights of four layers, rgba8 with mipmaps, clamped + table, // the table of the layers of a chunk, max_layers x 2 rgba32f, nearest + holes // the mask of the holes, r8, led between the samples, clamped + }; + + // the textures of the arrays: diffuse, normal maps, specular/gloss + std::array const &arrays() const + { + return m_arrays; + } + + // the objects the textures are drawn with (the copy of the layer maps); false: the terrain can't be drawn + virtual bool gpu_init() = 0; + // the object of a texture made by the bank, of the kind + virtual void gpu_texture(texture_source &Texture, texture_kind const Kind) = 0; + // the white texel + virtual void gpu_white(texture_source &Texture) = 0; + // room for Layers layers in the array; Fresh: the array had layers before, they go + virtual void gpu_storage(texture_array const &Array, int const Layers, bool const Fresh) = 0; + // the content of the textures of a new slot, before its material is made of them: no weights, a table of zeros, no holes + virtual void gpu_blank(slot const &Slot) = 0; + // Side x Side texels of rgba8, the mipmaps made of them + virtual void gpu_weights(texture_handle const Texture, int const Side, std::uint8_t const *Texels) = 0; + // max_layers x 2 texels of rgba32f + virtual void gpu_table(texture_handle const Texture, float const *Table) = 0; + // Side x Side samples of r8 + virtual void gpu_holes(texture_handle const Texture, int const Side, std::uint8_t const *Samples) = 0; + // the copies of the layer maps into the arrays: begun, then a copy per array of each layer copied, ended; Copied: whether any + // was made (the mipmaps of the arrays are made then) + virtual void gpu_begin_copies() = 0; + // Source: the map of the layer, or null when it has none, the array is filled with Default then + virtual void gpu_copy(texture_array const &Array, int const Layer, texture_source *Source, std::array const &Default) = 0; + virtual void gpu_end_copies(bool const Copied) = 0; + // textures were made or given new content (the opengl renderer forgets the textures bound) + virtual void gpu_changed() {} + + material_manager &m_materials; + texture_bank &m_textures; + + private: + bool init(); + texture_handle make_texture(std::string const &Name, texture_kind const Kind); + void read_material(layer &Layer); + int layer_index(gfx::terrain_layer const &Layer); + void write_table(slot const &Slot); + bool copy_layer(layer &Layer); + std::size_t acquire_slot(material_handle const Reuse); + + int m_state{0}; // 0: not set up yet, 1: ready, -1: can't be used + texture_handle m_white{null_handle}; + std::array m_arrays; // diffuse, normal maps, specular/gloss + int m_capacity{0}; // layers the arrays have room for + std::vector m_layers; + bool m_pending{false}; + std::vector m_slots; + std::vector m_free; + std::unordered_map m_slotof; + std::shared_ptr m_program; // of the chunks, mat_terrain + std::shared_ptr m_holesprogram; // of the chunks with holes, mat_terrain_holes +}; + +} // namespace gfx diff --git a/rendering/common/weatherstate.cpp b/rendering/common/weatherstate.cpp new file mode 100644 index 00000000..96724f64 --- /dev/null +++ b/rendering/common/weatherstate.cpp @@ -0,0 +1,343 @@ +/* +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/common/weatherstate.h" + +#include "environment/starcatalog.h" +#include "environment/weather.h" +#include "simulation/simulationenvironment.h" +#include "utilities/Globals.h" +#include "utilities/Timer.h" +#include "widgets/weatherpanel.h" + +namespace +{ + +// how many times the cell of a genus fits in the tile of the shape noise, and its spread in the tile of the coverage noise +double const SHAPE_TILE_CELLS{4.0}; +double const COVERAGE_TILE_SPREADS{4.0}; +// size of the patches of the ground fog +float const FOG_PATCH_SIZE{300.f}; +// the haze the atmosphere draws in the sky already, as an extinction: about the haze of a day with 30 km of visibility +float const SKY_HAZE_EXTINCTION{3.f / 30000.f}; +// near end of the fog volume (view depth, metres) +float const FOG_VOLUME_NEAR{0.5f}; +// bands the panorama is drawn in, one a frame +int const PANORAMA_BANDS{8}; +// brightness of a star of magnitude 0 in the units of the scene +float const STAR_BRIGHTNESS{4.f}; +// stars dimmer than this aren't drawn +float const STAR_LIMIT{6.5f}; + +float luminance(glm::vec3 const &Color) +{ + return glm::dot(Color, glm::vec3{0.2126f, 0.7152f, 0.0722f}); +} + +// the position in a tiling noise, of a period in metres, kept small for the single precision of the shaders +double wrap(double const Value, double const Period) +{ + return Value - Period * std::floor(Value / Period); +} + +// the colour of a star of a colour temperature, linear, of luminance 1, somewhat paler than the black body: the eye sees most +// stars white +glm::vec3 star_color(float const Temperature) +{ + auto const t{std::clamp(Temperature, 1500.f, 40000.f) / 100.f}; + glm::vec3 color; + color.r = (t <= 66.f) ? 255.f : 329.698727446f * std::pow(t - 60.f, -0.1332047592f); + color.g = (t <= 66.f) ? 99.4708025861f * std::log(t) - 161.1195681661f : 288.1221695283f * std::pow(t - 60.f, -0.0755148492f); + color.b = (t >= 66.f) ? 255.f : (t <= 19.f) ? 0.f : 138.5177312231f * std::log(t - 10.f) - 305.0447927307f; + color = glm::pow(glm::clamp(color / 255.f, glm::vec3{0.f}, glm::vec3{1.f}), glm::vec3{2.2f}); + color /= std::max(luminance(color), 1e-3f); + return glm::mix(color, glm::vec3{1.f}, 0.5f); +} + +} // namespace + +namespace gfx +{ + +weather_state::quality_level weather_state::quality(int const Clouds, int const Fog) +{ + quality_level level; + switch (std::clamp(Clouds, 1, 4)) + { + case 1: level = {4, 32, 3, false, {512, 128}, {}}; break; + case 2: level = {2, 48, 4, true, {1024, 256}, {}}; break; + case 3: level = {2, 80, 6, true, {1024, 256}, {}}; break; + default: level = {2, 128, 8, true, {2048, 512}, {}}; break; + } + switch (std::clamp(Fog, 1, 4)) + { + case 1: level.fog_grid = {80, 45, 48}; break; + case 2: level.fog_grid = {160, 90, 64}; break; + case 3: level.fog_grid = {160, 90, 96}; break; + default: level.fog_grid = {240, 135, 128}; break; + } + return level; +} + +weather_state::weather_state() +{ + // a grey fog and no clouds until the first update + for (auto &color : m_weather.fog_horizon) + color = glm::vec4{0.5f, 0.5f, 0.5f, 1.f}; + m_weather.fog_haze = glm::vec4{3.f / 10000.f, 1.f / 800.f, SKY_HAZE_EXTINCTION, 1.f}; + m_weather.fog_light = glm::vec4{0.f, 1.f, 0.6f, 1.f / 5000.f}; +} + +void weather_state::update(float const Atmosphereweight, bool const Skyview, glm::vec3 const &Keylight, glm::vec3 const &Keycolor, glm::vec3 const &Ambient, glm::vec3 const &Fogcolor, glm::dvec3 const &Camera) +{ + auto const &weather{simulation::Environment.weather()}; + auto const &conditions{weather.conditions()}; + m_fog_volume_valid = false; + m_clouds_view_valid = false; + ++m_frame; + + auto const height{static_cast(Camera.y) - weather.ground()}; + auto const time{weather.time()}; + auto const sun{glm::normalize(Keylight)}; + + // the fog. the visibility is where a dark object fades to 5%: the extinction is 3 over it + auto const haze{3.f / conditions.visibility}; + auto const hazerate{1.f / conditions.haze_height}; + m_weather.fog_haze = glm::vec4{haze, hazerate, SKY_HAZE_EXTINCTION, 1.f}; + if (conditions.groundfog_thickness > 0.f) + { + auto const thickness{conditions.groundfog_thickness}; + m_weather.fog_ground = glm::vec4{std::max(3.f / conditions.groundfog_visibility - haze, 0.f), 1.f / (0.25f * thickness), 0.75f * thickness, conditions.fog_noise}; + } + else + m_weather.fog_ground = glm::vec4{0.f, 1.f, 0.f, conditions.fog_noise}; + // the patches drift with the wind, slower than the clouds + auto const drift{glm::dvec2{weather.cloud_offset()} * 0.3}; + auto const patch{static_cast(FOG_PATCH_SIZE)}; + m_weather.fog_noise = glm::vec4{static_cast(wrap(Camera.x - drift.x, patch)), static_cast(wrap(Camera.y, patch)), static_cast(wrap(Camera.z - drift.y, patch)), 1.f / FOG_PATCH_SIZE}; + // the extinction at the height of the camera tells how far things are hidden + auto const extinction{haze * std::exp(-hazerate * std::max(height, 0.f)) + + m_weather.fog_ground.x * std::exp(-m_weather.fog_ground.y * std::max(height - m_weather.fog_ground.z, 0.f))}; + // 2% left at 1.3 visibilities; the draw range is twice this + m_fog_range = std::clamp(3.f / std::max(extinction, 1e-7f) / 1.5f, 5.f, 1e6f); + + // the share of the light of the fog the sun gives, which the shadows hold back + auto const sunlight{luminance(Keycolor) * 1.5f * std::max(sun.y, 0.f)}; + auto const skylight{luminance(Ambient) * 0.65f}; + m_sun_share = (sunlight + skylight > 1e-6f) ? sunlight / (sunlight + skylight) : 0.f; + + // clouds + auto const layers{std::min(conditions.clouds.size(), gl::MAX_CLOUD_LAYERS)}; + m_clouds_drawn = (Global.gfx_clouds > 0) && (layers > 0); + m_average_transmittance = 1.f; + auto const velocity{weather.cloud_velocity()}; + auto const speed{glm::length(velocity)}; + auto const wind{speed > 1e-3f ? glm::dvec2{velocity / speed} : glm::dvec2{1.0, 0.0}}; + m_weather.cloud_wind = glm::vec4{static_cast(wind.x), static_cast(wind.y), speed, m_cloud_detail_scale}; + for (std::size_t i = 0; i < gl::MAX_CLOUD_LAYERS; ++i) + { + auto &target{m_weather.cloud_layers[i]}; + if (i >= layers) + { + target = gl::cloud_layer_ubs{}; + continue; + } + auto const &layer{conditions.clouds[i]}; + auto const &genus{cloud_genus_info(layer.type)}; + target.heights = glm::vec4{layer.base, layer.top, 1.f / std::max(layer.top - layer.base, 1.f), static_cast(layer.type)}; + auto const shapetile{static_cast(genus.cell) * SHAPE_TILE_CELLS}; + auto const coveragetile{static_cast(genus.spread) * COVERAGE_TILE_SPREADS}; + target.shape = glm::vec4{layer.coverage, genus.extinction * layer.density, static_cast(1.0 / shapetile), genus.erosion}; + target.profile = glm::vec4{genus.bottom, genus.top, genus.top_variation, genus.anvil}; + // the higher, the faster the wind carries them + auto const offset{glm::dvec2{weather.cloud_offset()} * (1.0 + static_cast(layer.base) / 8000.0)}; + // the camera's place in the noise, in the space stretched along the wind as the shaders take it + auto const place{glm::dvec2{Camera.x, Camera.z} - offset}; + auto const along{glm::dot(place, wind)}; + auto const stretched{(place - wind * along) + wind * (along / std::max(static_cast(genus.stretch), 1.0))}; + target.motion = glm::vec4{static_cast(wrap(stretched.x, shapetile)), static_cast(wrap(stretched.y, shapetile)), static_cast(1.0 / coveragetile), genus.stretch}; + // the clouds change their shapes slowly; the seed moves the layers apart in the noise + auto const evolution{std::fmod(static_cast(conditions.seed) * 0.6180339887 + static_cast(i) * 0.37 + time * 0.00002, 1.0)}; + target.extra = glm::vec4{genus.flat ? 1.f : 0.f, static_cast(evolution), static_cast(wrap(stretched.x, coveragetile)), static_cast(wrap(stretched.y, coveragetile))}; + m_average_transmittance *= 1.f - std::clamp(layer.coverage * genus.opacity * std::min(layer.density, 1.5f), 0.f, 1.f); + } + if (Global.gfx_clouds <= 0) + m_average_transmittance = 1.f; + m_weather.fog_light = glm::vec4{m_sun_share, m_average_transmittance, 0.6f, fog_density()}; + m_weather.weather_camera = glm::vec4{height, static_cast(std::fmod(time, 100000.0)), m_clouds_drawn ? 1.f : 0.f, m_clouds_drawn ? static_cast(layers) : 0.f}; + m_weather.weather_sun_direction = glm::vec4{sun, sun.y > -0.02f ? 1.f : 0.f}; + m_weather.weather_sun_color = glm::vec4{Keycolor, m_cloud_sun_scale}; + m_weather.weather_ambient = glm::vec4{Ambient * 1.3f, m_cloud_sky_scale}; + m_weather.fog_volume_params = glm::vec4{0.f, FOG_VOLUME_NEAR, 1.f, 1.f}; + m_weather.fog_volume_target = glm::vec4{1.f, 1.f, Global.gfx_fog_range, 1.f - Atmosphereweight}; + + // the colours of the horizon: from the atmosphere on the gpu, the old fog colour otherwise + m_horizon_on_gpu = (Atmosphereweight > 0.f) && Skyview; + if (false == m_horizon_on_gpu) + { + for (auto &color : m_weather.fog_horizon) + color = glm::vec4{Fogcolor, 1.f}; + } + + // the shadows of the clouds + m_shadows_wanted = m_clouds_drawn && Global.gfx_clouds_shadows && (sun.y > 0.02f); + m_weather.cloud_shadow_params = glm::vec4{0.f}; + m_report = to_metar(conditions); +} + +void weather_state::place_shadows(int const Size, glm::dvec3 const &Camera) +{ + // a map snapped to its texels so it doesn't crawl as the camera moves + auto const extent{static_cast(Global.gfx_clouds_shadows_range)}; + auto const texel{extent / static_cast(Size)}; + auto const centre{glm::dvec2{std::round(Camera.x / texel) * texel - Camera.x, std::round(Camera.z / texel) * texel - Camera.z}}; + m_weather.cloud_shadow_params = glm::vec4{static_cast(centre.x), static_cast(centre.y), static_cast(1.0 / extent), 0.f}; +} + +void weather_state::shadows_drawn(bool const Drawn) +{ + m_shadows_valid = Drawn; + if (Drawn) + m_weather.cloud_shadow_params.w = 1.f; +} + +void weather_state::panorama_resized() +{ + m_panorama_full = false; + m_panorama_band = 0; +} + +void weather_state::panorama_band(glm::ivec2 const &Size, int &First, int &Rows) +{ + auto const bandrows{(Size.y + PANORAMA_BANDS - 1) / PANORAMA_BANDS}; + First = m_panorama_full ? m_panorama_band * bandrows : 0; + Rows = m_panorama_full ? bandrows : Size.y; + m_panorama_band = m_panorama_full ? (m_panorama_band + 1) % PANORAMA_BANDS : 0; + m_panorama_full = true; +} + +bool weather_state::continuity(view_state const &Previous, glm::dvec3 const &Camera) +{ + // a jump of the camera (another vehicle, the free camera) leaves nothing to blend with + return Previous.valid && glm::length(Previous.camera - Camera) < 50.0; +} + +glm::vec3 weather_state::fog_jitter(bool const Temporal) const +{ + // the cells are sampled at another point in every frame: along the depth in the Halton sequence of 2, across the view in + // those of 3 and 5 (fog_inject.comp moves the point of every cell by a value of its own) + auto const halton = [](int Index, int const Base) { + float value{0.f}, fraction{1.f}; + for (; Index > 0; Index /= Base) + { + fraction /= static_cast(Base); + value += fraction * static_cast(Index % Base); + } + return value; + }; + auto const sample{(m_frame % 64) + 1}; + return Temporal ? glm::vec3{halton(sample, 3), halton(sample, 5), halton(sample, 2)} : glm::vec3{0.5f}; +} + +glm::vec4 weather_state::fog_slicing(int const Slices, glm::vec3 const &Jitter) +{ + auto const farthest{std::max(Global.gfx_fog_range, FOG_VOLUME_NEAR * 4.f)}; + return glm::vec4{FOG_VOLUME_NEAR, farthest, static_cast(Slices) / std::log((farthest + gl::FOG_VOLUME_BIAS) / (FOG_VOLUME_NEAR + gl::FOG_VOLUME_BIAS)), Jitter.z}; +} + +void weather_state::fog_made(glm::vec4 const &Slicing, glm::ivec3 const &Grid) +{ + m_fog_slicing = Slicing; + m_fog_grid = Grid; + m_fog_volume_valid = true; +} + +void weather_state::setup_pass(bool const Volume, glm::ivec2 const &Size) +{ + auto const volume{Volume && m_fog_volume_valid && (Global.gfx_fog == 1)}; + m_weather.fog_volume_params = volume ? glm::vec4{1.f, m_fog_slicing.x, m_fog_slicing.z, static_cast(m_fog_grid.z)} : glm::vec4{0.f, FOG_VOLUME_NEAR, 1.f, 1.f}; + m_weather.fog_volume_target.x = 1.f / static_cast(std::max(Size.x, 1)); + m_weather.fog_volume_target.y = 1.f / static_cast(std::max(Size.y, 1)); + m_weather.fog_volume_target.z = volume ? m_fog_slicing.y : Global.gfx_fog_range; +} + +std::vector weather_state::star_vertices() +{ + // the stars, in the equatorial frame + std::vector stars; + auto const *catalogue{star_catalogue::stars()}; + for (std::size_t i = 0; i < star_catalogue::size(); ++i) + { + auto const &star{catalogue[i]}; + auto const magnitude{static_cast(star.magnitude) * 0.01f}; + if (magnitude > STAR_LIMIT) + continue; + auto const ascension{glm::radians(static_cast(star.right_ascension) * 0.01f)}; + auto const declination{glm::radians(static_cast(star.declination) * 0.01f)}; + stars.emplace_back(std::cos(declination) * std::cos(ascension), std::cos(declination) * std::sin(ascension), std::sin(declination), magnitude); + // a phase of the twinkling which doesn't repeat from star to star + auto const phase{std::fmod(static_cast(i) * 0.6180339887f, 1.f)}; + stars.emplace_back(star_color(static_cast(star.temperature) * 10.f), phase); + } + return stars; +} + +bool weather_state::stars(float const Sunangle, double const Siderealtime, double const Latitude, glm::mat4 const &View, glm::mat4 &Toworld, glm::mat4 &Toview, glm::vec4 &Params) const +{ + if (!Global.gfx_stars) + return false; + // the stars come out between the civil and the nautical twilight + auto const night{1.f - glm::smoothstep(-12.f, -3.f, Sunangle)}; + if (night <= 0.f) + return false; + // the equatorial frame (z to the pole, x to the vernal equinox) to the sky of the observer (x west, y up, z north) + auto const theta{glm::radians(Siderealtime)}; + auto const phi{glm::radians(Latitude)}; + auto const st{static_cast(std::sin(theta))}, ct{static_cast(std::cos(theta))}; + auto const sp{static_cast(std::sin(phi))}, cp{static_cast(std::cos(phi))}; + glm::mat3 equatorial; + equatorial[0] = glm::vec3{st, cp * ct, -sp * ct}; + equatorial[1] = glm::vec3{-ct, cp * st, -sp * st}; + equatorial[2] = glm::vec3{0.f, sp, cp}; + Toworld = glm::mat4{equatorial}; + Toview = glm::mat4{glm::mat3{View}}; + Params = glm::vec4{STAR_BRIGHTNESS * Global.gfx_stars_brightness * night, static_cast(std::fmod(Timer::GetTime(), 10000.0)), 2.5f, 0.f}; + return true; +} + +void weather_state::draw_debug_ui() +{ + char const *fogmodes[]{"analytic (along the ray, as the old fog)", "volumetric"}; + ImGui::Combo("fog (gfx.fog)", &Global.gfx_fog, fogmodes, IM_ARRAYSIZE(fogmodes)); + char const *levels[]{"low", "medium", "high", "ultra"}; + auto fogquality{std::clamp(Global.gfx_fog_quality, 1, 4) - 1}; + if (ImGui::Combo("fog volume quality (gfx.fog.quality)", &fogquality, levels, IM_ARRAYSIZE(levels))) + Global.gfx_fog_quality = fogquality + 1; + ImGui::SliderFloat("fog volume depth, m (gfx.fog.range)", &Global.gfx_fog_range, 100.f, 10000.f, "%.0f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("lamps in the fog (gfx.fog.lights)", &Global.gfx_fog_lights, 0.f, 4.f); + ImGui::Checkbox("shadows of the sun and headlights in the fog (gfx.fog.shadows)", &Global.gfx_fog_shadows); + ImGui::Checkbox("fog blended over frames (gfx.fog.temporal)", &Global.gfx_fog_temporal); + ImGui::SliderFloat("share of a new frame in the fog (gfx.fog.temporal.share)", &Global.gfx_fog_temporal_share, 0.02f, 1.f); + char const *cloudlevels[]{"none", "low", "medium", "high", "ultra"}; + ImGui::Combo("clouds (gfx.clouds)", &Global.gfx_clouds, cloudlevels, IM_ARRAYSIZE(cloudlevels)); + ImGui::SliderFloat("cloud distance, m (gfx.clouds.distance)", &Global.gfx_clouds_distance, 5000.f, 200000.f, "%.0f", ImGuiSliderFlags_Logarithmic); + ImGui::Checkbox("cloud shadows (gfx.clouds.shadows)", &Global.gfx_clouds_shadows); + ImGui::SliderFloat("cloud shadow map, m (gfx.clouds.shadows.range)", &Global.gfx_clouds_shadows_range, 1000.f, 100000.f, "%.0f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("cloud sunlight (tuning, not saved)", &m_cloud_sun_scale, 0.25f, 3.f); + ImGui::SliderFloat("cloud sky light (tuning, not saved)", &m_cloud_sky_scale, 0.f, 3.f); + ImGui::SliderFloat("cloud edge detail (tuning, not saved)", &m_cloud_detail_scale, 0.f, 3.f); + ImGui::Checkbox("stars (gfx.stars)", &Global.gfx_stars); + ImGui::SliderFloat("star brightness", &Global.gfx_stars_brightness, 0.f, 8.f); + ImGui::Text("fog range %.0f m, sun share of the fog light %.2f, clouds let through %.2f on average", m_fog_range, m_sun_share, m_average_transmittance); + ImGui::Separator(); + ui::weather_controls(simulation::Environment.weather()); +} + +} // namespace gfx diff --git a/rendering/common/weatherstate.h b/rendering/common/weatherstate.h new file mode 100644 index 00000000..b7c254f0 --- /dev/null +++ b/rendering/common/weatherstate.h @@ -0,0 +1,168 @@ +/* +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/. +*/ + +// the weather the renderers draw (the part of the stage F of claude/gl46-renderer-features-plan.md which isn't the work of the +// gpu, taken out of opengl46_weather for the vulkan renderer, stage P2 of claude/vulkan14-renderer-plan.md): the block of the +// weather of the shaders (gl::weather_ubs) made from the weather of the simulation (environment/weather.h) for the frame and for +// each pass, the quality levels, the place of the map of the shadows of the clouds, the bands of the panorama of the clouds, the +// sampling of the fog volume over the frames, and the stars of the catalogue (environment/starcatalog.h) + +#pragma once + +#include +#include + +#include "gl/ubo.h" + +namespace gfx +{ + +class weather_state +{ + public: + struct quality_level + { + int divisor; // of the resolution of the clouds of the main view + int steps; // samples through a layer at most + int light_steps; // samples towards the sun + bool detail; // with the small noise + glm::ivec2 panorama; // size of the panorama + glm::ivec3 fog_grid; // cells of the fog volume + }; + static quality_level quality(int const Clouds, int const Fog); + + // the camera of the previous frame of the main view, for the blending over frames + struct view_state + { + glm::mat4 projection_view{1.f}; // world axes to clip space, camera at the origin + glm::dvec3 camera{0.0}; + glm::vec4 slicing{0.f}; + bool valid{false}; + }; + + // a grey fog and no clouds until the first update + weather_state(); + + // what the weather of the simulation is for the frame: the block, whether the clouds are drawn and cast shadows. before the + // gpu work of the frame. Atmosphereweight: share of the atmosphere in the sky, Skyview: its sky-view table is there to + // give the colours of the horizon; Keylight: world direction towards the sun or the moon, Keycolor: its colour as the + // shading gets it, Ambient: the ambient light of the sky, Fogcolor: the old colour of the fog + void update(float const Atmosphereweight, bool const Skyview, glm::vec3 const &Keylight, glm::vec3 const &Keycolor, glm::vec3 const &Ambient, glm::vec3 const &Fogcolor, glm::dvec3 const &Camera); + // the shadows of the clouds are to be drawn in this frame (after update()) + bool shadows_wanted() const + { + return m_shadows_wanted; + } + // the place of the map of the shadows of Size texels, snapped to its texels so it doesn't crawl as the camera moves + void place_shadows(int const Size, glm::dvec3 const &Camera); + // the map of the shadows was drawn (or not) in this frame + void shadows_drawn(bool const Drawn); + // the rows of the panorama of the clouds of Size to draw in this frame: a band of it, all of it the first time after + // panorama_resized() + void panorama_band(glm::ivec2 const &Size, int &First, int &Rows); + void panorama_resized(); + // a camera close enough to the one of the previous frame to blend with + static bool continuity(view_state const &Previous, glm::dvec3 const &Camera); + // the point the cells of the fog volume are sampled at in this frame (Temporal: it moves from frame to frame) and the slicing of + // the volume of Slices slices it makes + glm::vec3 fog_jitter(bool const Temporal) const; + static glm::vec4 fog_slicing(int const Slices, glm::vec3 const &Jitter); + // the clouds of the main view and the fog volume were made in this frame + void clouds_made() + { + m_clouds_view_valid = true; + } + void fog_made(glm::vec4 const &Slicing, glm::ivec3 const &Grid); + // the fog of a pass: the volume of the main view (made in the same frame), or along the ray. Size: the target of the pass + void setup_pass(bool const Volume, glm::ivec2 const &Size); + + gl::weather_ubs const &block() const + { + return m_weather; + } + // the colours of the horizon come from the sky-view table of the atmosphere on the gpu, not from the cpu + bool horizon_on_gpu() const + { + return m_horizon_on_gpu; + } + // true when the volumetric clouds are drawn (the sky leaves its own overcast look then) + bool clouds_drawn() const + { + return m_clouds_drawn; + } + bool clouds_view_valid() const + { + return m_clouds_view_valid; + } + // true when the clouds cast shadows (the sunlight isn't dimmed by the overcast then, the shadows do it) + bool cloud_shadows() const + { + return m_shadows_valid; + } + bool fog_volume_valid() const + { + return m_fog_volume_valid; + } + // fog density of a regular draw (model_ubs.fog_density): 1 / the fog range + float fog_density() const + { + return 1.f / m_fog_range; + } + // distance the fog hides things at, for the draw range: a dark object fades to about 2% there + float fog_range() const + { + return m_fog_range; + } + // frames updated, the sequence of the sampling over frames + int frame() const + { + return m_frame; + } + + // the stars, in the equatorial frame: for each star the direction and the magnitude, then the colour and the phase of its + // twinkling + static std::vector star_vertices(); + // whether the stars are drawn in the sky pass, and what with: the rotation from the equatorial frame to the world, from the + // world to the view of the pass (View: rotation of its camera), the brightness, the time, the size of the points + bool stars(float const Sunangle, double const Siderealtime, double const Latitude, glm::mat4 const &View, glm::mat4 &Toworld, glm::mat4 &Toview, glm::vec4 &Params) const; + + // controls of the debug window, Volumeavailable: the renderer has the fog volume + void draw_debug_ui(); + + // the cameras of the clouds and of the fog volume of the previous frame + view_state previous_clouds; + view_state previous_fog; + + private: + gl::weather_ubs m_weather{}; + bool m_horizon_on_gpu{false}; + bool m_shadows_wanted{false}; + bool m_shadows_valid{false}; + bool m_clouds_drawn{false}; + bool m_clouds_view_valid{false}; + int m_panorama_band{0}; + bool m_panorama_full{false}; + glm::ivec3 m_fog_grid{0}; + bool m_fog_volume_valid{false}; + float m_fog_range{5000.f}; + glm::vec4 m_fog_slicing{0.f}; + int m_frame{0}; + + // tuning of the look of the clouds in the debug window, not saved: scales of their sunlight, of their sky light and of the + // erosion of their edges by the detail noise (weather_sun_color.w, weather_ambient.w, cloud_wind.w) + float m_cloud_sun_scale{1.f}; + float m_cloud_sky_scale{1.f}; + float m_cloud_detail_scale{1.f}; + // what the debug window shows + float m_sun_share{0.f}; + float m_average_transmittance{1.f}; + std::string m_report; +}; + +} // namespace gfx diff --git a/rendering/opengl46/opengl46exposure.cpp b/rendering/opengl46/opengl46exposure.cpp index 470cdc32..c8ef8e4b 100644 --- a/rendering/opengl46/opengl46exposure.cpp +++ b/rendering/opengl46/opengl46exposure.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/opengl46/opengl46exposure.h" +#include "rendering/common/exposure.h" #include "model/Texture.h" #include "utilities/Globals.h" @@ -17,16 +18,8 @@ http://mozilla.org/MPL/2.0/. 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 @@ -58,12 +51,8 @@ void opengl46_exposure::bind() 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) + for (auto const &size : gfx::exposure_settings::bloom_sizes(Width, Height)) { - 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); @@ -94,19 +83,18 @@ void opengl46_exposure::measure(opengl_texture &Image, double const Deltatime) 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 histogramrange{gfx::exposure_settings::histogram_range()}; + glProgramUniform2f(*m_histogram_program, 0, histogramrange.x, histogramrange.y); 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)); + auto const averagerange{gfx::exposure_settings::average_range()}; + auto const adaptation{gfx::exposure_settings::adaptation(Deltatime)}; + auto const exposurerange{gfx::exposure_settings::exposure_range()}; + glProgramUniform2f(*m_average_program, 0, averagerange.x, averagerange.y); + glProgramUniform4f(*m_average_program, 1, adaptation.x, adaptation.y, adaptation.z, adaptation.w); + glProgramUniform2f(*m_average_program, 2, exposurerange.x, exposurerange.y); 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); @@ -122,7 +110,7 @@ opengl_texture *opengl46_exposure::bloom(bloom_chain &Chain, opengl_texture &Ima // 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}; + Model.param[0] = gfx::exposure_settings::bloom_params(level); 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()); @@ -141,12 +129,7 @@ opengl_texture *opengl46_exposure::bloom(bloom_chain &Chain, opengl_texture &Ima 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}; + return gfx::exposure_settings::tonemap_params(Bloomlevels); } std::string opengl46_exposure::info_times() @@ -161,15 +144,5 @@ std::string opengl46_exposure::info_times() 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); + gfx::exposure_settings::draw_debug_ui(m_readback_data); } diff --git a/rendering/opengl46/opengl46gpuparticles.cpp b/rendering/opengl46/opengl46gpuparticles.cpp index 862f9fde..cb07a3ba 100644 --- a/rendering/opengl46/opengl46gpuparticles.cpp +++ b/rendering/opengl46/opengl46gpuparticles.cpp @@ -10,46 +10,13 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/opengl46/opengl46gpuparticles.h" -#include "rendering/particles.h" -#include "simulation/simulationenvironment.h" -#include "utilities/Globals.h" #include "utilities/Logs.h" -#include "utilities/Timer.h" #include "utilities/utilities.h" #include "gl/vao.h" namespace { -// places of the smoke at most, all sources together (48 bytes each, as many again for the draw); the sources past it get none -std::uint32_t constexpr MAX_SMOKE_PLACES{1u << 19}; -// the origin of the smoke moves by whole steps when the camera gets this far from it -double constexpr ORIGIN_STEP{256.0}; -double constexpr ORIGIN_REACH{1024.0}; -// how much the smoke scatters the light onwards (Henyey-Greenstein) -float constexpr SMOKE_FORWARD{0.3f}; - -// the boxes of the rain and snow around the camera: their sizes (metres) and the shares of the particles in each -std::array constexpr BOX_SIZE{10.f, 24.f, 60.f}; -std::array constexpr BOX_SHARE{0.2f, 0.35f, 0.45f}; - -// what the rain and the snow are like: the speed of the fall (m/s), how long a streak lasts (s, the eye's or the camera's -// exposure), opacity, how much a drop or a flake scatters the light onwards and how bright it is against a white surface -struct precipitation_kind -{ - float fall; - float exposure; - float opacity; - float forward; - float brightness; -}; -precipitation_kind constexpr RAIN{7.f, 0.025f, 0.5f, 0.6f, 0.6f}; -precipitation_kind constexpr SNOW{1.1f, 0.01f, 0.85f, 0.25f, 0.9f}; -// the share of the wind of the environment the drops and flakes drift with (as the old cylinder leant with it) -double constexpr WIND_SHARE{0.5}; -// the camera faster than this from a frame to the next was moved, not driven or flown -double constexpr CAMERA_JUMP{120.0}; - std::unique_ptr make_program(std::vector const &Names) { std::vector> shaders; @@ -76,11 +43,8 @@ opengl46_gpu_particles::~opengl46_gpu_particles() ::glDeleteVertexArrays(1, &m_vao); } -bool opengl46_gpu_particles::init() +bool opengl46_gpu_particles::gpu_init() { - if (m_state != 0) - return m_state > 0; - m_state = -1; try { m_smokesimulate = make_program({"particles_smoke_simulate.comp"}); @@ -109,88 +73,26 @@ bool opengl46_gpu_particles::init() m_precipitationcommand = std::make_unique(4 * sizeof(GLuint), gl::PARTICLE_COMMAND_SSBO_BINDING); m_smokereadback = std::make_unique(4 * sizeof(GLuint)); m_precipitationreadback = std::make_unique(4 * sizeof(GLuint)); - m_state = 1; return true; } -bool opengl46_gpu_particles::smoke_available() +void opengl46_gpu_particles::gpu_clear() { - return init(); -} - -bool opengl46_gpu_particles::precipitation_available() -{ - return init(); -} - -void opengl46_gpu_particles::clear() -{ - m_pool.clear(); - m_requested.clear(); - m_poolfull = false; - m_sourcerecords.clear(); - m_places = 0; m_particles.reset(); m_sources.reset(); m_owners.reset(); m_smokeoutput.reset(); m_precipitationoutput.reset(); - m_precipitationcapacity = 0; - m_originset = false; - m_lastupdate = 0; } -void opengl46_gpu_particles::layout(particle_manager &Particles) +void opengl46_gpu_particles::gpu_layout(std::uint32_t const Places, std::uint32_t const Kept, std::vector const &Owners, std::size_t const Sources) { - auto const &sources{Particles.sequence()}; - // the sources already laid out which asked for the same places since - std::size_t kept{0}; - while ((kept < m_requested.size()) && (kept < sources.size()) && (m_requested[kept] == sources[kept].max_particles())) - ++kept; - if ((kept == m_requested.size()) && (kept == sources.size()) && m_particles) - return; - if (kept < m_requested.size()) - { - // a source went away or changed: all anew - kept = 0; - m_pool.clear(); - m_requested.clear(); - m_places = 0; - m_poolfull = false; - } - auto const oldplaces{m_places}; - - auto first{m_places}; - m_pool.resize(sources.size()); - m_requested.resize(sources.size()); - for (std::size_t i = kept; i < sources.size(); ++i) - { - m_requested[i] = sources[i].max_particles(); - auto const size{static_cast(std::min(m_requested[i], MAX_SMOKE_PLACES))}; - m_pool[i] = pool_source{}; - if (first + size > MAX_SMOKE_PLACES) - { - if (false == m_poolfull) - WriteLog("particles: the smoke of some sources left out, over " + std::to_string(MAX_SMOKE_PLACES) + " places of particles"); - m_poolfull = true; - continue; - } - m_pool[i].first = first; - m_pool[i].size = size; - first += size; - } - m_places = first; - - std::vector owners(std::max(m_places, 1), 0); - for (std::size_t i = 0; i < m_pool.size(); ++i) - std::fill_n(owners.begin() + m_pool[i].first, m_pool[i].size, static_cast(i)); - - auto const places{static_cast(std::max(m_places, 1))}; + auto const places{static_cast(Places)}; // the particles of the places kept go over to the new storage, the new places are dead auto particles{std::make_unique(places * static_cast(sizeof(gl::particle_smoke)), gl::PARTICLE_SSBO_BINDING)}; ::glClearNamedBufferData(*particles, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, nullptr); - if (m_particles && (oldplaces > 0) && (kept > 0)) - ::glCopyNamedBufferSubData(*m_particles, *particles, 0, 0, static_cast(oldplaces) * static_cast(sizeof(gl::particle_smoke))); + if (m_particles && (Kept > 0)) + ::glCopyNamedBufferSubData(*m_particles, *particles, 0, 0, static_cast(Kept) * static_cast(sizeof(gl::particle_smoke))); m_particles = std::move(particles); auto const ensure = [](std::unique_ptr &Buffer, GLsizeiptr const Size, GLuint const Binding) { @@ -201,96 +103,29 @@ void opengl46_gpu_particles::layout(particle_manager &Particles) }; ensure(m_owners, places * static_cast(sizeof(std::uint32_t)), gl::PARTICLE_OWNER_SSBO_BINDING); ensure(m_smokeoutput, places * static_cast(sizeof(gl::particle_draw)), gl::PARTICLE_OUTPUT_SSBO_BINDING); - ensure(m_sources, static_cast(std::max(sources.size(), 1) * sizeof(gl::particle_smoke_source)), gl::PARTICLE_SOURCE_SSBO_BINDING); - m_owners->update(owners.data(), 0, static_cast(owners.size() * sizeof(std::uint32_t))); - m_sourcerecords.resize(sources.size(), gl::particle_smoke_source{}); + ensure(m_sources, static_cast(std::max(Sources, 1) * sizeof(gl::particle_smoke_source)), gl::PARTICLE_SOURCE_SSBO_BINDING); + m_owners->update(Owners.data(), 0, static_cast(Owners.size() * sizeof(std::uint32_t))); } -void opengl46_gpu_particles::simulate_smoke(particle_manager &Particles, glm::dvec3 const &Camera) +void opengl46_gpu_particles::gpu_simulate(smoke_simulation const &Simulation, std::vector const &Records) { - if (false == init()) - return; - layout(Particles); - if (Particles.updates() == m_lastupdate) - return; - m_lastupdate = Particles.updates(); - if (m_places == 0) - return; - - // the origin of the positions near the camera; when it moves, the particles move by as much the other way - glm::vec3 shift{0.f}; - auto const snapped{glm::floor(Camera / ORIGIN_STEP) * ORIGIN_STEP}; - if (false == m_originset) - { - m_origin = snapped; - m_originset = true; - } - else if (glm::length(Camera - m_origin) > ORIGIN_REACH) - { - shift = glm::vec3{m_origin - snapped}; - m_origin = snapped; - } - - // the light of the smoke as the cpu works it out, for gfx.smoke.lighting off - auto const oldlight{(glm::vec3{Global.DayLight.ambient} + 0.35f * glm::vec3{Global.DayLight.diffuse}) * simulation::Environment.light_intensity()}; - auto const &sources{Particles.sequence()}; - m_smokealive = 0; - for (std::size_t i = 0; i < sources.size(); ++i) - { - auto const &state{sources[i].gpu()}; - auto &pool{m_pool[i]}; - auto &record{m_sourcerecords[i]}; - auto const spawns{(pool.size > 0) ? std::min(state.spawns, pool.size) : 0u}; - record.pool = glm::uvec4{pool.first, pool.size, pool.head, spawns}; - record.location = glm::vec4{glm::vec3{state.location - m_origin}, 0.f}; - record.previous = glm::vec4{glm::vec3{state.previous - m_origin}, 0.f}; - record.launch = glm::vec4{state.inclination[0], state.inclination[1], state.velocity[0], state.velocity[1]}; - record.initial = glm::vec4{state.size[0], state.size[1], state.opacity[0], state.opacity[1]}; - record.sizechange = glm::vec4{state.sizechange[0], state.sizechange[1], state.sizechange[2], state.spread}; - record.opacitychange = glm::vec4{state.opacitychange[0], state.opacitychange[1], state.opacitychange[2], 0.f}; - record.motion = glm::vec4{state.rise, state.damping, state.risemin, 0.f}; - record.settling = glm::vec4{state.settling, state.drag, state.settles ? 1.f : 0.f, state.fadein}; - record.color = glm::vec4{glm::clamp(state.color, glm::vec3{0.f}, glm::vec3{1.f}), 0.f}; - record.oldcolor = glm::vec4{glm::clamp(state.color * oldlight, glm::vec3{0.f}, glm::vec3{1.f}), 0.f}; - if (pool.size > 0) - { - pool.head = (pool.head + spawns) % pool.size; - if (spawns > 0) - m_smokealive += pool.size; - } - } - if (false == m_sourcerecords.empty()) - m_sources->update(m_sourcerecords.data(), 0, static_cast(m_sourcerecords.size() * sizeof(gl::particle_smoke_source))); + if (false == Records.empty()) + m_sources->update(Records.data(), 0, static_cast(Records.size() * sizeof(gl::particle_smoke_source))); m_simulatetimer.begin(); m_particles->bind_base(); m_sources->bind_base(); m_owners->bind_base(); - auto const wind{glm::vec3{simulation::Environment.wind()}}; auto &program{*m_smokesimulate}; - ::glProgramUniform1ui(program, 0, m_places); - ::glProgramUniform4f(program, 1, static_cast(Particles.timedelta()), shift.x, shift.y, shift.z); - ::glProgramUniform4f(program, 2, wind.x, wind.y, wind.z, 0.f); - ::glProgramUniform1ui(program, 3, static_cast(m_lastupdate)); - program.dispatch(groups(m_places)); + ::glProgramUniform1ui(program, 0, Simulation.places); + ::glProgramUniform4fv(program, 1, 1, glm::value_ptr(Simulation.step)); + ::glProgramUniform4fv(program, 2, 1, glm::value_ptr(Simulation.wind)); + ::glProgramUniform1ui(program, 3, static_cast(Simulation.seed)); + program.dispatch(groups(Simulation.places)); ::glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); m_simulatetimer.end(); } -std::array opengl46_gpu_particles::planes(view const &View) -{ - // the sides and the near side of the clip space, w >= 0 (the far plane is at infinity) - auto const matrix{glm::transpose(View.projection * View.modelview)}; - std::array result{matrix[3] + matrix[0], matrix[3] - matrix[0], matrix[3] + matrix[1], matrix[3] - matrix[1], matrix[3]}; - for (auto &plane : result) - { - auto const length{glm::length(glm::vec3{plane})}; - if (length > 0.f) - plane /= length; - } - return result; -} - void opengl46_gpu_particles::collect(gl::readback &Readback, std::size_t &Drawn) { GLuint command[4]; @@ -317,11 +152,9 @@ void opengl46_gpu_particles::draw(GLuint const Output, GLuint const Command, gl: ::glEnable(GL_CULL_FACE); } -std::size_t opengl46_gpu_particles::render_smoke(view const &View) +void opengl46_gpu_particles::gpu_render_smoke(smoke_draw const &Draw, std::size_t &Drawn) { - if ((false == init()) || (m_places == 0)) - return 0; - collect(*m_smokereadback, m_smokedrawn); + collect(*m_smokereadback, Drawn); m_smoketimer.begin(); GLuint const command[4]{4, 0, 0, 0}; @@ -332,125 +165,60 @@ std::size_t opengl46_gpu_particles::render_smoke(view const &View) m_smokeoutput->bind_base(); m_smokecommand->bind_base(); auto &program{*m_smokeprepare}; - auto const origin{glm::vec3{m_origin - View.camera}}; - auto const viewplanes{planes(View)}; - ::glProgramUniform1ui(program, 0, m_places); - ::glProgramUniform3f(program, 1, origin.x, origin.y, origin.z); - ::glProgramUniformMatrix4fv(program, 2, 1, GL_FALSE, glm::value_ptr(View.modelview)); - ::glProgramUniform4f(program, 3, Global.gfx_smoke_lighting ? 1.f : 0.f, SMOKE_FORWARD, static_cast(m_places), 0.f); - ::glProgramUniform4fv(program, 4, 5, glm::value_ptr(viewplanes[0])); - program.dispatch(groups(m_places)); + ::glProgramUniform1ui(program, 0, Draw.places); + ::glProgramUniform3fv(program, 1, 1, glm::value_ptr(Draw.origin)); + ::glProgramUniformMatrix4fv(program, 2, 1, GL_FALSE, glm::value_ptr(Draw.view)); + ::glProgramUniform4fv(program, 3, 1, glm::value_ptr(Draw.options)); + ::glProgramUniform4fv(program, 4, 5, glm::value_ptr(Draw.planes[0])); + program.dispatch(groups(Draw.places)); ::glMemoryBarrier(GL_COMMAND_BARRIER_BIT | GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT); - ::glProgramUniform4f(*m_smokedraw, 0, View.soft ? Global.gfx_particles_soft : 0.f, 0.f, 0.f, 0.f); + ::glProgramUniform4fv(*m_smokedraw, 0, 1, glm::value_ptr(Draw.soft)); draw(*m_smokeoutput, *m_smokecommand, *m_smokedraw); m_smoketimer.end(); m_smokereadback->push(*m_smokecommand); - return m_smokedrawn; } -std::size_t opengl46_gpu_particles::render_precipitation(view const &View, bool const Snow, float const Intensity, unsigned int const Frame) +void opengl46_gpu_particles::gpu_collect_precipitation(std::size_t &Drawn) { - if (false == init()) - return 0; - collect(*m_precipitationreadback, m_precipitationdrawn); + collect(*m_precipitationreadback, Drawn); +} - auto const &kind{Snow ? SNOW : RAIN}; - // once per frame: the time moves on, the particles drift with the wind and fall, the camera has its velocity - if ((false == m_framestarted) || (Frame != m_frame)) - { - m_framestarted = true; - m_frame = Frame; - auto const seconds{Timer::GetDeltaTime()}; - if (seconds > 0.0) - { - auto const wind{glm::dvec3{simulation::Environment.wind()} * WIND_SHARE}; - for (std::size_t box = 0; box < BOX_SIZE.size(); ++box) - { - auto const size{static_cast(BOX_SIZE[box])}; - m_drift[box] = glm::mod(m_drift[box] + wind * seconds, size); - m_fall[box] = std::fmod(m_fall[box] + kind.fall * seconds, 16.0 * size); - } - m_time = std::fmod(m_time + seconds, 3600.0); - if (m_camerastarted) - { - auto const velocity{(View.camera - m_lastcamera) / seconds}; - if (glm::length(velocity) < CAMERA_JUMP) - m_cameravelocity = glm::mix(m_cameravelocity, velocity, 1.0 - std::exp(-10.0 * seconds)); - } - } - m_lastcamera = View.camera; - m_camerastarted = true; - } - - // the particles, more of them in heavier rain or snow - auto const most{static_cast(std::max(Global.gfx_precipitation_particles, 0))}; - auto const count{static_cast(most * std::clamp(0.15f + 0.85f * Intensity, 0.f, 1.f))}; - m_precipitationcount = count; - m_precipitationsnow = Snow; - if (count == 0) - return 0; - if (m_precipitationcapacity < count) - { - m_precipitationcapacity = std::max(count, most); - auto const size{static_cast(m_precipitationcapacity) * static_cast(sizeof(gl::particle_draw))}; - if (!m_precipitationoutput) - m_precipitationoutput = std::make_unique(size, gl::PARTICLE_OUTPUT_SSBO_BINDING); - else - m_precipitationoutput->resize(size); - } +void opengl46_gpu_particles::gpu_precipitation_storage(std::uint32_t const Capacity) +{ + auto const size{static_cast(Capacity) * static_cast(sizeof(gl::particle_draw))}; + if (!m_precipitationoutput) + m_precipitationoutput = std::make_unique(size, gl::PARTICLE_OUTPUT_SSBO_BINDING); + else + m_precipitationoutput->resize(size); +} +void opengl46_gpu_particles::gpu_render_precipitation(precipitation_draw const &Draw) +{ m_precipitationtimer.begin(); GLuint const command[4]{4, 0, 0, 0}; m_precipitationcommand->update(command, 0, sizeof(command)); m_precipitationoutput->bind_base(); m_precipitationcommand->bind_base(); auto &program{*m_precipitationprepare}; - auto const second{static_cast(count * BOX_SHARE[0])}; - auto const third{static_cast(count * (BOX_SHARE[0] + BOX_SHARE[1]))}; - ::glProgramUniform4ui(program, 0, second, third, count, Snow ? 1u : 0u); - std::array offsets; - for (std::size_t box = 0; box < BOX_SIZE.size(); ++box) - { - auto const size{static_cast(BOX_SIZE[box])}; - offsets[box] = glm::vec4{glm::vec3{glm::mod(m_drift[box] - glm::mod(View.camera, size), size)}, static_cast(m_fall[box])}; - } - ::glProgramUniform4fv(program, 1, 3, glm::value_ptr(offsets[0])); - ::glProgramUniform4f(program, 4, BOX_SIZE[0], BOX_SIZE[1], BOX_SIZE[2], 0.f); - auto const velocity{glm::vec3{glm::dvec3{simulation::Environment.wind()} * WIND_SHARE - m_cameravelocity}}; - ::glProgramUniform4f(program, 5, velocity.x, velocity.y, velocity.z, kind.fall); - ::glProgramUniform4f(program, 6, static_cast(m_time), kind.exposure, kind.opacity, static_cast(m_precipitationcapacity)); - ::glProgramUniformMatrix4fv(program, 7, 1, GL_FALSE, glm::value_ptr(View.modelview)); - ::glProgramUniform4f(program, 11, 1.f, kind.forward, kind.brightness, 0.f); - auto const viewplanes{planes(View)}; - ::glProgramUniform4fv(program, 12, 5, glm::value_ptr(viewplanes[0])); - program.dispatch(groups(count)); + ::glProgramUniform4uiv(program, 0, 1, glm::value_ptr(Draw.counts)); + ::glProgramUniform4fv(program, 1, 3, glm::value_ptr(Draw.offsets[0])); + ::glProgramUniform4fv(program, 4, 1, glm::value_ptr(Draw.sizes)); + ::glProgramUniform4fv(program, 5, 1, glm::value_ptr(Draw.velocity)); + ::glProgramUniform4fv(program, 6, 1, glm::value_ptr(Draw.timing)); + ::glProgramUniformMatrix4fv(program, 7, 1, GL_FALSE, glm::value_ptr(Draw.view)); + ::glProgramUniform4fv(program, 11, 1, glm::value_ptr(Draw.options)); + ::glProgramUniform4fv(program, 12, 5, glm::value_ptr(Draw.planes[0])); + program.dispatch(groups(Draw.count)); ::glMemoryBarrier(GL_COMMAND_BARRIER_BIT | GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT); - // the width of a pixel a metre in front of the camera, for the least width of a streak - auto const pixel{(View.height > 0) && (View.projection[1][1] != 0.f) ? 2.f / (std::abs(View.projection[1][1]) * static_cast(View.height)) : 0.f}; - ::glProgramUniform4f(*m_precipitationdraw, 0, View.soft ? 0.25f * Global.gfx_particles_soft : 0.f, 0.f, 0.f, 0.f); - ::glProgramUniform4f(*m_precipitationdraw, 1, pixel, 0.f, 0.f, 0.f); + ::glProgramUniform4fv(*m_precipitationdraw, 0, 1, glm::value_ptr(Draw.soft)); + ::glProgramUniform4fv(*m_precipitationdraw, 1, 1, glm::value_ptr(Draw.pixel)); draw(*m_precipitationoutput, *m_precipitationcommand, *m_precipitationdraw); m_precipitationtimer.end(); m_precipitationreadback->push(*m_precipitationcommand); - return m_precipitationdrawn; -} - -std::string opengl46_gpu_particles::info() const -{ - if (m_state <= 0) - return {}; - std::string text; - if (m_places > 0) - text += " smoke of the gpu: " + std::to_string(m_smokedrawn) + " drawn, " + std::to_string(m_places) + " places of " + std::to_string(m_pool.size()) + " sources (" - + std::to_string(m_smokealive) + " of the ones which bore lately)" + (m_poolfull ? ", some sources left out" : "") + "\n"; - if (m_precipitationcount > 0) - text += std::string(" ") + (m_precipitationsnow ? "snow" : "rain") + " of the gpu: " + std::to_string(m_precipitationdrawn) + " of " + std::to_string(m_precipitationcount) - + " drawn\n"; - return text; } std::string opengl46_gpu_particles::info_times() @@ -460,13 +228,13 @@ std::string opengl46_gpu_particles::info_times() if (auto const time = Timer.result()) text += std::string(text.empty() ? " particles:" : ",") + " " + Name + " " + to_string(static_cast(*time) / 1000000.0, 3) + " ms"; }; - if (m_places > 0) + if (places() > 0) { add(m_simulatetimer, "smoke simulation"); add(m_smoketimer, "smoke"); } - if (m_precipitationcount > 0) - add(m_precipitationtimer, m_precipitationsnow ? "snow" : "rain"); + if (precipitation_count() > 0) + add(m_precipitationtimer, precipitation_snow() ? "snow" : "rain"); if (false == text.empty()) text += "\n"; return text; diff --git a/rendering/opengl46/opengl46gpuparticles.h b/rendering/opengl46/opengl46gpuparticles.h index 6b24e029..3922f956 100644 --- a/rendering/opengl46/opengl46gpuparticles.h +++ b/rendering/opengl46/opengl46gpuparticles.h @@ -9,87 +9,42 @@ http://mozilla.org/MPL/2.0/. #pragma once -#include #include #include -#include -#include +#include "rendering/common/gpuparticles.h" #include "gl/shader.h" #include "gl/ssbo.h" -#include "gl/ubo.h" #include "gl/query.h" #include "gl/readback.h" -class particle_manager; - -// particles of the gpu for the opengl 4.6 renderer: the smoke and the rain or snow. -// -// smoke: the sources (particle_manager in its gpu mode) work out on the cpu only how many particles they bear in an update of -// the simulation and what these start from; the particles are kept in a storage buffer, a ring of places for each source, and -// simulated by a compute pass once per update (particles_smoke_simulate.comp). in every colour pass which draws them another -// compute pass (particles_smoke_prepare.comp) leaves out the dead ones and the ones out of the view, lights the rest with the -// lights of the pass and adds them to an indirect draw of billboards (particles_smoke.vert/.frag). -// -// rain and snow: particles in three boxes around the camera, each repeated along the world axes, which keep no state: their -// place follows from their number, how far the wind took them and how far they fell (particles_precipitation.comp), drawn as -// streaks of their motion against the camera (particles_precipitation.vert/.frag). -// -// the positions of the smoke are kept from an origin near the camera, moved by whole steps as the camera goes, so they keep -// their precision far from the centre of the scenery -class opengl46_gpu_particles +// particles of the gpu for the opengl 4.6 renderer (gfx::gpu_particles): the storage buffers, the compute programs and the +// indirect draws +class opengl46_gpu_particles : public gfx::gpu_particles { public: + using view = gfx::gpu_particles::view; + opengl46_gpu_particles() = default; - ~opengl46_gpu_particles(); - opengl46_gpu_particles(opengl46_gpu_particles const &) = delete; - opengl46_gpu_particles &operator=(opengl46_gpu_particles const &) = delete; + ~opengl46_gpu_particles() override; - // what a colour pass draws with - struct view - { - glm::mat4 modelview; // world axes around the camera to the view space of the pass - glm::mat4 projection; - glm::dvec3 camera; - int height{0}; // of the target, pixels - bool soft{false}; // the resolved depth of the scene drawn so far is bound to gl::SCENE_DEPTH_TEX - }; - - // whether the programs could be made; without them the smoke stays on the cpu and the rain is the old cylinder - bool smoke_available(); - bool precipitation_available(); - // once per update of the particle manager (in its gpu mode): the births and the change of the smoke. Camera: of the view - void simulate_smoke(particle_manager &Particles, glm::dvec3 const &Camera); - // draws the smoke the view sees, with the texture bound to unit 0. returns: particles drawn, as read back a few frames later - std::size_t render_smoke(view const &View); - // draws the rain (Snow false) or the snow of the intensity 0-1. Frame: number of the frame, the time moves on once per frame. - // returns: particles drawn, read back - std::size_t render_precipitation(view const &View, bool Snow, float Intensity, unsigned int Frame); - // drops what is on the gpu, e.g. when the context goes - void clear(); - - std::string info() const; std::string info_times(); - private: - struct pool_source - { - std::uint32_t first{0}; - std::uint32_t size{0}; - std::uint32_t head{0}; - }; + protected: + bool gpu_init() override; + void gpu_layout(std::uint32_t const Places, std::uint32_t const Kept, std::vector const &Owners, std::size_t const Sources) override; + void gpu_simulate(smoke_simulation const &Simulation, std::vector const &Records) override; + void gpu_render_smoke(smoke_draw const &Draw, std::size_t &Drawn) override; + void gpu_collect_precipitation(std::size_t &Drawn) override; + void gpu_precipitation_storage(std::uint32_t const Capacity) override; + void gpu_render_precipitation(precipitation_draw const &Draw) override; + void gpu_clear() override; - bool init(); - // the places of the sources in the storage: sources added after the others get theirs after the places already given, and - // the particles stay; otherwise all is laid out anew - void layout(particle_manager &Particles); + private: // reads back the instances counted by a pass; Drawn is left as it was when nothing came back yet static void collect(gl::readback &Readback, std::size_t &Drawn); - // the planes of the view of the pass, for the culling of the compute passes - static std::array planes(view const &View); void draw(GLuint Output, GLuint Command, gl::program &Program); - int m_state{0}; // 0: not set up yet, 1: ready, -1: can't be used std::unique_ptr m_smokesimulate; std::unique_ptr m_smokeprepare; std::unique_ptr m_smokedraw; @@ -98,22 +53,12 @@ class opengl46_gpu_particles GLuint m_vao{0}; // the attributes of the instances, from the buffer of the draw bound to it before the draw // smoke - std::vector m_pool; - std::vector m_requested; // the sizes the sources asked for - std::vector m_sourcerecords; - std::uint32_t m_places{0}; - bool m_poolfull{false}; // some sources got no places std::unique_ptr m_particles; std::unique_ptr m_sources; std::unique_ptr m_owners; std::unique_ptr m_smokeoutput; std::unique_ptr m_smokecommand; std::unique_ptr m_smokereadback; - std::uint64_t m_lastupdate{0}; - glm::dvec3 m_origin{0.0}; - bool m_originset{false}; - std::size_t m_smokedrawn{0}; - std::size_t m_smokealive{0}; // places of the sources which bore particles lately gl::gpu_timer m_simulatetimer; gl::gpu_timer m_smoketimer; @@ -121,17 +66,5 @@ class opengl46_gpu_particles std::unique_ptr m_precipitationoutput; std::unique_ptr m_precipitationcommand; std::unique_ptr m_precipitationreadback; - std::uint32_t m_precipitationcapacity{0}; - unsigned int m_frame{0}; - bool m_framestarted{false}; - std::array m_drift{}; // how far the wind took the particles of each box, within the box - std::array m_fall{}; // how far they fell, within 16 sizes of the box - double m_time{0.0}; // seconds, for the flutter of the flakes - glm::dvec3 m_lastcamera{0.0}; - glm::dvec3 m_cameravelocity{0.0}; - bool m_camerastarted{false}; - std::size_t m_precipitationdrawn{0}; - std::uint32_t m_precipitationcount{0}; - bool m_precipitationsnow{false}; gl::gpu_timer m_precipitationtimer; }; diff --git a/rendering/opengl46/opengl46grass.cpp b/rendering/opengl46/opengl46grass.cpp index a72f2017..a4e51615 100644 --- a/rendering/opengl46/opengl46grass.cpp +++ b/rendering/opengl46/opengl46grass.cpp @@ -12,51 +12,12 @@ http://mozilla.org/MPL/2.0/. #include "rendering/openglcamera.h" #include "model/Texture.h" -#include "simulation/simulationenvironment.h" -#include "simulation/simulationtime.h" #include "utilities/Globals.h" #include "utilities/Logs.h" -#include "utilities/Timer.h" namespace { -int constexpr max_blades{65536}; // drawn in a tile at most -float constexpr blade_reach{1.5f}; // metres the bounds of a tile reach above its ground, for the blades - -// the share of the blades drawn at specified distance: fewer further away, for speed (from Thinning), and fewer and fewer from -// Fade to Reach, where the grass ends; the same as lod_share() of grass.vert -float lod_share(float const Distance, float const Thinning, float const Fade, float const Reach) -{ - auto const share{Distance <= Thinning ? 1.f : std::pow(Thinning / Distance, 1.5f)}; - auto const t{std::clamp((Distance - Fade) / std::max(Reach - Fade, 1e-3f), 0.f, 1.f)}; - return share * (1.f - t * t * (3.f - 2.f * t)); -} - -// the grass through the year, by the day of the year: x how dry it is, y how fresh (the green of spring), z how grown (share -// of the height). the turns follow the seasons of the textures (world_environment::compute_season(): winter till day 65, -// spring till 158, summer till 252, autumn till 341): dormant and dry in winter, greening up and growing through spring, -// fresh in May, drying out from August, the dry grass lying lower towards winter -glm::vec3 grass_season(int const Yearday) -{ - struct point - { - float day; - glm::vec3 value; - }; - static point const year[]{{1.f, {0.80f, 0.f, 0.55f}}, {65.f, {0.80f, 0.f, 0.55f}}, {100.f, {0.45f, 0.3f, 0.70f}}, {135.f, {0.05f, 1.f, 0.90f}}, - {170.f, {0.f, 0.4f, 1.f}}, {200.f, {0.10f, 0.f, 1.f}}, {240.f, {0.35f, 0.f, 1.f}}, {280.f, {0.50f, 0.f, 0.95f}}, - {320.f, {0.65f, 0.f, 0.80f}}, {341.f, {0.80f, 0.f, 0.60f}}, {367.f, {0.80f, 0.f, 0.55f}}}; - auto const day{std::clamp(static_cast(Yearday), 1.f, 366.f)}; - for (std::size_t i = 1; i < std::size(year); ++i) - if (day <= year[i].day) - { - auto const t{(day - year[i - 1].day) / (year[i].day - year[i - 1].day)}; - return glm::mix(year[i - 1].value, year[i].value, t * t * (3.f - 2.f * t)); - } - return year[std::size(year) - 1].value; -} - GLuint make_texture(GLenum const Format, int const Width, int const Height, GLenum const Filter) { GLuint texture{0}; @@ -112,37 +73,28 @@ bool opengl46_grass::init() return true; } -void opengl46_grass::release(chunk &Chunk) +void opengl46_grass::gpu_release(std::uint64_t const Id) { - for (auto *texture : {&Chunk.heights, &Chunk.density, &Chunk.holes}) + auto const lookup{m_textures.find(Id)}; + if (lookup == m_textures.end()) + return; + auto &chunk{lookup->second}; + for (auto *texture : {&chunk.heights, &chunk.density, &chunk.holes}) if (*texture != 0) { ::glDeleteTextures(1, texture); *texture = 0; } + m_textures.erase(lookup); } -void opengl46_grass::clear() +void opengl46_grass::gpu_upload(std::uint64_t const Id, gfx::grass_chunk const &Source) { - for (auto &entry : m_chunks) - release(entry.second); - m_chunks.clear(); -} - -void opengl46_grass::upload(chunk &Chunk, gfx::grass_chunk const &Source) -{ - release(Chunk); - Chunk.version = Source.version; - Chunk.corner = Source.corner; - Chunk.size = Source.size; - Chunk.cells = Source.cells; - Chunk.samples = Source.samples; - Chunk.kinds = Source.kinds; - + auto &chunk{m_textures[Id]}; auto const side{Source.cells + 1}; - Chunk.heights = make_texture(GL_R32F, side, side, GL_NEAREST); + chunk.heights = make_texture(GL_R32F, side, side, GL_NEAREST); ::glPixelStorei(GL_UNPACK_ALIGNMENT, 4); - ::glTextureSubImage2D(Chunk.heights, 0, 0, 0, side, side, GL_RED, GL_FLOAT, Source.heights); + ::glTextureSubImage2D(chunk.heights, 0, 0, 0, side, side, GL_RED, GL_FLOAT, Source.heights); // the densities of the kinds side by side, as the channels of a texel auto const samples{Source.samples + 1}; @@ -154,186 +106,57 @@ void opengl46_grass::upload(chunk &Chunk, gfx::grass_chunk const &Source) for (std::size_t kind = 0; kind < kinds; ++kind) for (std::size_t i = 0; i < plane; ++i) texels[i * 4 + kind] = Source.density[kind * plane + i]; - Chunk.density = make_texture(GL_RGBA8, samples, samples, GL_LINEAR); - ::glTextureSubImage2D(Chunk.density, 0, 0, 0, samples, samples, GL_RGBA, GL_UNSIGNED_BYTE, texels.data()); + chunk.density = make_texture(GL_RGBA8, samples, samples, GL_LINEAR); + ::glTextureSubImage2D(chunk.density, 0, 0, 0, samples, samples, GL_RGBA, GL_UNSIGNED_BYTE, texels.data()); std::uint8_t const none{0}; auto const holeside{(Source.holes > 0 && Source.hole_mask != nullptr) ? Source.holes + 1 : 1}; - Chunk.holes = make_texture(GL_R8, holeside, holeside, GL_LINEAR); + chunk.holes = make_texture(GL_R8, holeside, holeside, GL_LINEAR); ::glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - ::glTextureSubImage2D(Chunk.holes, 0, 0, 0, holeside, holeside, GL_RED, GL_UNSIGNED_BYTE, holeside > 1 ? Source.hole_mask : &none); + ::glTextureSubImage2D(chunk.holes, 0, 0, 0, holeside, holeside, GL_RED, GL_UNSIGNED_BYTE, holeside > 1 ? Source.hole_mask : &none); ::glPixelStorei(GL_UNPACK_ALIGNMENT, 4); - - // the ground and the densest place of every kind of each tile, to skip the empty tiles and to draw as many blades as needed - auto const spacing{static_cast(Source.size) / Source.cells}; - auto const step{static_cast(Source.size) / Source.samples}; - auto const tilesize{static_cast(Source.size) / tiles}; - for (int tz = 0; tz < tiles; ++tz) - for (int tx = 0; tx < tiles; ++tx) - { - auto &part{Chunk.parts[static_cast(tz) * tiles + tx]}; - auto const range = [](double const From, double const To, double const Step, int const Last) { - return std::make_pair(std::clamp(static_cast(std::floor(From / Step)), 0, Last), std::clamp(static_cast(std::ceil(To / Step)), 0, Last)); - }; - auto const [gx0, gx1]{range(tx * tilesize, (tx + 1) * tilesize, spacing, Source.cells)}; - auto const [gz0, gz1]{range(tz * tilesize, (tz + 1) * tilesize, spacing, Source.cells)}; - part.low = std::numeric_limits::max(); - part.high = -std::numeric_limits::max(); - for (int z = gz0; z <= gz1; ++z) - for (int x = gx0; x <= gx1; ++x) - { - auto const height{Source.heights[static_cast(z) * side + x]}; - part.low = std::min(part.low, height); - part.high = std::max(part.high, height); - } - part.most.fill(0.f); - auto const [sx0, sx1]{range(tx * tilesize, (tx + 1) * tilesize, step, Source.samples)}; - auto const [sz0, sz1]{range(tz * tilesize, (tz + 1) * tilesize, step, Source.samples)}; - for (std::size_t kind = 0; kind < kinds; ++kind) - { - std::uint8_t most{0}; - for (int z = sz0; z <= sz1; ++z) - for (int x = sx0; x <= sx1; ++x) - most = std::max(most, Source.density[kind * plane + static_cast(z) * samples + x]); - part.most[kind] = most / 255.f; - } - } -} - -void opengl46_grass::update(std::vector const &Kinds, std::vector const &Chunks) -{ - m_kinds = Kinds; - for (auto &entry : m_chunks) - entry.second.listed = false; - if (false == Global.gfx_grass) - { - clear(); - return; - } - for (auto const &source : Chunks) - { - if (source.heights == nullptr || source.density == nullptr || source.cells <= 0 || source.samples <= 0) - continue; - auto &entry{m_chunks[source.id]}; - entry.listed = true; - if (entry.heights == 0 || entry.version != source.version) - upload(entry, source); - } - // what's no longer near goes - for (auto it = m_chunks.begin(); it != m_chunks.end();) - { - if (it->second.listed) - { - ++it; - continue; - } - release(it->second); - it = m_chunks.erase(it); - } } std::size_t opengl46_grass::render(opengl_camera const &Camera, glm::mat4 const &View, std::function const &Modelview) { - if (false == Global.gfx_grass || m_chunks.empty() || m_kinds.empty() || false == init()) + if (false == prepare(Camera.position(), [&](scene::bounding_area const &Area) { return Camera.visible(Area); }) || false == init()) return 0; - // whole up to the distance, thinning out to the fading distance - auto const fade{std::max(5.f, Global.gfx_grass_distance)}; - auto const reach{std::max(fade + 1.f, Global.gfx_grass_fadedistance)}; - auto const thinning{std::max(3.f, fade * 0.2f)}; - auto const multiplier{std::max(0.f, Global.gfx_grass_density)}; - auto const camera{Camera.position()}; - m_program->bind(); m_vao->bind(); ::glDisable(GL_CULL_FACE); - ::glUniform4f(m_uniforms.lod, thinning, fade, reach, 0.f); - auto const season{grass_season(simulation::Time.year_day())}; - ::glUniform4f(m_uniforms.season, season.x, season.y, season.z, 0.f); - // the wind sways the blades along it, harder in the gusts - auto const wind{glm::vec3{simulation::Environment.wind()}}; - auto const windspeed{glm::length(glm::vec2{wind.x, wind.z})}; - auto const winddirection{windspeed > 0.01f ? glm::vec2{wind.x, wind.z} / windspeed : glm::vec2{1.f, 0.f}}; - ::glUniform4f(m_uniforms.wind, winddirection.x, winddirection.y, std::clamp(windspeed / 15.f, 0.f, 1.f), static_cast(std::fmod(Timer::GetTime(), 3600.0))); + auto const &frame{this->frame()}; + ::glUniform4fv(m_uniforms.lod, 1, glm::value_ptr(frame.lod)); + ::glUniform4fv(m_uniforms.season, 1, glm::value_ptr(frame.season)); + ::glUniform4fv(m_uniforms.wind, 1, glm::value_ptr(frame.wind)); - std::size_t triangles{0}; - for (auto &entry : m_chunks) + auto const &tiles{tile_draws()}; + for (auto const &chunk : chunk_draws()) { - auto &chunk{entry.second}; - // the kinds of the chunk, and the share of the blades of each against its densest - std::array base{}, tip{}, shape{}, dry{}; - glm::vec4 share{0.f}; - float densest{0.f}; - for (std::size_t i = 0; i < chunk.kinds.size(); ++i) - if (chunk.kinds[i] >= 0 && chunk.kinds[i] < static_cast(m_kinds.size())) - densest = std::max(densest, m_kinds[chunk.kinds[i]].density); - if (densest <= 0.f) - continue; - for (std::size_t i = 0; i < chunk.kinds.size(); ++i) + // the chunk is drawn relative to its corner, the heights as they are + Modelview(glm::translate(View, glm::vec3{chunk.offset})); + ::glUniform4fv(m_uniforms.chunk, 1, glm::value_ptr(chunk.chunk)); + ::glUniform4fv(m_uniforms.camera, 1, glm::value_ptr(chunk.camera)); + ::glUniform4fv(m_uniforms.base, 4, glm::value_ptr(chunk.base[0])); + ::glUniform4fv(m_uniforms.tip, 4, glm::value_ptr(chunk.tip[0])); + ::glUniform4fv(m_uniforms.shape, 4, glm::value_ptr(chunk.shape[0])); + ::glUniform4fv(m_uniforms.share, 1, glm::value_ptr(chunk.share)); + ::glUniform4fv(m_uniforms.dry, 4, glm::value_ptr(chunk.dry[0])); + auto const &textures{m_textures[chunk.id]}; + GLuint const units[]{textures.heights, textures.density, textures.holes}; + ::glBindTextures(0, 3, units); + for (auto i = chunk.first; i < chunk.first + chunk.count; ++i) { - if (chunk.kinds[i] < 0 || chunk.kinds[i] >= static_cast(m_kinds.size())) - continue; - auto const &kind{m_kinds[chunk.kinds[i]]}; - base[i] = glm::vec4{kind.base, 0.f}; - tip[i] = glm::vec4{kind.tip, 0.f}; - shape[i] = glm::vec4{kind.height, kind.variation, kind.width, kind.bend}; - dry[i] = glm::vec4{kind.dry, kind.seasonal}; - share[i] = kind.density / densest; + auto const &tile{tiles[i]}; + ::glUniform4fv(m_uniforms.tile, 1, glm::value_ptr(tile.tile)); + ::glUniform4fv(m_uniforms.part, 1, glm::value_ptr(tile.part)); + ::glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, tile.segments * 2 + 1, tile.count); } - auto const tallest{std::max({shape[0].x, shape[1].x, shape[2].x, shape[3].x}) * 1.5f + blade_reach}; - - auto const tilesize{chunk.size / tiles}; - bool set{false}; - for (int tz = 0; tz < tiles; ++tz) - for (int tx = 0; tx < tiles; ++tx) - { - auto const &part{chunk.parts[static_cast(tz) * tiles + tx]}; - auto const fill{std::min(1.f, glm::dot(glm::vec4{part.most[0], part.most[1], part.most[2], part.most[3]}, share))}; - if (fill <= 0.f) - continue; - // distance to the box of the tile - glm::dvec3 const low{chunk.corner.x + tx * tilesize, part.low, chunk.corner.y + tz * tilesize}; - glm::dvec3 const high{low.x + tilesize, part.high + tallest, low.z + tilesize}; - auto const nearest{glm::clamp(camera, low, high)}; - auto const distance{static_cast(glm::length(nearest - camera))}; - if (distance > reach) - continue; - if (false == Camera.visible(scene::bounding_area{(low + high) * 0.5, static_cast(glm::length(high - low) * 0.5)})) - continue; - auto const full{tilesize * tilesize * densest * multiplier * fill}; - auto const count{std::min(max_blades, static_cast(std::ceil(full * lod_share(distance, thinning, fade, reach))))}; - if (count <= 0) - continue; - if (false == set) - { - // the chunk is drawn relative to its corner, the heights as they are - set = true; - Modelview(glm::translate(View, glm::vec3{glm::dvec3{chunk.corner.x - camera.x, -camera.y, chunk.corner.y - camera.z}})); - ::glUniform4f(m_uniforms.chunk, chunk.size, static_cast(chunk.cells), static_cast(chunk.samples), 0.f); - ::glUniform4f(m_uniforms.camera, static_cast(camera.x - chunk.corner.x), static_cast(camera.y), static_cast(camera.z - chunk.corner.y), 0.f); - ::glUniform4fv(m_uniforms.base, 4, glm::value_ptr(base[0])); - ::glUniform4fv(m_uniforms.tip, 4, glm::value_ptr(tip[0])); - ::glUniform4fv(m_uniforms.shape, 4, glm::value_ptr(shape[0])); - ::glUniform4fv(m_uniforms.share, 1, glm::value_ptr(share)); - ::glUniform4fv(m_uniforms.dry, 4, glm::value_ptr(dry[0])); - GLuint const textures[]{chunk.heights, chunk.density, chunk.holes}; - ::glBindTextures(0, 3, textures); - } - // fewer segments further away - auto const segments{distance < thinning * 1.5f ? 3 : distance < fade * 0.5f ? 2 : 1}; - // the number of the tile in the world, the seed of its blades - auto const seedx{static_cast(std::floor(chunk.corner.x / tilesize + 0.5) + tx)}; - auto const seedz{static_cast(std::floor(chunk.corner.y / tilesize + 0.5) + tz)}; - ::glUniform4f(m_uniforms.tile, tx * tilesize, tz * tilesize, tilesize, std::max(1.f, full)); - ::glUniform4f(m_uniforms.part, fill, static_cast(segments), seedx, seedz); - ::glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, segments * 2 + 1, count); - triangles += static_cast(count) * (segments * 2 - 1); - } } ::glEnable(GL_CULL_FACE); gl::vao::unbind(); // the units of the textures were taken behind the back of the texture cache opengl_texture::reset_unit_cache(); - return triangles; + return triangles(); } diff --git a/rendering/opengl46/opengl46grass.h b/rendering/opengl46/opengl46grass.h index 52370fe9..005eed8a 100644 --- a/rendering/opengl46/opengl46grass.h +++ b/rendering/opengl46/opengl46grass.h @@ -13,69 +13,44 @@ http://mozilla.org/MPL/2.0/. #include #include #include -#include #include -#include "rendering/renderer.h" +#include "rendering/common/grassfield.h" #include "gl/shader.h" #include "gl/vao.h" class opengl_camera; -// grass of the heightmap terrain for the opengl 4.6 renderer: blades made up by the vertex shader (grass.vert) where the -// grass is painted, in the chunks near the camera. -// -// a chunk is cut into tiles; each tile visible within the reach of the grass is drawn as instances of a blade, as many as -// its densest kind and its distance ask for. a blade finds its place, its kind and its shape from its number, so the same -// blades stand at the same places in every frame. further away the blades thin out and grow wider, and fade out at the end -// of the reach. the heights, the densities and the holes of a chunk are kept in textures, made when the chunk comes near -class opengl46_grass +// grass of the heightmap terrain for the opengl 4.6 renderer (gfx::grass_field): the textures of the chunks, and the draws of +// the tiles seen, instances of a blade made up by grass.vert +class opengl46_grass : public gfx::grass_field { public: opengl46_grass() = default; - ~opengl46_grass(); + ~opengl46_grass() override; - // see gfx_renderer::Terrain_Grass() - void update(std::vector const &Kinds, std::vector const &Chunks); // draws the grass seen by the camera. View: the modelview of the pass with the camera at its origin; Modelview: hands a // modelview to the model block of the draws which follow. returns: triangles drawn std::size_t render(opengl_camera const &Camera, glm::mat4 const &View, std::function const &Modelview); - // drops everything made on the gpu, e.g. when the context goes - void clear(); - bool empty() const { return m_chunks.empty(); } - static int constexpr tiles{16}; // along a side of a chunk + protected: + void gpu_upload(std::uint64_t const Id, gfx::grass_chunk const &Source) override; + void gpu_release(std::uint64_t const Id) override; private: - struct tile + struct chunk_textures { - float low{0.f}, high{0.f}; // of the ground - std::array most{}; // the highest density of each kind of the chunk within the tile, 0..1 - }; - struct chunk - { - std::uint64_t version{0}; - glm::dvec2 corner{0.0}; - float size{0.f}; - int cells{0}; - int samples{0}; - std::array kinds{-1, -1, -1, -1}; GLuint heights{0}; // R32F, the grid points GLuint density{0}; // RGBA8, the densities of the kinds GLuint holes{0}; // R8, a single texel of 0 without holes - std::array parts; - bool listed{false}; // in the last list given }; bool init(); - void upload(chunk &Chunk, gfx::grass_chunk const &Source); - void release(chunk &Chunk); int m_state{0}; // 0: not set up yet, 1: ready, -1: can't be used std::unique_ptr m_program; std::unique_ptr m_vao; // without attributes, the blades are made of the numbers of their vertices - std::vector m_kinds; - std::unordered_map m_chunks; + std::unordered_map m_textures; // locations of the uniforms of the program struct locations { diff --git a/rendering/opengl46/opengl46instancecull.cpp b/rendering/opengl46/opengl46instancecull.cpp index c4e312c2..e36790ef 100644 --- a/rendering/opengl46/opengl46instancecull.cpp +++ b/rendering/opengl46/opengl46instancecull.cpp @@ -10,10 +10,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/opengl46/opengl46instancecull.h" -#include "model/AnimModel.h" -#include "model/Model3d.h" #include "model/Texture.h" -#include "rendering/frustum.h" #include "utilities/Globals.h" #include "utilities/utilities.h" @@ -36,6 +33,7 @@ void opengl46_instance_cull::init(opengl46_instance_cull const *Shared) if (Shared != nullptr) { m_shared = Shared->m_shared; + share_records(*Shared); } else { @@ -59,203 +57,11 @@ void opengl46_instance_cull::init(opengl46_instance_cull const *Shared) m_output = std::make_unique(65536 * sizeof(glm::mat4), gl::INSTANCE_OUTPUT_SSBO_BINDING); m_commands = std::make_unique(4096 * sizeof(gl::instance_draw_command), gl::INSTANCE_COMMAND_SSBO_BINDING); m_readback = std::make_unique(readbacksize); - m_readbackdata.resize(gl::INSTANCE_COUNTER_HEADER + READBACK_ENTRIES); -} - -void opengl46_instance_cull::begin_pass(pass_setup const &Setup) -{ - collect(); - - m_inpass = true; - m_ran = true; - m_dispatched = false; - m_setup = Setup; - m_buckets.clear(); - m_joblist.clear(); - m_grouplist.clear(); - m_drawgroups.clear(); - m_lodlist.clear(); - m_groupmap.clear(); - m_sleepergroupmap.clear(); - m_cpucounts.clear(); - m_candidates = 0; - m_entries = 0; - m_sleepertracks = 0; - m_sleepers = 0; - m_models = 0; - m_counts = {}; -} - -void opengl46_instance_cull::add_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area) -{ - add(Key, Bucket, Area, false); -} - -void opengl46_instance_cull::skip_bucket(scene::basic_cell::instance_bucket const &Bucket) -{ - if (!m_inpass) - return; - ++m_counts.skippedbuckets; - m_counts.skippedinstances += Bucket.records.size(); -} - -void opengl46_instance_cull::add_sleepers(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area) -{ - add(Key, Bucket, Area, true); -} - -void opengl46_instance_cull::add(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area, bool const Sleepers) -{ - if (!m_inpass || Bucket.records.empty() || Key.pModel == nullptr) - return; - - // range of the squared distances the instances of the bucket can be at, scaled as in the culling, with some slack for - // its single precision: the draws are made only for the lod bands within it. sleepers go by the plain distance from the - // camera of the pass - auto const &distancecamera{Sleepers ? m_setup.camera : m_setup.distancecamera}; - auto const distancescale{Sleepers ? 1.0 : static_cast(m_setup.distancescale)}; - auto const distanceoffset{Sleepers ? 0.0 : static_cast(m_setup.distanceoffset)}; - auto const centerdistance{glm::length(Area.center - distancecamera)}; - auto const nearest{std::max(0.0, centerdistance - Area.radius)}; - auto const farthest{centerdistance + Area.radius}; - auto const distancemin{static_cast(0.999 * (nearest * nearest * distancescale + distanceoffset))}; - auto const distancemax{static_cast(1.001 * (farthest * farthest * distancescale + distanceoffset))}; - - auto &groupmap{Sleepers ? m_sleepergroupmap : m_groupmap}; - auto lookup{groupmap.find(Key)}; - if (lookup == groupmap.end()) - { - // lod bounds of the model, the same set Render_Instanced() splits the distance into bands with, found once for the model - auto const lodfirst{static_cast(m_lodlist.size())}; - auto const &lodbounds{Key.pModel->lod_bounds()}; - m_lodlist.insert(m_lodlist.end(), lodbounds.begin(), lodbounds.end()); - auto const lodcount{static_cast(lodbounds.size())}; - - group_data const group{static_cast(m_grouplist.size()), m_entries, lodcount + 1, distancemin, distancemax}; - m_grouplist.push_back({lodfirst, lodcount, m_entries, 0}); - m_entries += group.entrycount; - lookup = groupmap.emplace(Key, group).first; - if (m_setup.drawing) - { - // any instance of the group serves; it's only prepared and asked for its material - TAnimModel *instance{nullptr}; - for (auto *candidate : Bucket.instances) - { - if (candidate != nullptr) - { - instance = candidate; - break; - } - } - m_drawgroups.push_back({&lookup->first, instance, Sleepers, lodfirst, lodcount, group.entryfirst, distancemin, distancemax}); - } - } - else - { - lookup->second.distancemin = std::min(lookup->second.distancemin, distancemin); - lookup->second.distancemax = std::max(lookup->second.distancemax, distancemax); - if (m_setup.drawing) - { - auto &drawgroup{m_drawgroups[lookup->second.index]}; - drawgroup.distancemin = lookup->second.distancemin; - drawgroup.distancemax = lookup->second.distancemax; - if (drawgroup.instance == nullptr) - { - for (auto *candidate : Bucket.instances) - { - if (candidate != nullptr) - { - drawgroup.instance = candidate; - break; - } - } - } - } - } - - place(Bucket); - - gl::instance_cull_job job{}; - job.origin = packed_vec3{Bucket.recordorigin - m_setup.camera}; - job.first = Bucket.gpufirst; - job.distanceorigin = packed_vec3{Bucket.recordorigin - m_setup.distancecamera}; - job.count = static_cast(Bucket.records.size()); - job.group = lookup->second.index; - job.candidatestart = m_candidates; - job.flags = (Sleepers ? gl::INSTANCE_JOB_SLEEPERS : 0u); - m_joblist.push_back(job); - m_buckets.push_back(&Bucket); - m_candidates += job.count; - if (Sleepers) - { - ++m_sleepertracks; - m_sleepers += job.count; - } - else if (Key.instance != nullptr) - { - ++m_models; - } -} - -void opengl46_instance_cull::place(scene::basic_cell::instance_bucket const &Bucket) -{ - auto &shared{*m_shared}; - auto const count{static_cast(Bucket.records.size())}; - auto const current{Bucket.gpugeneration == shared.recordgeneration}; - if (current && !Bucket.gpustale) - return; - if (current && count <= Bucket.gpucapacity) - { - shared.records->update(Bucket.records.data(), static_cast(Bucket.gpufirst) * RECORD_SIZE, static_cast(count) * RECORD_SIZE); - m_counts.sentrecords += count; - Bucket.gpustale = false; - return; - } - - // a new place, with some room for instances the editor adds - auto const allocate = [this, &shared](scene::basic_cell::instance_bucket const &Target) { - auto const records{static_cast(Target.records.size())}; - Target.gpufirst = shared.recordfill; - Target.gpucapacity = records + records / 4; - Target.gpugeneration = shared.recordgeneration; - Target.gpustale = false; - shared.recordfill += Target.gpucapacity; - shared.records->update(Target.records.data(), static_cast(Target.gpufirst) * RECORD_SIZE, static_cast(records) * RECORD_SIZE); - m_counts.sentrecords += records; - }; - - auto const capacity{static_cast(shared.records->size() / RECORD_SIZE)}; - if (shared.recordfill + count + count / 4 > capacity) - { - // the buffer is started over, and only the buckets of this pass get places again. the places left behind by - // buckets which were made anew or are out of sight are dropped this way. the passes of a frame queue different - // buckets: when those of all of them don't fit in the buffer together, some pass would start it over in every frame, - // so a buffer started over again in the same or the next frame grows - GLsizeiptr live{count + count / 4}; - for (auto const *bucket : m_buckets) - live += bucket->records.size() + bucket->records.size() / 4; - auto const again{shared.restarted && (m_setup.frame - shared.restartframe <= 1)}; - if ((2 * live * RECORD_SIZE > shared.records->size()) || again) - shared.records->resize(std::max(2 * live * RECORD_SIZE, 2 * shared.records->size())); - shared.restarted = true; - shared.restartframe = m_setup.frame; - m_counts.restarted = true; - ++shared.recordgeneration; - shared.recordfill = 0; - for (std::size_t job = 0; job < m_buckets.size(); ++job) - { - allocate(*m_buckets[job]); - m_joblist[job].first = m_buckets[job]->gpufirst; - } - } - allocate(Bucket); } void opengl46_instance_cull::dispatch(glm::mat4 const &View, occlusion_setup const *Occlusion, bool const Twophase) { - m_occlusion = false; - m_twophase = false; - if (!m_inpass || m_joblist.empty()) + if (!dispatch_begin()) return; auto const countersize{static_cast((gl::INSTANCE_COUNTER_HEADER + 3 * m_entries) * sizeof(std::uint32_t))}; @@ -277,14 +83,8 @@ void opengl46_instance_cull::dispatch(glm::mat4 const &View, occlusion_setup con bind_buffers(); // planes moved to the camera of the pass, in double precision: the cpu tests world coordinates - auto const &frustumplanes{m_setup.frustum->planes()}; glm::vec4 planes[6]; - for (int plane = 0; plane < 6; ++plane) - { - auto const *source{frustumplanes[plane]}; - planes[plane] = glm::vec4{source[0], source[1], source[2], - static_cast(source[0] * m_setup.camera.x + source[1] * m_setup.camera.y + source[2] * m_setup.camera.z + source[3])}; - } + frustum_planes(planes); glProgramUniformMatrix4fv(*m_shared->cull_program, 0, 1, GL_FALSE, glm::value_ptr(View)); glProgramUniform4fv(*m_shared->cull_program, 1, 6, glm::value_ptr(planes[0])); glProgramUniform4f(*m_shared->cull_program, 7, m_setup.distancescale, m_setup.distanceoffset, m_setup.distancerange, m_setup.drawrange); @@ -328,8 +128,7 @@ void opengl46_instance_cull::dispatch(glm::mat4 const &View, occlusion_setup con opengl_texture::reset_unit_cache(); } - m_cpucounts.assign(m_entries, 0); - m_dispatched = true; + dispatch_end(); } void opengl46_instance_cull::bind_buffers() @@ -401,144 +200,29 @@ void opengl46_instance_cull::bind_draw() glBindBuffersBase(GL_SHADER_STORAGE_BUFFER, gl::INSTANCE_SSBO_BINDING, 1, &output); } -void opengl46_instance_cull::draw_result(std::uint32_t const Draws, std::uint32_t const Prepassdraws, std::uint32_t const Calls, std::uint32_t const Prepasscalls, double const Cputime) +std::int64_t opengl46_instance_cull::record_buffer_size() const { - m_draws = Draws; - m_prepassdraws = Prepassdraws; - m_calls = Calls; - m_prepasscalls = Prepasscalls; - m_cputime = Cputime; + return m_shared->records->size(); } -void opengl46_instance_cull::cpu_result(scene::basic_cell::instance_bucket_key const &Key, std::vector const &Bandcounts, bool const Sleepers) +void opengl46_instance_cull::resize_record_buffer(std::int64_t const Size) { - if (!m_dispatched) - return; - auto const &groupmap{Sleepers ? m_sleepergroupmap : m_groupmap}; - auto const lookup{groupmap.find(Key)}; - if (lookup == groupmap.end()) - return; - auto const &group{lookup->second}; - for (std::size_t band = 0; band < std::min(Bandcounts.size(), group.entrycount); ++band) - m_cpucounts[group.entryfirst + band] += Bandcounts[band]; + m_shared->records->resize(static_cast(Size)); } -void opengl46_instance_cull::cpu_instance(scene::basic_cell::instance_bucket_key const &Key, float const Distancesquared) +void opengl46_instance_cull::send_records(std::uint32_t const First, scene::basic_cell::instance_record const *Records, std::uint32_t const Count) { - if (!m_dispatched) - return; - auto const lookup{m_groupmap.find(Key)}; - if (lookup == m_groupmap.end()) - return; - auto const &group{lookup->second}; - auto const &lods{m_grouplist[group.index]}; - // the band the culling gives the distance: the number of bounds it reached - auto const first{m_lodlist.begin() + lods.lodfirst}; - auto const band{static_cast(std::upper_bound(first, first + lods.lodcount, Distancesquared) - first)}; - if (band < group.entrycount) - ++m_cpucounts[group.entryfirst + band]; + m_shared->records->update(Records, static_cast(First) * RECORD_SIZE, static_cast(Count) * RECORD_SIZE); } -void opengl46_instance_cull::end_pass() +void opengl46_instance_cull::push_counts() { - // the counts of both phases, for the panel and the comparison - if (m_dispatched) - m_readback->push(*m_counters); - // the cpu counts are there to compare with only when the cpu drew - if (m_dispatched && !m_setup.drawing) - { - std::uint64_t total{0}; - for (auto const count : m_cpucounts) - total += count; - m_cpupasses.push_back({m_setup.frame, m_cpucounts, total}); - // the gpu is never this far behind - while (m_cpupasses.size() > 8) - m_cpupasses.pop_front(); - } - m_lastsleepertracks = m_sleepertracks; - m_lastsleepers = m_sleepers; - m_lastmodels = m_models; - m_counts.jobs = static_cast(m_joblist.size()); - m_counts.candidates = m_candidates; - m_lastcounts = m_counts; - m_inpass = false; - m_dispatched = false; + m_readback->push(*m_counters); } -void opengl46_instance_cull::collect() +bool opengl46_instance_cull::pop_counts(std::uint32_t *Counts) { - if (!m_readback || !m_readback->pop(m_readbackdata.data())) - return; - - auto const frame{m_readbackdata[0]}; - comparison result; - result.frame = frame; - result.occluded = m_readbackdata[3]; - result.gputotal = m_readbackdata[1] - std::min(m_readbackdata[1], result.occluded); - result.entries = m_readbackdata[2]; - result.first = m_readbackdata[4]; - while (!m_cpupasses.empty() && m_cpupasses.front().frame != frame) - m_cpupasses.pop_front(); - if (m_cpupasses.empty()) - { - // drawn from the gpu: nothing to compare with - m_last = result; - return; - } - - auto const &cpu{m_cpupasses.front()}; - result.cputotal = cpu.total; - result.compared = std::min({result.entries, READBACK_ENTRIES, static_cast(cpu.counts.size())}); - for (std::uint32_t entry = 0; entry < result.compared; ++entry) - { - auto const gpu{m_readbackdata[gl::INSTANCE_COUNTER_HEADER + entry]}; - auto const difference{gpu > cpu.counts[entry] ? gpu - cpu.counts[entry] : cpu.counts[entry] - gpu}; - if (difference > 0) - { - ++result.mismatched; - result.largestdifference = std::max(result.largestdifference, difference); - } - } - m_cpupasses.pop_front(); - - m_last = result; - ++m_comparedframes; - if ((result.mismatched > 0) || (result.gputotal != result.cputotal) || (result.entries != cpu.counts.size())) - ++m_mismatchedframes; -} - -std::string opengl46_instance_cull::info(std::string const &Label, bool const Occlusion) -{ - if (Global.gfx_gpucull == 0) - return {}; - // the sleepers are among the instances counted - auto sleepers{m_lastsleepertracks > 0 ? "\n" + Label + ": " + std::to_string(m_lastsleepers) + " sleepers of " + std::to_string(m_lastsleepertracks) + " tracks queued" : std::string{}}; - // and so are the models drawn on their own - if (m_lastmodels > 0) - sleepers += "\n" + Label + ": " + std::to_string(m_lastmodels) + " models drawn on their own queued"; - // what the pass tested and sent to the gpu - sleepers += "\n" + Label + ": " + std::to_string(m_lastcounts.candidates) + " instances tested in " + std::to_string(m_lastcounts.jobs) + " buckets"; - if (Global.gfx_gpucull_bucketrange) - sleepers += ", " + std::to_string(m_lastcounts.skippedbuckets) + " buckets (" + std::to_string(m_lastcounts.skippedinstances) + " instances) left out before the culling"; - sleepers += ", " + std::to_string(m_lastcounts.sentrecords) + " records sent" + (m_lastcounts.restarted ? ", record buffer started over" : ""); - if (Global.gfx_gpucull == 1) - { - if (m_last.frame == 0) - return "\n" + Label + ": no result yet" + sleepers; - if (false == Occlusion) - return "\n" + Label + ": " + std::to_string(m_last.gputotal) + " instances drawn, " + std::to_string(m_last.entries) + " bands; " + std::to_string(m_draws) + - " indirect draws in " + std::to_string(m_calls) + " calls, cpu " + to_string(m_cputime, 3) + " ms" + sleepers; - return "\n" + Label + ": " + std::to_string(m_last.gputotal) + " instances drawn" + - (m_twophase ? " (" + std::to_string(m_last.first) + " of them first, seen in the last frame)" : std::string{}) + ", " + std::to_string(m_last.occluded) + - " hidden by occlusion" + (m_occlusion ? (m_twophase ? " (also by one another)" : "") : " (test off)") + ", " + std::to_string(m_last.entries) + " bands; " + - std::to_string(m_draws) + " indirect draws in " + std::to_string(m_calls) + " calls (+" + std::to_string(m_prepassdraws) + " depth prepass in " + - std::to_string(m_prepasscalls) + "), cpu " + to_string(m_cputime, 3) + " ms" + sleepers; - } - if (m_comparedframes == 0) - return "\n" + Label + ": no result yet" + sleepers; - return "\n" + Label + ": " + std::to_string(m_last.gputotal) + " instances (cpu " + std::to_string(m_last.cputotal) + "), " + std::to_string(m_last.entries) + " bands, " + - std::to_string(m_last.mismatched) + " differ (by up to " + std::to_string(m_last.largestdifference) + "); frames differing " + std::to_string(m_mismatchedframes) + " of " + - std::to_string(m_comparedframes) + sleepers; + return (m_readback && m_readback->pop(Counts)); } std::string opengl46_instance_cull::info_times(std::string const &Label) diff --git a/rendering/opengl46/opengl46instancecull.h b/rendering/opengl46/opengl46instancecull.h index 533595af..7812eac9 100644 --- a/rendering/opengl46/opengl46instancecull.h +++ b/rendering/opengl46/opengl46instancecull.h @@ -21,53 +21,15 @@ http://mozilla.org/MPL/2.0/. #include "gl/query.h" #include "gl/ubo.h" #include "scene/scene.h" +#include "rendering/common/instancecull.h" -class cFrustum; -class TAnimModel; - -// culling of the model instances of a render pass in compute passes (instance_cull.comp, instance_cull_offsets.comp), -// the gpu side of opengl46_renderer::Queue_Instances() and Render_Instanced(): -// - the records of the instances (scene::basic_cell::instance_record) are kept in one buffer, each bucket of a scene cell -// in its own place, and sent again only when the bucket made them anew -// - every bucket queued in the pass becomes a job; the buckets of one model and one set of skins make a group, whose -// lod bands are its entries -// - the passes count the instances which pass per entry, turn the counts into places in the output and write the -// modelview matrices of the instances there -// with Global.gfx_gpucull 1 the instances are drawn from the result: the renderer turns each lod band of a group into a -// list of indirect draw commands (one per submodel drawn in the band), whose instances a fourth pass fills in from the -// counts (instance_cull_commands.comp), and draws them with the output bound as the instance buffer -// (opengl46_renderer::Render_Instances_Indirect()). an occlusion test against a depth pyramid of what the pass drew before -// (opengl46_hiz) can leave out more instances. in two phases the instances seen in the previous frame are culled and drawn -// first, then the pyramid is made again with them in it (dispatch_second()) and the others are tested against it, so the -// instances hide one another too. the command buffer holds the commands three times over: the instances drawn first, all -// of them, and the ones drawn second, each copy command_count() commands long (draw_command_offset()) -// with Global.gfx_gpucull 2 the cpu still draws, and the result is only compared with its own: the counts per entry are -// read back a few frames later and set against the counts Render_Instanced() reported for the same frame -// each render pass culled on the gpu (the main view, the cascades of the sun shadows, the faces of the environment map, the -// shadow maps of headlights and the scenery parts of those of lamps) has a culling of its own, with its own buffers, while the -// records and the programs are shared by all of them: the records of a bucket go to the gpu once and serve every pass which -// queues the bucket -// the sleepers of tracks (TTrack::m_sleeper_local_transforms) are culled the same way, each track with them a bucket of its -// own, and drawn together with the instances; their groups are kept apart from those of the model instances -// so are the model instances drawn on their own (not instanceable): each one a bucket and a group of its own (the instance in -// its key), whose submodel tree the renderer animates and walks for it alone -class opengl46_instance_cull +// culling of the model instances of a render pass in compute passes (instance_cull.comp, instance_cull_offsets.comp, +// instance_cull_commands.comp): the buffers and the passes of gfx::instance_cull, which tells what they do. with +// Global.gfx_gpucull 1 the renderer draws the result with opengl46_renderer::Render_Instances_Indirect(); the occlusion test +// takes the depth pyramid of opengl46_hiz +class opengl46_instance_cull : public gfx::instance_cull { public: - // what a pass needs for the tests, the way Render_Instanced() takes it - struct pass_setup - { - unsigned int frame{0}; - glm::dvec3 camera{0.0}; // camera of the pass: frustum and placement - glm::dvec3 distancecamera{0.0}; // the distance is measured from it - cFrustum const *frustum{nullptr}; // of the pass camera, in world coordinates - float distancescale{1.f}; // of the squared distance - float distanceoffset{0.f}; // added to the scaled squared distance past the range test - float distancerange{0.f}; // squared range of the scaled distance before the offset - float drawrange{0.f}; // squared draw range - float zoom{1.f}; - bool drawing{false}; // the instances are drawn from the result (Global.gfx_gpucull 1), not compared - }; // occlusion test of a pass struct occlusion_setup { @@ -77,47 +39,11 @@ class opengl46_instance_cull glm::vec2 screen{0.f}; // size of the depth it was made from, pixels glm::mat4 projection{1.f}; // of the pass }; - // a group of instances (model and skins) of the pass, for the renderer to make the draw commands of - struct draw_group - { - scene::basic_cell::instance_bucket_key const *key; - TAnimModel *instance; // one of the group, prepares the model and gives the material. none for sleepers - bool sleepers; // the skins come from the key, as Render_Sleepers() sets them - std::uint32_t lodfirst; // in lod_bounds() - std::uint32_t lodcount; - std::uint32_t entryfirst; - float distancemin; // squared distances (scaled as the culling does) the instances of the group can be at - float distancemax; - }; opengl46_instance_cull() = default; - opengl46_instance_cull(const opengl46_instance_cull &) = delete; - opengl46_instance_cull &operator=(const opengl46_instance_cull &) = delete; // creates the buffers, and the programs and the record buffer, or takes the ones of Shared. throws gl::shader_exception void init(opengl46_instance_cull const *Shared = nullptr); - // starts collecting the buckets of a pass - void begin_pass(pass_setup const &Setup); - bool in_pass() const - { - return m_inpass; - } - bool drawing() const - { - return m_inpass && m_setup.drawing; - } - // adds the records of a bucket (made already) to the pass, sending them to the gpu when they changed. - // Area: bounds which hold the locations of all its visible instances: their own (opengl46_renderer::Bucket_Area()) or of the cell - // holding the bucket - void add_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area); - // adds the sleepers of a track, their records made already as a bucket with no instances (only the records and the place - // in the record buffer count). Area: bounds of the locations of the sleepers. the range of the whole track is tested by - // the caller, the culling tests each sleeper against the frustum and gives it the lod band of its plain distance from the - // camera of the pass (Render_Sleepers()) - void add_sleepers(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area); - // a bucket the renderer left out of the pass, as none of its instances can pass the culling (Global.gfx_gpucull_bucketrange): only counted, - // for the panel - void skip_bucket(scene::basic_cell::instance_bucket const &Bucket); // runs the culling passes for the buckets added. View: rotation of the view of the pass. Occlusion: the depth pyramid // to test the instances against, or null. Twophase: with the occlusion test, only the instances seen in the previous // frame pass for now, dispatch_second() takes the others @@ -126,108 +52,22 @@ class opengl46_instance_cull // the others are tested against it, then the commands of all instances and of the ones of this phase are filled in. // needs fill_commands() before it void dispatch_second(occlusion_setup const &Occlusion); - // the last dispatch() works in two phases - bool two_phase() const - { - return m_twophase; - } - // groups of the pass and the lod bounds they refer to, valid from dispatch() to end_pass() - std::vector const &draw_groups() const - { - return m_drawgroups; - } - std::vector const &lod_bounds() const - { - return m_lodlist; - } // sends the indirect draw commands made for the entries of the pass and fills in their instances (of the instances drawn // first, in two phases). after this the commands are in the buffer bound by bind_draw(), the modelview matrices of the // instances in the output void fill_commands(std::vector const &Commands); - // copies of the commands in the command buffer - enum class command_set : std::uint32_t - { - first = 0, // instances which passed dispatch(): in one phase all of them - all = 1, // after dispatch_second() - second = 2 // the ones which passed dispatch_second() - }; - // offset of command Index of a copy in the command buffer - std::intptr_t draw_command_offset(command_set const Set, std::size_t const Index) const - { - return static_cast((static_cast(Set) * m_commandcount + Index) * sizeof(gl::instance_draw_command)); - } // binds the commands to GL_DRAW_INDIRECT_BUFFER and the output in place of the instance buffer of the frame void bind_draw(); - // counts of the last result which came back from the gpu, with the draws of the pass and the gl calls which made them (fewer - // with multi draws), for the debug panel - void draw_result(std::uint32_t const Draws, std::uint32_t const Prepassdraws, std::uint32_t const Calls, std::uint32_t const Prepasscalls, double const Cputime); - // what draw_result() was given last - std::uint32_t prepass_draws() const - { - return m_prepassdraws; - } - std::uint32_t prepass_calls() const - { - return m_prepasscalls; - } - double cpu_time() const - { - return m_cputime; - } - // instances which passed in the last result which came back from the gpu, a few frames old - std::uint32_t last_count() const - { - return static_cast(m_last.gputotal); - } - // counts per lod band of the instances Render_Instanced() drew for a model and its skins in this pass, or of the sleepers - // Render_Sleepers() drew. needs dispatch() before it - void cpu_result(scene::basic_cell::instance_bucket_key const &Key, std::vector const &Bandcounts, bool const Sleepers = false); - // the cpu drew the instance of a group of its own (Key with the instance) at a squared distance scaled as the culling - // scales it: counted in the lod band the distance falls in. needs dispatch() before it - void cpu_instance(scene::basic_cell::instance_bucket_key const &Key, float const Distancesquared); - // ends the pass: its cpu counts are kept for the comparison - void end_pass(); - // takes the newest counts of the gpu which arrived and compares them with the ones of the cpu. done when a pass begins; - // for the panel also by the culls of passes which aren't made every frame - void collect(); - // a pass was culled since the start - bool ran() const - { - return m_ran; - } - // lines for the debug panel, Label naming the pass. Occlusion: the pass may test occlusion (the main view) - std::string info(std::string const &Label = "gpu cull", bool const Occlusion = true); std::string info_times(std::string const &Label = "instance cull"); - private: - struct group_data - { - std::uint32_t index; - std::uint32_t entryfirst; - std::uint32_t entrycount; - float distancemin; - float distancemax; - }; - // counts of the cpu of a pass, waiting for those of the gpu - struct cpu_pass - { - unsigned int frame; - std::vector counts; - std::uint64_t total; - }; - struct comparison - { - unsigned int frame{0}; - std::uint64_t gputotal{0}; - std::uint64_t cputotal{0}; - std::uint32_t entries{0}; - std::uint32_t compared{0}; // entries compared, at most READBACK_ENTRIES - std::uint32_t mismatched{0}; - std::uint32_t largestdifference{0}; - std::uint32_t occluded{0}; // left out by the occlusion test - std::uint32_t first{0}; // drawn first, seen in the previous frame (two phases) - }; + protected: + std::int64_t record_buffer_size() const override; + void resize_record_buffer(std::int64_t const Size) override; + void send_records(std::uint32_t const First, scene::basic_cell::instance_record const *Records, std::uint32_t const Count) override; + void push_counts() override; + bool pop_counts(std::uint32_t *Counts) override; + private: // what the culls of all passes share struct shared_data { @@ -235,26 +75,11 @@ class opengl46_instance_cull std::unique_ptr offsets_program; std::unique_ptr commands_program; std::unique_ptr records; - // record buffer: records placed so far, the generation which tells current places from the ones of a replaced buffer - std::uint32_t recordfill{0}; - unsigned int recordgeneration{1}; - // frame of the last time the buffer was started over, and whether it was - unsigned int restartframe{0}; - bool restarted{false}; }; - using group_map = std::unordered_map; - - // add_bucket() and add_sleepers() - void add(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, scene::bounding_area const &Area, bool const Sleepers); - // finds a place for the records of a bucket in the record buffer and sends them - void place(scene::basic_cell::instance_bucket const &Bucket); // binds the buffers of the culling passes void bind_buffers(); - // entries read back for the comparison; with more of them only the totals and these are compared - static constexpr std::uint32_t READBACK_ENTRIES{16384}; - std::shared_ptr m_shared; std::unique_ptr m_jobs; std::unique_ptr m_groups; @@ -265,55 +90,4 @@ class opengl46_instance_cull std::unique_ptr m_readback; gl::gpu_timer m_timer; gl::gpu_timer m_secondtimer; - - bool m_inpass{false}; - bool m_ran{false}; - pass_setup m_setup; - std::vector m_buckets; // of the jobs, in their order - std::vector m_joblist; - std::vector m_grouplist; - std::vector m_drawgroups; - std::vector m_lodlist; - group_map m_groupmap; - group_map m_sleepergroupmap; // groups of the sleepers - std::uint32_t m_candidates{0}; - std::uint32_t m_entries{0}; - // tracks with sleepers and their sleepers queued in the pass, and in the last pass, for the panel - std::uint32_t m_sleepertracks{0}; - std::uint32_t m_sleepers{0}; - std::uint32_t m_lastsleepertracks{0}; - std::uint32_t m_lastsleepers{0}; - // model instances drawn on their own queued in the pass, and in the last pass, for the panel - std::uint32_t m_models{0}; - std::uint32_t m_lastmodels{0}; - // what the pass held and sent, and the same of the last pass, for the panel: jobs (buckets queued), buckets left out beyond - // their range and their instances, records sent to the gpu, the record buffer started over - struct pass_counts - { - std::uint32_t jobs{0}; - std::uint32_t candidates{0}; - std::uint32_t skippedbuckets{0}; - std::uint64_t skippedinstances{0}; - std::uint64_t sentrecords{0}; - bool restarted{false}; - }; - pass_counts m_counts; - pass_counts m_lastcounts; - bool m_dispatched{false}; - - std::vector m_cpucounts; // of the current pass, by entry - std::deque m_cpupasses; - std::vector m_readbackdata; - comparison m_last; - std::uint64_t m_comparedframes{0}; - std::uint64_t m_mismatchedframes{0}; - // draws of the last pass drawn from the gpu - std::uint32_t m_draws{0}; - std::uint32_t m_prepassdraws{0}; - std::uint32_t m_calls{0}; - std::uint32_t m_prepasscalls{0}; - std::uint32_t m_commandcount{0}; - double m_cputime{0.0}; - bool m_occlusion{false}; // the last dispatch tested occlusion - bool m_twophase{false}; // in two phases }; diff --git a/rendering/opengl46/opengl46localshadows.cpp b/rendering/opengl46/opengl46localshadows.cpp index 502d6ada..81b00049 100644 --- a/rendering/opengl46/opengl46localshadows.cpp +++ b/rendering/opengl46/opengl46localshadows.cpp @@ -19,17 +19,13 @@ http://mozilla.org/MPL/2.0/. namespace { -// near plane of the maps, metres. surfaces closer to the lamp (its own fitting) cast no shadow -float const SHADOW_NEAR{0.15f}; +// near plane of the maps (gfx::local_shadows) +float const SHADOW_NEAR{gfx::local_shadows::shadow_near}; // uniform locations of shadowcube.geom GLint const NEAR_LOCATION{0}; GLint const LAYER_LOCATION{1}; // layer of the map of the cab lamp in the array -int const CAB_SLOT{0}; -// a lamp which moved farther than this (an animated submodel) has its scenery drawn anew, metres -double const LAMP_MOVED{0.01}; -// lamps with a shorter reach aren't worth a map, metres -float const LAMP_RANGE_MIN{1.f}; +int const CAB_SLOT{gfx::local_shadows::cab_slot}; // cube map array of depth maps compared to the reference, one level, Cubes cubes of Size x Size faces GLuint make_cube_array(int const Size, int const Cubes) @@ -99,9 +95,9 @@ void setup_drawing(GLuint const Framebuffer, int const Size) opengl46_local_shadows::~opengl46_local_shadows() { - release(); - release_lamps(); - release_headlights(); + gpu_release(); + gpu_release_lamps(); + gpu_release_headlights(); if (m_empty != 0) glDeleteTextures(1, &m_empty); if (m_emptyheadlights != 0) @@ -167,40 +163,14 @@ void opengl46_local_shadows::set_layer(int const Layer) glProgramUniform1i(*m_alpha_multidraw_program, LAYER_LOCATION, Layer); } -opengl46_local_shadows::level_setup opengl46_local_shadows::setup(int const Level) +bool opengl46_local_shadows::gpu_allocate(int const Size) { - switch (Level) - { - case 1: - return {512, 4, 0.f, 1, 1024, 2, 8}; - case 2: - return {1024, 2, 1.f, 1, 1024, 1, 4}; - case 3: - return {1024, 1, 2.f, 2, 2048, 1, 2}; - default: - return {0, 0, 0.f, 0, 0, 0, 0}; - } -} - -void opengl46_local_shadows::allocate(int const Size) -{ - release(); - if (Size <= 0) - return; m_texture = make_cube_array(Size, CAB_SLOT + 1); m_framebuffer = make_framebuffer(m_texture); - if (m_framebuffer == 0) - { - ErrorLog("lamp shadow map framebuffer setup failed, the lamps have no shadows"); - release(); - Global.gfx_lights_shadows = 0; - return; - } - m_size = Size; - WriteLog("lamp shadow maps: " + std::to_string(Size) + " x " + std::to_string(Size) + " cube"); + return (m_framebuffer != 0); } -void opengl46_local_shadows::release() +void opengl46_local_shadows::gpu_release() { if (m_framebuffer != 0) glDeleteFramebuffers(1, &m_framebuffer); @@ -208,32 +178,17 @@ void opengl46_local_shadows::release() glDeleteTextures(1, &m_texture); m_framebuffer = 0; m_texture = 0; - m_size = 0; - m_valid = false; } -void opengl46_local_shadows::allocate_lamps(int const Count, int const Size) +bool opengl46_local_shadows::gpu_allocate_lamps(int const Count, int const Size) { - release_lamps(); - if ((Count <= 0) || (Size <= 0)) - return; // the scenery parts and the working copies m_lampstexture = make_cube_array(Size, 2 * Count); m_lampsframebuffer = make_framebuffer(m_lampstexture); - if (m_lampsframebuffer == 0) - { - ErrorLog("scenery lamp shadow map framebuffer setup failed, the scenery lamps have no shadows"); - release_lamps(); - Global.gfx_lights_shadows_lamps = 0; - return; - } - m_lampssize = Size; - m_slots.assign(static_cast(Count), lamp_slot{}); - WriteLog("scenery lamp shadow maps: " + std::to_string(Count) + " x " + std::to_string(Size) + " x " + std::to_string(Size) + " cube (" + - std::to_string(2 * Count * 6 * Size / 1024 * Size * 4 / 1024) + " MB)"); + return (m_lampsframebuffer != 0); } -void opengl46_local_shadows::release_lamps() +void opengl46_local_shadows::gpu_release_lamps() { if (m_lampsframebuffer != 0) glDeleteFramebuffers(1, &m_lampsframebuffer); @@ -241,35 +196,19 @@ void opengl46_local_shadows::release_lamps() glDeleteTextures(1, &m_lampstexture); m_lampsframebuffer = 0; m_lampstexture = 0; - m_lampssize = 0; - m_slots.clear(); - m_wanted.clear(); } -void opengl46_local_shadows::allocate_headlights(int const Count, int const Size) +bool opengl46_local_shadows::gpu_allocate_headlights(int const Count, int const Size) { - release_headlights(); - if ((Count <= 0) || (Size <= 0)) - return; m_headlightstexture = make_array(Size, Count); glCreateFramebuffers(1, &m_headlightsframebuffer); glNamedFramebufferTextureLayer(m_headlightsframebuffer, GL_DEPTH_ATTACHMENT, m_headlightstexture, 0, 0); glNamedFramebufferDrawBuffer(m_headlightsframebuffer, GL_NONE); glNamedFramebufferReadBuffer(m_headlightsframebuffer, GL_NONE); - if (glCheckNamedFramebufferStatus(m_headlightsframebuffer, GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) - { - ErrorLog("headlight shadow map framebuffer setup failed, the headlights have no shadows"); - release_headlights(); - Global.gfx_lights_shadows_headlights = 0; - return; - } - m_headlightssize = Size; - m_headlightslots.assign(static_cast(Count), headlight_slot{}); - WriteLog("headlight shadow maps: " + std::to_string(Count) + " x " + std::to_string(Size) + " x " + std::to_string(Size) + " (" + - std::to_string(Count * Size / 1024 * Size * 4 / 1024) + " MB)"); + return (glCheckNamedFramebufferStatus(m_headlightsframebuffer, GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE); } -void opengl46_local_shadows::release_headlights() +void opengl46_local_shadows::gpu_release_headlights() { if (m_headlightsframebuffer != 0) glDeleteFramebuffers(1, &m_headlightsframebuffer); @@ -277,9 +216,6 @@ void opengl46_local_shadows::release_headlights() glDeleteTextures(1, &m_headlightstexture); m_headlightsframebuffer = 0; m_headlightstexture = 0; - m_headlightssize = 0; - m_headlightslots.clear(); - m_headlightswanted.clear(); } void opengl46_local_shadows::bind() @@ -289,42 +225,8 @@ void opengl46_local_shadows::bind() glBindTextureUnit(gl::HEADLIGHT_SHADOW_TEX, (m_headlightstexture != 0) ? m_headlightstexture : m_emptyheadlights); } -bool opengl46_local_shadows::update_wanted(TDynamicObject const *Vehicle, TModel3d const *Cab, glm::vec3 const &Lamp, unsigned int const Framestamp) +void opengl46_local_shadows::gpu_begin_cab() { - auto const level{std::clamp(Global.gfx_lights_shadows, 0, 3)}; - if (level != m_level) - { - m_level = level; - auto const size{setup(level).size}; - if (size != m_size) - allocate(size); - m_valid = false; - } - if ((m_size == 0) || (Vehicle == nullptr) || (Cab == nullptr)) - { - // with the lamp off nothing reads the map; once it is on again the cab may have changed - m_valid = false; - return false; - } - if ((false == m_valid) || (Vehicle != m_vehicle) || (Cab != m_cab) || (Lamp != m_lamp)) - { - m_valid = false; - } - // the moving parts of the cab are followed every few frames - if (m_valid && (Framestamp - m_drawnframe < setup(level).interval)) - { - return false; - } - m_vehicle = Vehicle; - m_cab = Cab; - m_lamp = Lamp; - m_drawnframe = Framestamp; - return true; -} - -void opengl46_local_shadows::begin() -{ - m_cpustart = std::chrono::steady_clock::now(); m_timer.begin(); set_layer(CAB_SLOT * 6); setup_drawing(m_framebuffer, m_size); @@ -332,308 +234,46 @@ void opengl46_local_shadows::begin() glClearNamedFramebufferfv(m_framebuffer, GL_DEPTH, 0, &farthest); } -void opengl46_local_shadows::end(std::uint32_t const Drawcalls) +void opengl46_local_shadows::gpu_end_cab() { glEnable(GL_CULL_FACE); gl::framebuffer::unbind(); m_timer.end(); - m_valid = true; - m_drawcalls = Drawcalls; - ++m_updatecount; - auto const elapsed{std::chrono::duration(std::chrono::steady_clock::now() - m_cpustart).count()}; - m_cputime = (m_cputime == 0.0 ? elapsed : m_cputime * 0.9 + elapsed * 0.1); } -glm::vec4 opengl46_local_shadows::weights() const +void opengl46_local_shadows::gpu_begin_lamp(int const Scenerycube, int const Workingcube, bool const Vehicles) { - // a face spans twice the distance from the lamp - return glm::vec4{static_cast(CAB_SLOT), SHADOW_NEAR, 2.f / static_cast(std::max(m_size, 1)), setup(m_level).radius}; -} - -void opengl46_local_shadows::update_lamps(std::vector const &Lights, glm::dvec3 const &Camera, std::uint64_t const Version, unsigned int const Framestamp) -{ - m_frame = Framestamp; - m_wanted.clear(); - auto const level{std::clamp(Global.gfx_lights_shadows, 0, 3)}; - auto const count{level > 0 ? Global.gfx_lights_shadows_lamps : 0}; - auto const size{count > 0 ? Global.gfx_lights_shadows_lamps_size : 0}; - if ((static_cast(count) != m_slots.size()) || (size != m_lampssize)) - { - allocate_lamps(count, size); - } - if (m_slots.empty()) - { - return; - } - m_sceneryleft = setup(level).sceneryperframe; - auto const reach{static_cast(Global.gfx_lights_shadows_lamps_range)}; - // the lights come the most important first: the first ones within reach get the slots - std::vector picked; - for (auto const &light : Lights) - { - if (picked.size() >= m_slots.size()) - { - break; - } - if ((light.owner == nullptr) || (light.range < LAMP_RANGE_MIN) || (glm::length(light.position - Camera) > reach)) - { - continue; - } - picked.emplace_back(&light); - } - // lamps which already have a slot keep it - std::vector unplaced; - for (auto const *light : picked) - { - auto placed{false}; - for (std::size_t idx = 0; idx < m_slots.size(); ++idx) - { - auto &slot{m_slots[idx]}; - if ((slot.owner == light->owner) && (slot.part == light->part) && (slot.wantedframe != Framestamp)) - { - if (glm::length(slot.position - light->position) > LAMP_MOVED) - { - slot.scenery = false; - slot.vehicles = false; - } - slot.position = light->position; - slot.range = light->range; - slot.wantedframe = Framestamp; - m_wanted.emplace_back(idx); - placed = true; - break; - } - } - if (false == placed) - { - unplaced.emplace_back(light); - } - } - // the others take the slots of lamps no longer picked - for (auto const *light : unplaced) - { - for (std::size_t idx = 0; idx < m_slots.size(); ++idx) - { - auto &slot{m_slots[idx]}; - if (slot.wantedframe == Framestamp) - { - continue; - } - slot = lamp_slot{}; - slot.owner = light->owner; - slot.part = light->part; - slot.position = light->position; - slot.range = light->range; - slot.wantedframe = Framestamp; - m_wanted.emplace_back(idx); - break; - } - } - // the scene changed (events, the editor): the scenery parts are out of date - for (auto &slot : m_slots) - { - if (slot.scenery && (slot.version != Version)) - { - slot.scenery = false; - slot.vehicles = false; - } - } -} - -void opengl46_local_shadows::reset_lamps() -{ - for (auto &slot : m_slots) - { - slot = lamp_slot{}; - } - m_wanted.clear(); -} - -bool opengl46_local_shadows::scenery_wanted(std::size_t const Slot) -{ - if (m_slots[Slot].scenery || (m_sceneryleft <= 0)) - { - return false; - } - --m_sceneryleft; - return true; -} - -bool opengl46_local_shadows::vehicles_wanted(std::size_t const Slot, unsigned int const Framestamp) const -{ - auto const &slot{m_slots[Slot]}; - return slot.scenery && ((false == slot.vehicles) || (Framestamp - slot.vehiclesframe >= setup(m_level).interval)); -} - -void opengl46_local_shadows::begin_lamp(std::size_t const Slot, bool const Vehicles, std::uint64_t const Version, unsigned int const Framestamp) -{ - auto &slot{m_slots[Slot]}; - auto const scenerycube{static_cast(Slot)}; - auto const workingcube{static_cast(m_slots.size() + Slot)}; if (Vehicles) { // the vehicles go over the scenery - glCopyImageSubData(m_lampstexture, GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, scenerycube * 6, m_lampstexture, GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, workingcube * 6, m_lampssize, m_lampssize, 6); - slot.vehicles = true; - slot.vehiclesframe = Framestamp; - ++m_lampsvehicles; + glCopyImageSubData(m_lampstexture, GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, Scenerycube * 6, m_lampstexture, GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, Workingcube * 6, m_lampssize, m_lampssize, 6); } else { float const farthest{0.f}; - glClearTexSubImage(m_lampstexture, 0, 0, 0, scenerycube * 6, m_lampssize, m_lampssize, 6, GL_DEPTH_COMPONENT, GL_FLOAT, &farthest); - slot.scenery = true; - slot.version = Version; - slot.vehicles = false; - ++m_lampsscenery; + glClearTexSubImage(m_lampstexture, 0, 0, 0, Scenerycube * 6, m_lampssize, m_lampssize, 6, GL_DEPTH_COMPONENT, GL_FLOAT, &farthest); } - set_layer((Vehicles ? workingcube : scenerycube) * 6); + set_layer((Vehicles ? Workingcube : Scenerycube) * 6); setup_drawing(m_lampsframebuffer, m_lampssize); } -void opengl46_local_shadows::end_lamp(std::uint32_t const Drawcalls) +void opengl46_local_shadows::gpu_end_lamp() { glEnable(GL_CULL_FACE); gl::framebuffer::unbind(); set_layer(CAB_SLOT * 6); - m_lampsframedrawcalls += Drawcalls; } -void opengl46_local_shadows::lamps_timer_begin() +void opengl46_local_shadows::gpu_lamps_timer(bool const Begin) { - m_lampsframedrawcalls = 0; - m_cpustart = std::chrono::steady_clock::now(); - m_lampstimer.begin(); - m_lampstiming = true; + if (Begin) + m_lampstimer.begin(); + else + m_lampstimer.end(); } -void opengl46_local_shadows::lamps_timer_end() +void opengl46_local_shadows::gpu_begin_headlight(std::size_t const Slot) { - if (false == m_lampstiming) - { - return; - } - m_lampstimer.end(); - m_lampstiming = false; - m_lampsdrawcalls = m_lampsframedrawcalls; - auto const elapsed{std::chrono::duration(std::chrono::steady_clock::now() - m_cpustart).count()}; - m_lampscputime = (m_lampscputime == 0.0 ? elapsed : m_lampscputime * 0.9 + elapsed * 0.1); -} - -opengl46_local_shadows::lamp_slot const *opengl46_local_shadows::lamp(gfx::local_lights::light const &Light, std::size_t &Slot) const -{ - if (Light.owner == nullptr) - { - return nullptr; - } - for (auto const idx : m_wanted) - { - auto const &slot{m_slots[idx]}; - if ((slot.owner == Light.owner) && (slot.part == Light.part)) - { - if (false == slot.scenery) - { - return nullptr; - } - Slot = idx; - return &slot; - } - } - return nullptr; -} - -glm::vec4 opengl46_local_shadows::lamp_weights(std::size_t const Slot) const -{ - auto const cube{m_slots[Slot].vehicles ? m_slots.size() + Slot : Slot}; - return glm::vec4{static_cast(cube), SHADOW_NEAR, 2.f / static_cast(std::max(m_lampssize, 1)), setup(m_level).radius}; -} - -void opengl46_local_shadows::update_headlights(std::vector const &Lights, unsigned int const Framestamp) -{ - m_headlightswanted.clear(); - auto const level{std::clamp(Global.gfx_lights_shadows, 0, 3)}; - auto const count{level > 0 ? std::clamp(Global.gfx_lights_shadows_headlights, 0, static_cast(gl::MAX_HEADLIGHT_SHADOWS)) : 0}; - auto const size{count > 0 ? setup(level).headlightsize : 0}; - if ((static_cast(count) != m_headlightslots.size()) || (size != m_headlightssize)) - { - allocate_headlights(count, size); - } - if (m_headlightslots.empty()) - { - return; - } - // sets which already have a slot keep it - std::vector unplaced; - for (auto const *light : Lights) - { - if (m_headlightswanted.size() + unplaced.size() >= m_headlightslots.size()) - { - break; - } - auto placed{false}; - for (std::size_t idx = 0; idx < m_headlightslots.size(); ++idx) - { - auto &slot{m_headlightslots[idx]}; - if ((slot.owner == light->owner) && (slot.index == light->index) && (slot.wantedframe != Framestamp)) - { - slot.wantedframe = Framestamp; - m_headlightswanted.emplace_back(idx); - placed = true; - break; - } - } - if (false == placed) - { - unplaced.emplace_back(light); - } - } - // the others take the slots of sets no longer picked - for (auto const *light : unplaced) - { - for (std::size_t idx = 0; idx < m_headlightslots.size(); ++idx) - { - auto &slot{m_headlightslots[idx]}; - if (slot.wantedframe == Framestamp) - { - continue; - } - slot = headlight_slot{}; - slot.owner = light->owner; - slot.index = light->index; - slot.wantedframe = Framestamp; - m_headlightswanted.emplace_back(idx); - break; - } - } -} - -void opengl46_local_shadows::reset_headlights() -{ - for (auto &slot : m_headlightslots) - { - slot = headlight_slot{}; - } - m_headlightswanted.clear(); -} - -bool opengl46_local_shadows::headlight_wanted(std::size_t const Slot, bool const Moved, unsigned int const Framestamp) const -{ - auto const &slot{m_headlightslots[Slot]}; - auto const level{setup(m_level)}; - return (false == slot.drawn) || (Framestamp - slot.drawnframe >= (Moved ? level.headlightmoving : level.headlightstill)); -} - -void opengl46_local_shadows::begin_headlight(std::size_t const Slot, glm::dvec3 const &Projector, glm::mat3 const &Rotation, glm::mat4 const &Projection, glm::vec3 const &Direction, unsigned int const Framestamp) -{ - auto &slot{m_headlightslots[Slot]}; - slot.drawn = true; - slot.drawnframe = Framestamp; - slot.projector = Projector; - slot.rotation = Rotation; - slot.projection = Projection; - slot.direction = Direction; - ++m_headlightsdrawn; - gl::framebuffer::unbind(); glNamedFramebufferTextureLayer(m_headlightsframebuffer, GL_DEPTH_ATTACHMENT, m_headlightstexture, 0, static_cast(Slot)); glBindFramebuffer(GL_FRAMEBUFFER, m_headlightsframebuffer); @@ -649,51 +289,17 @@ void opengl46_local_shadows::begin_headlight(std::size_t const Slot, glm::dvec3 glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); } -void opengl46_local_shadows::end_headlight(std::uint32_t const Drawcalls) +void opengl46_local_shadows::gpu_end_headlight() { gl::framebuffer::unbind(); - m_headlightsframedrawcalls += Drawcalls; } -void opengl46_local_shadows::headlights_timer_begin() +void opengl46_local_shadows::gpu_headlights_timer(bool const Begin) { - m_headlightsframedrawcalls = 0; - m_headlightscpustart = std::chrono::steady_clock::now(); - m_headlightstimer.begin(); - m_headlightstiming = true; -} - -void opengl46_local_shadows::headlights_timer_end() -{ - if (false == m_headlightstiming) - { - return; - } - m_headlightstimer.end(); - m_headlightstiming = false; - m_headlightsdrawcalls = m_headlightsframedrawcalls; - auto const elapsed{std::chrono::duration(std::chrono::steady_clock::now() - m_headlightscpustart).count()}; - m_headlightscputime = (m_headlightscputime == 0.0 ? elapsed : m_headlightscputime * 0.9 + elapsed * 0.1); -} - -int opengl46_local_shadows::headlight_map(TDynamicObject const *Owner, int const Index) const -{ - for (auto const idx : m_headlightswanted) - { - auto const &slot{m_headlightslots[idx]}; - if ((slot.owner == Owner) && (slot.index == Index)) - { - return (slot.drawn ? static_cast(idx) : -1); - } - } - return -1; -} - -glm::vec4 opengl46_local_shadows::headlight_params(std::size_t const Slot) const -{ - // a map spans 2 tan(fov / 2) one metre from the projector, and the projection holds 1 / tan(fov / 2) - auto const &projection{m_headlightslots[Slot].projection}; - return glm::vec4{2.f / (std::max(projection[1][1], 1e-6f) * static_cast(std::max(m_headlightssize, 1))), setup(m_level).radius, 0.f, 0.f}; + if (Begin) + m_headlightstimer.begin(); + else + m_headlightstimer.end(); } std::string opengl46_local_shadows::info_times() @@ -707,67 +313,3 @@ std::string opengl46_local_shadows::info_times() text += " headlight shadows: " + to_string(static_cast(*time) / 1000000.0, 3) + " ms\n"; return text; } - -std::string opengl46_local_shadows::info() const -{ - std::string text; - if (m_size != 0) - { - text += "\nlamp shadows: " + std::to_string(m_size) + " cube, " + (m_valid ? "cab lamp" : "none") + ", " + std::to_string(m_drawcalls) + " drawcalls, cpu " + to_string(m_cputime, 2) + - " ms"; - } - if (false == m_slots.empty()) - { - auto withvehicles{0}; - auto ready{0}; - for (auto const idx : m_wanted) - { - ready += (m_slots[idx].scenery ? 1 : 0); - withvehicles += (m_slots[idx].vehicles ? 1 : 0); - } - text += "\nscenery lamp shadows: " + std::to_string(m_wanted.size()) + " of " + std::to_string(m_slots.size()) + " slots (" + std::to_string(ready) + " ready, " + - std::to_string(withvehicles) + " with vehicles), drawn " + std::to_string(m_lampsscenery) + " scenery + " + std::to_string(m_lampsvehicles) + " vehicles, " + - std::to_string(m_lampsdrawcalls) + " drawcalls, cpu " + to_string(m_lampscputime, 2) + " ms"; - } - if (false == m_headlightslots.empty()) - { - auto ready{0}; - for (auto const idx : m_headlightswanted) - { - ready += (m_headlightslots[idx].drawn ? 1 : 0); - } - text += "\nheadlight shadows: " + std::to_string(m_headlightswanted.size()) + " of " + std::to_string(m_headlightslots.size()) + " slots (" + std::to_string(ready) + " ready), " + - std::to_string(m_headlightssize) + " map, drawn " + std::to_string(m_headlightsdrawn) + " times, " + std::to_string(m_headlightsdrawcalls) + " drawcalls, cpu " + - to_string(m_headlightscputime, 2) + " ms"; - } - return text; -} - -void opengl46_local_shadows::draw_debug_ui() -{ - char const *levels[]{"no", "low (512, every 4th frame)", "medium (1024, every 2nd frame)", "high (1024, every frame, softer)"}; - ImGui::Combo("lamp shadows (gfx.lights.shadows)", &Global.gfx_lights_shadows, levels, IM_ARRAYSIZE(levels)); - if (m_size != 0) - { - ImGui::Text("cab: cube %d x %d, %s, drawn %u times, %u drawcalls, %.2f ms (cpu)", m_size, m_size, m_valid ? "cab lamp" : "no lamp", m_updatecount, m_drawcalls, m_cputime); - } - ImGui::SliderInt("scenery lamps with shadows (gfx.lights.shadows.lamps)", &Global.gfx_lights_shadows_lamps, 0, 32); - ImGui::SliderFloat("their reach from the camera, m (.lamps.range)", &Global.gfx_lights_shadows_lamps_range, 0.f, 500.f); - ImGui::SliderFloat("vehicles casting them, from the camera, m (.vehicles.range)", &Global.gfx_lights_shadows_vehicles_range, 0.f, 500.f); - if (false == m_slots.empty()) - { - auto const text{info()}; - auto const line{text.find("\nscenery lamp shadows:")}; - auto const end{(line == std::string::npos) ? std::string::npos : text.find('\n', line + 1)}; - if (line != std::string::npos) - ImGui::TextUnformatted(text.substr(line + 1, end == std::string::npos ? std::string::npos : end - line - 1).c_str()); - } - ImGui::SliderInt("headlights with shadows (gfx.lights.shadows.headlights)", &Global.gfx_lights_shadows_headlights, 0, static_cast(gl::MAX_HEADLIGHT_SHADOWS)); - ImGui::SliderFloat("their reach ahead of the vehicle, m (.headlights.range)", &Global.gfx_lights_shadows_headlights_range, 10.f, 1000.f); - if (false == m_headlightslots.empty()) - { - auto const text{info()}; - auto const line{text.rfind('\n')}; - ImGui::TextUnformatted(text.c_str() + (line == std::string::npos ? 0 : line + 1)); - } -} diff --git a/rendering/opengl46/opengl46localshadows.h b/rendering/opengl46/opengl46localshadows.h index 604a11d0..b58127a9 100644 --- a/rendering/opengl46/opengl46localshadows.h +++ b/rendering/opengl46/opengl46localshadows.h @@ -19,37 +19,17 @@ http://mozilla.org/MPL/2.0/. #include "gl/shader.h" #include "gl/query.h" #include "rendering/common/locallights.h" +#include "rendering/common/localshadows.h" #include "rendering/lightarray.h" -class TDynamicObject; -class TModel3d; - -// cube shadow maps of the lamps of the opengl 4.6 renderer (Global.gfx_lights_shadows): -// - the lamp of the occupied cab, with the shadows cast by the cab (the model drawn by Render_cab()). the map is kept in the space -// of the cab model, centred on the lamp: it stays right while the vehicle moves and turns, and is drawn anew only every few frames -// (for the moving parts of the cab: levers, doors, wipers) or when the lamp, the cab or the level change; -// - the lamps of scenery models closest to the camera (Global.gfx_lights_shadows_lamps of them, within -// Global.gfx_lights_shadows_lamps_range). the map of such a lamp is kept in world axes, centred on the lamp, and has two parts: -// the scenery (drawn once, when the lamp gets its slot or the scene changes) and a working copy of it, with the vehicles near -// the camera (within Global.gfx_lights_shadows_vehicles_range) and the occupied cab added, made every few frames while there -// are any by the lamp. the shading reads the working copy then, the scenery part otherwise. -// the six faces of a map are drawn at once, by a geometry shader (shadowcube.vert/.geom), so a model costs one draw call per -// submodel, as in the colour pass. the depth is n / d (d the distance along the axis of the face), see local_light_shadow() in -// light_common.glsl, which reads the map for the light with the layer in its headlight_weights.x, from the array of scenery -// lamps when its linear is 1 and from the one of the cab lamp otherwise. -// - the headlights of the vehicles (Global.gfx_lights_shadows_headlights sets of them, the most important ones): a plain map, -// drawn from the projector of the beam with its projection (the beam is a narrow frustum aimed along the vehicle, see -// opengl46_renderer::Headlight_Projector()), with what is within Global.gfx_lights_shadows_headlights_range ahead of the -// vehicle, but not the vehicle itself. the map keeps the projector it was drawn with, in world space: one a few frames old is -// still right for what doesn't move. read by headlight_beam_shadow() in light_common.glsl, for the headlights with the map in their -// headlight_weights.w (1 + the layer) and the projection to it in gl::scene_ubs::headlight_shadow -class opengl46_local_shadows +// the shadow maps of the lamps of the opengl 4.6 renderer (Global.gfx_lights_shadows): the maps and the drawing to them of +// gfx::local_shadows, which tells what they hold. the six faces of a cube map are drawn at once, by a geometry shader +// (shadowcube.vert/.geom) +class opengl46_local_shadows : public gfx::local_shadows { public: opengl46_local_shadows() = default; - ~opengl46_local_shadows(); - opengl46_local_shadows(const opengl46_local_shadows &) = delete; - opengl46_local_shadows &operator=(const opengl46_local_shadows &) = delete; + ~opengl46_local_shadows() override; // creates the programs and the stand-in maps. throws gl::shader_exception void init(); @@ -67,157 +47,26 @@ class opengl46_local_shadows { return Alpha ? m_alpha_multidraw_program.get() : m_multidraw_program.get(); } - - // the cab lamp - - // whether the map of the cab lamp is to be drawn in this frame: Vehicle and Cab the occupied vehicle and its cab model, - // Lamp the position of the lamp in the space of the cab model; Vehicle nullptr when the lamp is off. (re)makes the map to - // the size of the level when the level changed - bool update_wanted(TDynamicObject const *Vehicle, TModel3d const *Cab, glm::vec3 const &Lamp, unsigned int const Framestamp); - // sets up the drawing to the map: the framebuffer, viewport, cleared faces, the state of the faces. the geometry is to be - // drawn with program() and a modelview from the space of the model to the space of the map - void begin(); - // ends the drawing to the map, leaves the default framebuffer bound and the culling of faces on - void end(std::uint32_t const Drawcalls); - // whether the map holds the shadows of the lamp of Vehicle, the occupied vehicle - bool ready(TDynamicObject const *Vehicle) const - { - return m_valid && (Vehicle != nullptr) && (Vehicle == m_vehicle); - } - // position of the lamp the map was drawn for, in the space of the cab model - glm::vec3 const &lamp() const - { - return m_lamp; - } - // the headlight_weights of the light which reads the map: layer, near plane, size of a texel one metre from the lamp, - // radius of the filter in texels - glm::vec4 weights() const; - - // lamps of scenery models - - // a lamp with a slot - struct lamp_slot - { - void const *owner{nullptr}; // the light it is, see gfx::local_lights::light - void const *part{nullptr}; - glm::dvec3 position{0.0}; // world space - float range{0.f}; // of the light, metres - unsigned int wantedframe{~0u}; // framestamp it was last among the lamps given shadows - bool scenery{false}; // the scenery part of the map is drawn - std::uint64_t version{0}; // of the scene the scenery part was drawn for - bool vehicles{false}; // the working copy is drawn, with vehicles, and is what the shading reads - unsigned int vehiclesframe{0}; // framestamp the working copy was last drawn in - }; - // picks the lamps which get shadows in this frame from the gathered lights (the most important first): the scenery lamps - // within reach of Camera. a lamp keeps its slot while it is picked; one which got a slot or moved, and all of them when the - // scene changes (Version), need their scenery drawn. (re)makes the maps when their count or size changed - void update_lamps(std::vector const &Lights, glm::dvec3 const &Camera, std::uint64_t const Version, unsigned int const Framestamp); - // forgets the lamps: the scene they come from is about to be replaced (a scenery being loaded) - void reset_lamps(); - // slots of the lamps picked in this frame - std::vector const &lamps() const - { - return m_wanted; - } - lamp_slot const &slot(std::size_t const Slot) const - { - return m_slots[Slot]; - } - // whether the scenery part of the map of the slot is to be drawn in this frame. a few per frame at most, by the level - bool scenery_wanted(std::size_t const Slot); - // whether the working copy of the slot, with vehicles, is to be drawn in this frame (every few frames, by the level) - bool vehicles_wanted(std::size_t const Slot, unsigned int const Framestamp) const; - // the slot has no vehicles by it: the shading reads the scenery part - void no_vehicles(std::size_t const Slot) - { - m_slots[Slot].vehicles = false; - } - // sets up the drawing to the scenery part (cleared) or to the working copy (a copy of the scenery part) of the map of the slot. - // the geometry is to be drawn with program() and a modelview to world axes centred on the lamp - void begin_lamp(std::size_t const Slot, bool const Vehicles, std::uint64_t const Version, unsigned int const Framestamp); - // ends the drawing to the map of a lamp - void end_lamp(std::uint32_t const Drawcalls); - // gpu timer of the drawing of the maps of the lamps in this frame - void lamps_timer_begin(); - void lamps_timer_end(); - // the slot of the lamp the light is, if it has its shadows ready, nullptr otherwise - lamp_slot const *lamp(gfx::local_lights::light const &Light, std::size_t &Slot) const; - // the headlight_weights of the light of the lamp in the slot, see weights() - glm::vec4 lamp_weights(std::size_t const Slot) const; - - // headlights of vehicles - - // a set of headlights (one end of a vehicle) with a slot - struct headlight_slot - { - TDynamicObject const *owner{nullptr}; // light_array::light_record::owner and index - int index{-1}; - unsigned int wantedframe{~0u}; // framestamp it was last among the headlights given shadows - bool drawn{false}; // the map holds the shadows of the headlights - unsigned int drawnframe{0}; - // the projector the map was drawn with: world space position, world axes to its axes, projection - glm::dvec3 projector{0.0}; - glm::mat3 rotation{1.f}; - glm::mat4 projection{1.f}; - glm::vec3 direction{0.f}; // of the light when the map was drawn - }; - // picks the headlights which get shadows in this frame: Lights, the most important first, as many as there are slots - // (Global.gfx_lights_shadows_headlights). a set keeps its slot while it is picked. (re)makes the maps when their count or size changed - void update_headlights(std::vector const &Lights, unsigned int const Framestamp); - // forgets the headlights: the vehicles may be about to go (a scenery being loaded) - void reset_headlights(); - // slots of the headlights picked in this frame - std::vector const &headlights() const - { - return m_headlightswanted; - } - headlight_slot const &headlight(std::size_t const Slot) const - { - return m_headlightslots[Slot]; - } - // whether the map of the slot is to be drawn in this frame: one never drawn, then every few frames by the level, more often - // while the headlights Moved (with the vehicle) - bool headlight_wanted(std::size_t const Slot, bool const Moved, unsigned int const Framestamp) const; - // sets up the drawing to the map of the slot (cleared), drawn from the projector given (see headlight_slot). the geometry is - // to be drawn by the shadow pass with this projection, back faces only - void begin_headlight(std::size_t const Slot, glm::dvec3 const &Projector, glm::mat3 const &Rotation, glm::mat4 const &Projection, glm::vec3 const &Direction, unsigned int const Framestamp); - // ends the drawing to the map of a set of headlights, leaves the default framebuffer bound - void end_headlight(std::uint32_t const Drawcalls); - // gpu timer of the drawing of the maps of the headlights in this frame - void headlights_timer_begin(); - void headlights_timer_end(); - // the slot of the headlights with their map ready in this frame, -1 if there is none - int headlight_map(TDynamicObject const *Owner, int const Index) const; - // the headlight_shadow_params of the map of the slot, see gl::scene_ubs - glm::vec4 headlight_params(std::size_t const Slot) const; - - // lines for the gpu times and statistics of the debug panel + // lines for the gpu times of the debug panel std::string info_times(); - std::string info() const; - // controls for the debug window - void draw_debug_ui(); + + protected: + bool gpu_allocate(int const Size) override; + void gpu_release() override; + bool gpu_allocate_lamps(int const Count, int const Size) override; + void gpu_release_lamps() override; + bool gpu_allocate_headlights(int const Count, int const Size) override; + void gpu_release_headlights() override; + void gpu_begin_cab() override; + void gpu_end_cab() override; + void gpu_begin_lamp(int const Scenerycube, int const Workingcube, bool const Vehicles) override; + void gpu_end_lamp() override; + void gpu_begin_headlight(std::size_t const Slot) override; + void gpu_end_headlight() override; + void gpu_lamps_timer(bool const Begin) override; + void gpu_headlights_timer(bool const Begin) override; private: - // size, refresh interval (frames), filter radius (texels, 0: the 2x2 filter of the hardware) of a level, the number of - // scenery parts of lamp maps drawn per frame, and the size and refresh intervals (frames, while the vehicle moves and while - // it doesn't) of the maps of headlights - struct level_setup - { - int size; - unsigned int interval; - float radius; - int sceneryperframe; - int headlightsize; - unsigned int headlightmoving; - unsigned int headlightstill; - }; - static level_setup setup(int const Level); - void allocate(int const Size); - void release(); - void allocate_lamps(int const Count, int const Size); - void release_lamps(); - void allocate_headlights(int const Count, int const Size); - void release_headlights(); void set_layer(int const Layer); std::unique_ptr m_program; @@ -226,48 +75,16 @@ class opengl46_local_shadows std::unique_ptr m_alpha_multidraw_program; GLuint m_empty{0}; // stand-in: a cube map array of 1x1 faces with the farthest depth GLuint m_emptyheadlights{0}; // the same, a 2d array of one 1x1 layer - int m_level{-1}; // level the maps were made for - // cab lamp - GLuint m_texture{0}; // cube map array, one cube + // cab lamp: cube map array, one cube + GLuint m_texture{0}; GLuint m_framebuffer{0}; - int m_size{0}; // of a face of m_texture, 0 when there is none - bool m_valid{false}; // m_texture holds the map of m_vehicle - TDynamicObject const *m_vehicle{nullptr}; - TModel3d const *m_cab{nullptr}; - glm::vec3 m_lamp{0.f}; - unsigned int m_drawnframe{0}; // framestamp the map was last drawn in gl::gpu_timer m_timer; // scenery lamps: cube map array, the scenery parts of the slots first, then their working copies GLuint m_lampstexture{0}; GLuint m_lampsframebuffer{0}; - int m_lampssize{0}; - std::vector m_slots; - std::vector m_wanted; - unsigned int m_frame{0}; // framestamp of the last update_lamps() - int m_sceneryleft{0}; // scenery parts which may still be drawn in this frame gl::gpu_timer m_lampstimer; - bool m_lampstiming{false}; // headlights: 2d array, a layer per slot GLuint m_headlightstexture{0}; GLuint m_headlightsframebuffer{0}; - int m_headlightssize{0}; - std::vector m_headlightslots; - std::vector m_headlightswanted; gl::gpu_timer m_headlightstimer; - bool m_headlightstiming{false}; - std::chrono::steady_clock::time_point m_headlightscpustart; - // statistics - std::uint32_t m_drawcalls{0}; // of the last drawing of the cab map - std::uint32_t m_updatecount{0}; // drawings of the cab map so far - double m_cputime{0.0}; // milliseconds, smoothed - std::chrono::steady_clock::time_point m_cpustart; - std::uint32_t m_lampsscenery{0}; // scenery parts drawn so far - std::uint32_t m_lampsvehicles{0}; // working copies drawn so far - std::uint32_t m_lampsdrawcalls{0}; // in the drawings of the lamp maps in the last frame with any - std::uint32_t m_lampsframedrawcalls{0}; - double m_lampscputime{0.0}; // milliseconds per frame, smoothed - std::uint32_t m_headlightsdrawn{0}; // maps of headlights drawn so far - std::uint32_t m_headlightsdrawcalls{0}; // in the drawings of the maps of headlights in the last frame with any - std::uint32_t m_headlightsframedrawcalls{0}; - double m_headlightscputime{0.0}; // milliseconds per frame, smoothed }; diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index c9a70c7c..c8b0b97e 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -412,6 +412,12 @@ bool opengl46_renderer::Init(GLFWwindow *Window) WriteLog("picking objects created"); + if (Global.gfx_threads != 0) + { + // the threads of the renderers (gfx.threads) record the command buffers of the vulkan renderer; the opengl renderer walks and + // draws the scene on the main thread + WriteLog("Gfx Renderer: gfx.threads without effect with opengl"); + } WriteLog("Gfx Renderer: setup complete"); return true; @@ -657,10 +663,7 @@ bool opengl46_renderer::Render() m_localshadows.reset_lamps(); m_localshadows.reset_headlights(); // and so may the tracks the sleeper records were made for, and the models drawn on their own - m_sleeperrecords.clear(); - m_modelrecords.clear(); - m_cascaderecords.clear(); - m_walkrecords.clear(); + Clear_Indirect_Records(); } // generate new frame opengl_texture::reset_unit_cache(); @@ -672,19 +675,8 @@ bool opengl46_renderer::Render() ++m_instancerecordsversion; m_nodestateversion = scene::node_state_version; } - if( ( m_framestamp % 256 ) == 0 ) { - // the sleeper records of tracks out of sight for a while. their places in the record buffer of the gpu culling are - // dropped when it starts over - for( auto records { m_sleeperrecords.begin() }; records != m_sleeperrecords.end(); ) { - if( m_framestamp - records->second.frame > 256 ) { records = m_sleeperrecords.erase( records ); } - else { ++records; } - } - // and so are the records of the models drawn on their own - for( auto records { m_modelrecords.begin() }; records != m_modelrecords.end(); ) { - if( m_framestamp - records->second.frame > 256 ) { records = m_modelrecords.erase( records ); } - else { ++records; } - } - } + // the sleeper records of tracks out of sight for a while, and the records of the models drawn on their own + Expire_Indirect_Records(); // scenery lamps which get shadows in this frame. their maps hold the scenery as of the version of the instance records if (simulation::is_ready && (false == Global.gfx_skiprendering)) m_localshadows.update_lamps(m_locallights.lights(), Global.pCamera.Pos, m_instancerecordsversion, m_framestamp); @@ -2449,45 +2441,8 @@ void opengl46_renderer::setup_pass(viewport_config &Viewport, renderpass_config pass_camera /= bounding_viewports_count; // ...a cascade fitted to the depth of the view (gfx.shadow.sdsm) covers the area of the pixels it takes, within the - // slice of the view. the areas are compared across the light, the depth along it stays the one of the slice... - auto quantizationstep{0.f}; - { - auto const lightrotation{Shadow_Light_Rotation()}; - glm::vec3 slicemin{std::numeric_limits::max()}, slicemax{std::numeric_limits::lowest()}; - for (auto const &point : frustumchunkshapepoints) - { - auto const lightpoint{lightrotation * glm::vec3{point}}; - slicemin = glm::min(slicemin, lightpoint); - slicemax = glm::max(slicemax, lightpoint); - } - m_shadowslicesize = std::max(slicemax.x - slicemin.x, slicemax.y - slicemin.y); - if (m_shadowfitarea != nullptr) - { - glm::vec3 areamin{std::numeric_limits::max()}, areamax{std::numeric_limits::lowest()}; - for (auto const &point : *m_shadowfitarea) - { - auto const lightpoint{lightrotation * point}; - areamin = glm::min(areamin, lightpoint); - areamax = glm::max(areamax, lightpoint); - } - auto const fitmin{glm::vec2{glm::max(glm::vec2{slicemin}, glm::vec2{areamin})}}; - auto const fitmax{glm::vec2{glm::min(glm::vec2{slicemax}, glm::vec2{areamax})}}; - if ((fitmin.x < fitmax.x) && (fitmin.y < fitmax.y)) - { - // the size changes in steps of a power of two of about 1/16 of it, so that the texels of the map keep - // their size until the area grows or shrinks by a step - auto const size{std::max(fitmax.x - fitmin.x, fitmax.y - fitmin.y)}; - quantizationstep = std::exp2(std::ceil(std::log2(std::max(size, 1.f) / 16.f))); - frustumchunkshapepoints.clear(); - auto const inverserotation{glm::transpose(lightrotation)}; - for (int corner = 0; corner < 8; ++corner) - { - auto const lightpoint{glm::vec3{(corner & 1) ? fitmax.x : fitmin.x, (corner & 2) ? fitmax.y : fitmin.y, (corner & 4) ? slicemax.z : slicemin.z}}; - frustumchunkshapepoints.emplace_back(inverserotation * lightpoint, 1.f); - } - } - } - } + // slice of the view... + auto quantizationstep{gfx::shadow_fit::fit_slice(frustumchunkshapepoints, m_shadowfitarea, Shadow_Light_Rotation(), m_shadowslicesize)}; // ...modelview matrix: determine the centre of frustum chunk in world space... glm::vec3 frustumchunkmin, frustumchunkmax; @@ -3424,11 +3379,7 @@ void opengl46_renderer::draw(const gfx::geometry_handle &handle) model_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW)); if( m_indirectrecording ) { // the walk of Render_Instances_Indirect(): the draw is made later, from the gpu culling, with this state - gfx::indirect_source source; - if( m_geometry.indirect_source( handle, source ) ) { - m_indirectcommands.push_back( { source.count, 0, source.first, static_cast( source.basevertex ), 0, m_indirectentry, ( source.indexed ? 1u : 0u ), 0 } ); - m_indirectdraws.push_back( { model_ubs, m_indirectmaterial, m_indirectsubmodel, handle, source.type, m_blendingenabled, m_geometry.draw_group( handle ) } ); - } + Record_Indirect_Draw( handle ); return; } if( m_scenerecording ) { @@ -3470,65 +3421,6 @@ void opengl46_renderer::draw(std::vector::iterator it, -// queues a model instance drawn on its own in the gpu culling of the pass, as a bucket and a group of its own. the culling -// makes the tests of Render( TAnimModel * ): visibility, frustum, the range of the instance, the draw range, the size on the -// screen, and gives it the lod band of the bounds of its submodels (mindistance/maxdistance) its distance falls in, then -// Record_Indirect_Draws() animates, prepares and walks its submodel tree from its origin, like it does for a group of -// instanced ones -bool opengl46_renderer::Queue_Model( TAnimModel *Instance, bool const Inrange ) -{ - // a billboard is turned to the camera from the modelview of the instance, which a walk from its origin doesn't have - if( ( false == Instance->m_cameraindependent ) || ( Instance->Model() == nullptr ) ) { return false; } - if( false == m_modelspass ) { - // the depth pass of the whole scene culled and recorded it, the colour pass draws what it recorded - return ( m_indirectcolourpending && m_indirectcolourmodels ); - } - auto const drawing { m_passcull->drawing() }; - // a model hidden by an event, or in a cell wholly out of the range of the pass, would be left out by the culling - if( ( false == Instance->visible() ) || ( false == Inrange ) ) { return drawing; } - - auto &records { m_modelrecords[ Instance ] }; - auto &bucket { records.bucket }; - if( ( bucket.recordsversion != m_instancerecordsversion ) || ( bucket.records.size() != 1 ) ) { - // the key, the instance and the record anew: the editor may have given the instance another model or skins, or - // another instance may have taken the place of a deleted one - auto &key { records.key }; - key = {}; - key.pModel = Instance->Model(); - if( auto const *material { Instance->Material() } ) { - for( int skin = 0; skin < 5; ++skin ) { key.skins[ skin ] = material->replacable_skins[ skin ]; } - } - key.instance = Instance; - bucket.instances.assign( 1, Instance ); - // the location of the instance as the origin of the record, which keeps the precision - Update_Instance_Records( key.pModel, bucket, Instance->location() ); - // drawn by its bucket, though it isn't instanceable - bucket.records.front().flags |= scene::basic_cell::instance_instanceable; - records.area = scene::bounding_area{ Instance->location(), 0.f }; - } - records.frame = m_framestamp; - if( m_renderpass.draw_mode == rendermode::shadows ) { - // a model with nothing to draw in the shadows is left out of them, like the instances. the answer depends on the - // model and the skins only, and is shared by the instances with them - auto modelkey { records.key }; - modelkey.instance = nullptr; - if( false == Instances_Cast_Shadow( modelkey, bucket ) ) { return drawing; } - } - // out of the frustum of the pass, beyond its range or too small on the screen by its record: the culling would leave it out, - // and the walk of Record_Indirect_Draws() isn't made for it - if( Global.gfx_gpucull_bucketrange && ( false == EditorModeFlag ) && Bucket_Culled( bucket ) ) { - m_passcull->skip_bucket( bucket ); - return drawing; - } - m_passcull->add_bucket( records.key, bucket, records.area ); - if( false == drawing ) { - // the cpu draws it too, and tells what it drew for the comparison - m_modelcomparekey = &records.key; - } - return drawing; -} - - // places supplied modelview matrices in the instance buffer of the frame. returns: base instance of the first of them std::uint32_t opengl46_renderer::Upload_Instance_Modelviews( glm::mat4 const *Modelviews, std::size_t const Count ) { @@ -3709,7 +3601,7 @@ void opengl46_renderer::Render_Instances_Indirect() void opengl46_renderer::Render_Instances_Indirect_Pass() { auto const cpustart { std::chrono::steady_clock::now() }; - auto &cull { *m_passcull }; + auto &cull { Pass_Cull() }; cull.dispatch( OpenGLMatrices.data( GL_MODELVIEW ) ); Record_Indirect_Draws( cull ); @@ -3734,178 +3626,12 @@ void opengl46_renderer::Render_Instances_Indirect_Pass() void opengl46_renderer::Record_Indirect_Draws( opengl46_instance_cull &Cull ) { m_indirectcull = &Cull; - m_indirectdraws.clear(); - m_indirectcommands.clear(); - auto const &lodbounds { Cull.lod_bounds() }; - // the cascades of the sun shadows make the same walks: the mode, the lod band and the state of the models are the same in all - // of them within a frame, only the instances differ, and those the gpu fills in. the walk of a group in a band is made by the - // first cascade which draws it, the others copy its draws and commands with entries of their own + // the cascades of the sun shadows share the walks auto const cascade { - ( m_renderpass.draw_mode == rendermode::shadows ) - && ( &Cull >= m_shadowcull.data() ) + ( &Cull >= m_shadowcull.data() ) && ( &Cull < m_shadowcull.data() + m_shadowcull.size() ) }; - auto constexpr nowalk { std::numeric_limits::max() }; - if( cascade && ( m_cascaderecordframe != m_framestamp ) ) { - m_cascaderecordframe = m_framestamp; - m_cascadedraws.clear(); - m_cascadecommands.clear(); - if( ( m_framestamp % 256 ) == 0 ) { - // the groups not drawn for a while - for( auto records { m_cascaderecords.begin() }; records != m_cascaderecords.end(); ) { - if( m_framestamp - records->second.frame > 256 ) { records = m_cascaderecords.erase( records ); } - else { ++records; } - } - } - } - if( m_walkcountsframe != m_framestamp ) { - m_walkcountsframe = m_framestamp; - m_lastwalkcounts = m_walkcounts; - m_walkcounts = {}; - } - auto const walkstart { std::chrono::steady_clock::now() }; - // the walks of earlier frames. they go when the switch is off, as the models may change meanwhile without the walks noticing - // it the next time (the season), and when the shadows take other submodels - auto const walkcache { Global.gfx_gpucull_walkcache && ( false == m_previewing ) }; - if( ( false == Global.gfx_gpucull_walkcache ) || ( m_walkrankcutoff != Global.gfx_shadow_rank_cutoff ) ) { - m_walkrecords.clear(); - m_walkrankcutoff = Global.gfx_shadow_rank_cutoff; - } - if( walkcache && ( ( m_framestamp % 256 ) == 0 ) ) { - // the groups not drawn for a while - for( auto records { m_walkrecords.begin() }; records != m_walkrecords.end(); ) { - if( m_framestamp - records->second.frame > 256 ) { records = m_walkrecords.erase( records ); } - else { ++records; } - } - } - auto const shadows { m_renderpass.draw_mode == rendermode::shadows }; - m_indirectrecording = true; - for( auto const &group : Cull.draw_groups() ) { - auto *model { group.key->pModel }; - if( ( model == nullptr ) || ( model->Root == nullptr ) || ( ( group.instance == nullptr ) && ( false == group.sleepers ) ) ) { continue; } - // a model drawn on its own is prepared with its lights and animations each time; the others are prepared only for a walk. - // the version is taken before, so a walk prepared by something which changes the model isn't taken up again - walk_record *kept { nullptr }; - if( walkcache && ( group.key->instance == nullptr ) ) { - kept = &m_walkrecords[ *group.key ]; - kept->frame = m_framestamp; - auto &bands { kept->bands[ shadows ? 1 : 0 ] }; - if( bands.size() != group.lodcount + 1 ) { - bands.clear(); - bands.resize( group.lodcount + 1 ); - } - } - auto const modelversion { model->state_version() }; - material_handle const *skins { nullptr }; - auto alpha { 0x30300030 }; - auto prepared { false }; - auto const prepare = [ & ]() { - prepared = true; - if( group.sleepers ) { - // as Render_Sleepers() sets them up: the skin of the track, if any. like scenery models they pick no texture - // variants of their own - if( group.key->skins[ 1 ] != null_handle ) { - skins = group.key->skins.data(); - } - TSubModel::iVariantSeed = 0; - } - else { - if( group.key->instance != nullptr ) { - // a model drawn on its own: its lights and animations, the way Render( TAnimModel * ) sets them up - group.instance->RaAnimate( m_framestamp ); - } - group.instance->RaPrepare(); - auto const *material { group.instance->Material() }; - if( material != nullptr ) { - skins = material->replacable_skins; - alpha = material->textures_alpha; - } - } - alpha ^= 0x0F0F000F; - }; - // the walks of the group the cascades made in this frame. a group whose bands were all walked already isn't prepared - cascade_record *walks { nullptr }; - if( cascade ) { - walks = &m_cascaderecords[ *group.key ]; - if( ( walks->frame != m_framestamp ) || ( walks->bands.size() != group.lodcount + 1 ) ) { - walks->frame = m_framestamp; - walks->bands.assign( group.lodcount + 1, cascade_band { nowalk, 0 } ); - } - } - else if( kept == nullptr ) { - prepare(); - } - for( std::uint32_t band = 0; band <= group.lodcount; ++band ) { - // a band holds the squared distances from its lower bound up to the next one; any of them selects the same lod - auto const bandmin { band == 0 ? 0.f : lodbounds[ group.lodfirst + band - 1 ] }; - auto const bandmax { band == group.lodcount ? std::numeric_limits::max() : lodbounds[ group.lodfirst + band ] }; - if( ( bandmin > group.distancemax ) || ( bandmax <= group.distancemin ) ) { continue; } - m_indirectentry = group.entryfirst + band; - if( ( walks != nullptr ) && ( walks->bands[ band ].first != nowalk ) ) { - // walked by another cascade: its draws, for the instances of this entry - auto const &walk { walks->bands[ band ] }; - m_indirectdraws.insert( m_indirectdraws.end(), m_cascadedraws.begin() + walk.first, m_cascadedraws.begin() + walk.first + walk.count ); - for( std::uint32_t draw = 0; draw < walk.count; ++draw ) { - auto &command { m_indirectcommands.emplace_back( m_cascadecommands[ walk.first + draw ] ) }; - command.entry = m_indirectentry; - } - // as the walk would count them - m_renderpass.draw_stats.submodels += static_cast( walk.count ); - m_renderpass.draw_stats.drawcalls += static_cast( walk.count ); - continue; - } - auto const walkfirst { m_indirectdraws.size() }; - auto *keptwalk { kept != nullptr ? &kept->bands[ shadows ? 1 : 0 ][ band ] : nullptr }; - if( ( keptwalk != nullptr ) - && ( keptwalk->valid ) - && ( keptwalk->bandmin == bandmin ) - && ( keptwalk->modelversion == modelversion ) - && ( keptwalk->blending == m_blendingenabled ) ) { - // walked in an earlier frame, with the model as it is now - Replay_Walk( *keptwalk, shadows ); - ++m_walkcounts.reused; - } - else { - if( false == prepared ) { - prepare(); - } - auto const blending { m_blendingenabled }; - m_indirectchanging = false; - ::glPushMatrix(); - ::glLoadIdentity(); - model->Root->fSquareDist = bandmin; - model->Root->ReplacableSet( skins, alpha ); - model->Root->pRoot = model; - Render( model->Root ); - ::glPopMatrix(); - ++m_walkcounts.walked; - if( m_indirectchanging ) { - ++m_walkcounts.changing; - } - if( keptwalk != nullptr ) { - keptwalk->valid = ( false == m_indirectchanging ); - if( keptwalk->valid ) { - keptwalk->draws.assign( m_indirectdraws.begin() + walkfirst, m_indirectdraws.end() ); - keptwalk->bandmin = bandmin; - keptwalk->modelversion = modelversion; - keptwalk->blending = blending; - } - else { - keptwalk->draws.clear(); - } - } - } - if( walks != nullptr ) { - walks->bands[ band ] = cascade_band { - static_cast( m_cascadedraws.size() ), - static_cast( m_indirectdraws.size() - walkfirst ) }; - m_cascadedraws.insert( m_cascadedraws.end(), m_indirectdraws.begin() + walkfirst, m_indirectdraws.end() ); - m_cascadecommands.insert( m_cascadecommands.end(), m_indirectcommands.begin() + walkfirst, m_indirectcommands.end() ); - } - } - } - m_indirectrecording = false; + Walk_Indirect_Draws( Cull, cascade ); auto const orderstart { std::chrono::steady_clock::now() }; - m_walkcounts.walktime += std::chrono::duration( orderstart - walkstart ).count(); // with multi draws the draws with the same state follow one another, and their model blocks are in a buffer the programs // of the multi draws read them from. the order goes into the commands, so it comes first @@ -3919,34 +3645,6 @@ void opengl46_renderer::Record_Indirect_Draws( opengl46_instance_cull &Cull ) m_walkcounts.ordertime += std::chrono::duration( std::chrono::steady_clock::now() - orderstart ).count(); } -void opengl46_renderer::Replay_Walk( walk_band const &Walk, bool const Shadows ) -{ - for( auto const &walkdraw : Walk.draws ) { - // the place of the chunk in its bank is asked for again: the bank may have been let go since and made anew, which the - // question (as the walk) takes care of, uploading the chunk - gfx::indirect_source source; - if( false == m_geometry.indirect_source( walkdraw.geometry, source ) ) { continue; } - m_indirectcommands.push_back( { source.count, 0, source.first, static_cast( source.basevertex ), 0, m_indirectentry, ( source.indexed ? 1u : 0u ), 0 } ); - auto &draw { m_indirectdraws.emplace_back( walkdraw ) }; - draw.type = source.type; - draw.group = m_geometry.draw_group( walkdraw.geometry ); - // the walk sets the placement of the submodel, and before each draw of the colour passes the parameters of its material - // (Bind_Material()); the rest of the block is what the pass has at the moment, as the walk would leave it - auto const &walked { walkdraw.ubs }; - draw.ubs = model_ubs; - draw.ubs.modelview = walked.modelview; - draw.ubs.modelviewnormal = walked.modelviewnormal; - if( false == Shadows ) { - std::memcpy( &draw.ubs.param[ 0 ], &walked.param[ 0 ], sizeof( draw.ubs.param ) ); - draw.ubs.opacity = walked.opacity; - draw.ubs.alpha_mult = walked.alpha_mult; - } - } - // as the walk would count them - m_renderpass.draw_stats.submodels += static_cast( Walk.draws.size() ); - m_renderpass.draw_stats.drawcalls += static_cast( Walk.draws.size() ); -} - opengl46_renderer::indirect_counts opengl46_renderer::Render_Instances_Indirect_Prepass( opengl46_instance_cull::command_set const Set, gl::gpu_timer &Timer ) { if( m_scenedepthpass ) { @@ -4151,149 +3849,6 @@ opengl46_renderer::indirect_counts opengl46_renderer::Render_Instances_Indirect_ return counts; } -texture_handle opengl46_renderer::Indirect_Shadow_Texture( material_handle const Material ) -{ - if( Material == null_handle ) { return null_handle; } - return m_materials.material( Material ).textures[ 0 ]; -} - -bool opengl46_renderer::Instances_Cast_Shadow( scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket ) -{ - auto const lookup { m_shadowcasters.find( Key ) }; - if( lookup != m_shadowcasters.end() ) { return lookup->second; } - - // the skins of the instance Render_Instanced() would take them from - TAnimModel const *instance { nullptr }; - for( auto const *candidate : Bucket.instances ) { - if( candidate != nullptr ) { - instance = candidate; - break; - } - } - auto const *skins { instance != nullptr && instance->Material() != nullptr ? instance->Material()->replacable_skins : nullptr }; - auto castsshadow { false }; - m_instance_lodpending.clear(); - m_instance_lodpending.push_back( Key.pModel != nullptr ? Key.pModel->Root : nullptr ); - while( ( false == castsshadow ) && ( false == m_instance_lodpending.empty() ) ) { - auto const *submodel = m_instance_lodpending.back(); - m_instance_lodpending.pop_back(); - if( submodel == nullptr ) { continue; } - if( submodel->eType < TP_ROTATOR ) { - auto const material { submodel->m_material >= 0 ? submodel->m_material : ( skins != nullptr ? skins[ -submodel->m_material ] : null_handle ) }; - castsshadow = ( Material( material ).shadow_rank <= Global.gfx_shadow_rank_cutoff ); - } - m_instance_lodpending.push_back( submodel->Child ); - m_instance_lodpending.push_back( submodel->Next ); - } - m_shadowcasters.emplace( Key, castsshadow ); - return castsshadow; -} - -// the order of the recorded draws for multi draws. draws go in one multi draw when they follow one another with the same -// state (material and its texture variant, geometry bank, primitive, blending; Indirect_Mergeable()). the draws of one entry, -// the walk of one submodel tree in one lod band, keep their order: submodels of a model may lie on one another and rely on -// the later one being drawn over the earlier one. the draws of different entries are drawn for different instances, which -// the gpu draws in no particular order anyway, so they're free to move. the entries are queues of draws; the draws at the -// heads of the queues which have the state taken next all go, each queue as far as its draws keep that state, then the -// queues wait for the state of their next draw. the states go in the order in which the queues first reach them -void opengl46_renderer::Order_Indirect_Draws() -{ - auto const count { m_indirectdraws.size() }; - if( count < 2 ) { return; } - - // a number for each state the draws have, by sorting the states (the buffers are kept between frames) - m_indirectstatekeys.clear(); - // the sun shadows draw with the program and the texture of the alpha test, whatever the material - auto const shadows { m_renderpass.draw_mode == rendermode::shadows }; - for( std::size_t index = 0; index < count; ++index ) { - auto const &draw { m_indirectdraws[ index ] }; - if( shadows ) { - m_indirectstatekeys.push_back( { Indirect_Shadow_Texture( draw.material ), 0u, draw.group, draw.type, m_indirectcommands[ index ].indexed, draw.blending, static_cast( index ) } ); - continue; - } - auto variant { 0u }; - if( draw.material != null_handle ) { - auto const &material { m_materials.material( draw.material ) }; - if( false == material.texture_variants.empty() ) { - variant = TSubModel::VariantId( draw.submodel ) % static_cast( material.texture_variants.size() ); - } - } - m_indirectstatekeys.push_back( { draw.material, variant, draw.group, draw.type, m_indirectcommands[ index ].indexed, draw.blending, static_cast( index ) } ); - } - std::sort( - std::begin( m_indirectstatekeys ), std::end( m_indirectstatekeys ), - []( indirect_state_key const &Left, indirect_state_key const &Right ) { - return Left.tie() < Right.tie(); } ); - m_indirectdrawstates.resize( count ); - std::uint32_t statecount { 0 }; - for( std::size_t index = 0; index < count; ++index ) { - if( ( index > 0 ) && ( m_indirectstatekeys[ index ].tie() != m_indirectstatekeys[ index - 1 ].tie() ) ) { - ++statecount; - } - m_indirectdrawstates[ m_indirectstatekeys[ index ].draw ] = statecount; - } - ++statecount; - if( statecount == count ) { - // no two draws share a state, nothing would go together - return; - } - - auto constexpr none { std::numeric_limits::max() }; - m_indirectstates.assign( statecount, indirect_state { none, none } ); - m_indirectstateorder.clear(); - m_indirectqueues.clear(); - // the queue waits for the state of its next draw, behind the queues waiting for it already - auto const enqueue = [ this, none ]( std::uint32_t const Queue ) { - auto &queue { m_indirectqueues[ Queue ] }; - auto &state { m_indirectstates[ m_indirectdrawstates[ queue.next ] ] }; - queue.link = none; - if( state.head == none ) { - state.head = Queue; - m_indirectstateorder.push_back( m_indirectdrawstates[ queue.next ] ); - } - else { - m_indirectqueues[ state.tail ].link = Queue; - } - state.tail = Queue; - }; - for( std::size_t index = 0; index < count; ) { - auto end { index + 1 }; - while( ( end < count ) && ( m_indirectcommands[ end ].entry == m_indirectcommands[ index ].entry ) ) { ++end; } - m_indirectqueues.push_back( { static_cast( index ), static_cast( end ), none } ); - index = end; - } - for( std::uint32_t queue = 0; queue < m_indirectqueues.size(); ++queue ) { - enqueue( queue ); - } - m_indirectorder.clear(); - for( std::size_t taken = 0; taken < m_indirectstateorder.size(); ++taken ) { - auto const stateindex { m_indirectstateorder[ taken ] }; - auto queueindex { m_indirectstates[ stateindex ].head }; - m_indirectstates[ stateindex ] = { none, none }; - while( queueindex != none ) { - auto &queue { m_indirectqueues[ queueindex ] }; - auto const nextqueue { queue.link }; - while( ( queue.next < queue.end ) && ( m_indirectdrawstates[ queue.next ] == stateindex ) ) { - m_indirectorder.push_back( queue.next ); - ++queue.next; - } - if( queue.next < queue.end ) { - enqueue( queueindex ); - } - queueindex = nextqueue; - } - } - - m_indirectdrawsordered.clear(); - m_indirectcommandsordered.clear(); - for( auto const index : m_indirectorder ) { - m_indirectdrawsordered.push_back( m_indirectdraws[ index ] ); - m_indirectcommandsordered.push_back( m_indirectcommands[ index ] ); - } - m_indirectdraws.swap( m_indirectdrawsordered ); - m_indirectcommands.swap( m_indirectcommandsordered ); -} - void opengl46_renderer::Upload_Indirect_Blocks() { m_indirectblocks.clear(); @@ -4318,24 +3873,6 @@ void opengl46_renderer::Bind_Indirect_Blocks( std::size_t const First, std::size ::glBindBuffersRange( GL_SHADER_STORAGE_BUFFER, gl::MODEL_DRAW_SSBO_BINDING, 1, &buffer, &offset, &size ); } -bool opengl46_renderer::Indirect_Mergeable( std::size_t const First, std::size_t const Other ) const -{ - auto const &first { m_indirectdraws[ First ] }; - auto const &other { m_indirectdraws[ Other ] }; - if( ( first.material != other.material ) - || ( first.blending != other.blending ) - || ( first.type != other.type ) - || ( first.group != other.group ) - || ( m_indirectcommands[ First ].indexed != m_indirectcommands[ Other ].indexed ) ) { - return false; - } - if( first.material == null_handle ) { return true; } - // the texture set Bind_Material() binds - auto const &material { m_materials.material( first.material ) }; - if( material.texture_variants.empty() ) { return true; } - return ( &material.GetTextures( TSubModel::VariantId( first.submodel ) ) == &material.GetTextures( TSubModel::VariantId( other.submodel ) ) ); -} - gl::program const *opengl46_renderer::Indirect_Program( material_handle Material ) const { // the same choice as Bind_Material() makes: what can't be drawn is drawn with the material of failures, if that one can @@ -5143,71 +4680,6 @@ std::vector const &opengl46_renderer::Model_Textures(TModel3d *M return textures; } -namespace { - -// the group of the sleepers of a track in the gpu culling: their model, and the skin in the slot Render_Sleepers() sets it in -scene::basic_cell::instance_bucket_key sleeper_key( TTrack const &Track ) { - scene::basic_cell::instance_bucket_key key; - key.pModel = Track.m_sleeper_model; - key.skins[ 1 ] = Track.m_sleeper_skin; - return key; -} - -} // namespace - -void opengl46_renderer::Queue_Sleepers( TTrack *Track ) -{ - // the gates of Render_Sleepers(); the range is tested for the whole track - if( ( Track->m_sleeper_model == nullptr ) || Track->m_sleeper_local_transforms.empty() || ( Global.SleeperDistance <= 0.f ) ) { return; } - auto const distsq { glm::length2( Track->location() - m_renderpass.pass_camera.position() ) }; - auto const cutoffsq { static_cast( Global.SleeperDistance ) * static_cast( Global.SleeperDistance ) }; - if( distsq > cutoffsq ) { return; } - - auto &records { m_sleeperrecords[ Track ] }; - auto const &transforms { Track->m_sleeper_local_transforms }; - if( ( records.bucket.recordsversion != m_instancerecordsversion ) - || ( records.model != Track->m_sleeper_model ) - || ( records.skin != Track->m_sleeper_skin ) - || ( records.transforms != transforms.data() ) - || ( records.count != transforms.size() ) ) { - // each sleeper is a record placed and turned by its transform, relative to the origin of the track (the centre of its - // cell), with the sphere Render_Sleepers() tests against the frustum - auto const radius { std::max( Track->m_sleeper_model->bounding_radius(), 2.0f ) }; - auto &bucket { records.bucket }; - bucket.records.clear(); - bucket.records.reserve( transforms.size() ); - glm::vec3 low { std::numeric_limits::max() }; - glm::vec3 high { std::numeric_limits::lowest() }; - for( auto const &local : transforms ) { - scene::basic_cell::instance_record record; - glm::vec3 const position { local[ 3 ] }; - record.position = packed_vec3{ position }; - record.radius = radius; - record.rangesquaredmin = 0.f; - record.rangesquaredmax = std::numeric_limits::max(); - record.flags = scene::basic_cell::instance_visible | scene::basic_cell::instance_instanceable; - for( int column = 0; column < 3; ++column ) { - record.rotationscale[ column ] = local[ column ]; - } - bucket.records.emplace_back( record ); - low = glm::min( low, position ); - high = glm::max( high, position ); - } - bucket.recordorigin = Track->m_origin; - bucket.lodrangesquared = std::numeric_limits::max(); - bucket.recordsversion = m_instancerecordsversion; - bucket.gpustale = true; - records.key = sleeper_key( *Track ); - records.area = scene::bounding_area{ Track->m_origin + glm::dvec3{ ( low + high ) * 0.5f }, 0.5f * glm::length( high - low ) }; - records.model = Track->m_sleeper_model; - records.skin = Track->m_sleeper_skin; - records.transforms = transforms.data(); - records.count = transforms.size(); - } - records.frame = m_framestamp; - m_passcull->add_sleepers( records.key, records.bucket, records.area ); -} - // the draws of the shared walk of the scene (gfx::scene_walk), made at once void opengl46_renderer::walk_material(material_handle const Material, TSubModel const *Submodel, lighting_data const *Lighting) @@ -5359,20 +4831,19 @@ void opengl46_renderer::walk_region_begin() // the gpu culling runs along the first color pass of the main viewport of a frame, and the passes of the cascades of the // sun shadows, of the faces of the environment map and of the maps of headlights and lamps drawn for it, each with a culling // of its own. its result is drawn (Global.gfx_gpucull 1), or compared with the one of the cpu, which draws (2) - m_instancecullpass = false; - m_passcull = nullptr; + opengl46_instance_cull *passcull { nullptr }; if( ( Global.gfx_gpucull != 0 ) && ( m_current_viewport != nullptr ) && ( m_current_viewport->main ) ) { if( ( m_renderpass.draw_mode == rendermode::color ) && ( m_instancecullframe != m_framestamp ) ) { m_instancecullframe = m_framestamp; - m_passcull = &m_instancecull; + passcull = &m_instancecull; } else if( ( m_renderpass.draw_mode == rendermode::shadows ) && ( m_lightshadowcull != nullptr ) ) { // the map of a set of headlights or the scenery part of the map of a lamp, which picked its culling - m_passcull = m_lightshadowcull; + passcull = m_lightshadowcull; m_shadowcasters.clear(); } else if( ( m_renderpass.draw_mode == rendermode::shadows ) @@ -5381,41 +4852,16 @@ void opengl46_renderer::walk_region_begin() && ( m_shadowcascade < static_cast( m_shadowcull.size() ) ) && ( false == m_cubeshadows ) ) { // the cascades of the sun - m_passcull = &m_shadowcull[ m_shadowcascade ]; + passcull = &m_shadowcull[ m_shadowcascade ]; m_shadowcasters.clear(); } else if( ( m_renderpass.draw_mode == rendermode::reflections ) && ( Global.gfx_gpucull_reflections ) && ( Global.reflectiontune.fidelity >= 1 ) ) { - m_passcull = &m_reflectioncull; + passcull = &m_reflectioncull; } } - if( m_passcull != nullptr ) { - m_instancecullpass = true; - // the same choices as in Render_Instanced() - auto const reflections { m_renderpass.draw_mode == rendermode::reflections }; - opengl46_instance_cull::pass_setup setup; - setup.frame = m_framestamp; - setup.camera = m_renderpass.pass_camera.position(); - setup.distancecamera = ( m_renderpass.draw_mode == rendermode::shadows ? m_renderpass.viewport_camera.position() : m_renderpass.pass_camera.position() ); - setup.frustum = &m_renderpass.pass_camera.frustum(); - setup.distancescale = ( reflections ? 1.f : static_cast( 1.0 / ( static_cast( Global.ZoomFactor ) * static_cast( Global.ZoomFactor ) * static_cast( Global.fDistanceFactor ) ) ) ); - setup.distanceoffset = ( reflections ? static_cast( sq( EU07_REFLECTIONFIDELITYOFFSET ) ) : 0.f ); - setup.distancerange = ( reflections ? static_cast( sq( Global.reflectiontune.range_instances ) ) : std::numeric_limits::max() ); - setup.drawrange = static_cast( sq( m_renderpass.draw_range + 250 ) ); - setup.zoom = Global.ZoomFactor; - setup.drawing = ( Global.gfx_gpucull == 1 ); - m_passcull->begin_pass( setup ); - } - // the sleepers of tracks go with the instances of the main view - m_sleeperspass = ( ( m_passcull == &m_instancecull ) && Global.gfx_gpucull_sleepers ); - if( m_sleeperspass && ( false == m_passcull->drawing() ) ) { - for( auto &counts : m_sleeperbandcounts ) { - counts.second.clear(); - } - } - // the models drawn on their own go with the instances of every pass culled on the gpu - m_modelspass = ( m_instancecullpass && Global.gfx_gpucull_models ); + Begin_Pass_Cull( passcull, ( passcull == &m_instancecull ) ); } void opengl46_renderer::walk_editor_cursor() @@ -5432,70 +4878,6 @@ void opengl46_renderer::walk_editor_cursor() } } -bool opengl46_renderer::walk_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, bool const Bucketrange, scene::bounding_area const &Area) -{ - if( m_instancecullpass ) { - // the gpu tests every instance of the bucket, the way Render_Instanced() does. models with nothing to draw in the - // sun shadows are left out of them, as Render_Instanced() leaves them - if( ( m_renderpass.draw_mode != rendermode::shadows ) || Instances_Cast_Shadow( Key, Bucket ) ) { - m_passcull->add_bucket( Key, Bucket, ( Bucketrange ? Bucket_Area( Bucket ) : Area ) ); - } - // and when the instances are drawn from its result, the cpu has nothing more to do with them - if( m_passcull->drawing() ) { return false; } - } - // the depth pass of the whole scene culled them already, the colour pass draws what it recorded - if( m_indirectcolourpending ) { return false; } - return true; -} - -void opengl46_renderer::walk_bucket_culled(scene::basic_cell::instance_bucket const &Bucket) -{ - if( m_instancecullpass ) { - m_passcull->skip_bucket( Bucket ); - } -} - -bool opengl46_renderer::walk_queue_model(TAnimModel *Instance, bool const Inrange) -{ - return Queue_Model( Instance, Inrange ); -} - -bool opengl46_renderer::walk_queue_sleepers(TTrack *Track) -{ - if( m_sleeperspass ) { - // culled with the instances of the pass and drawn from the result, or drawn by the cpu as well to compare - Queue_Sleepers( Track ); - if( m_passcull->drawing() ) { return true; } - } - else if( m_indirectcolourpending && m_indirectcoloursleepers ) { - // the depth pass of the whole scene culled and recorded them, the colour pass draws what it recorded - return true; - } - return false; -} - -void opengl46_renderer::walk_sleepers_drawn(TTrack const &Track, std::vector const &Lodbounds, std::vector> const &Drawn) -{ - if( m_sleeperspass && m_instancecullpass && ( false == m_passcull->drawing() ) ) { - // the comparison with the gpu culling: the sleepers drawn by lod band, the bands as the culling makes them, split by - // the bounds of the model past 0 and short of the largest float - auto const counted = []( float const Bound ) { - return ( Bound > 0.f ) && ( Bound < std::numeric_limits::max() ); }; - auto &counts { m_sleeperbandcounts[ sleeper_key( Track ) ] }; - auto const bands { static_cast( std::count_if( Lodbounds.begin(), Lodbounds.end(), counted ) ) + 1 }; - if( counts.size() < bands ) { - counts.resize( bands, 0 ); - } - for( auto const &survivor : Drawn ) { - std::size_t band { 0 }; - for( auto const bound : Lodbounds ) { - if( counted( bound ) && ( bound <= survivor.first ) ) { ++band; } - } - ++counts[ band ]; - } - } -} - bool opengl46_renderer::walk_instances_begin() { if( m_indirectcolourpending ) { @@ -5514,38 +4896,27 @@ bool opengl46_renderer::walk_instances_begin() } else { // the view of the pass is in place here, the one Render_Instanced() takes - m_passcull->dispatch( OpenGLMatrices.data( GL_MODELVIEW ) ); - // the sleepers the cpu drew along the tracks, before the culling ran - if( m_sleeperspass ) { - for( auto const &counts : m_sleeperbandcounts ) { - if( false == counts.second.empty() ) { - m_passcull->cpu_result( counts.first, counts.second, true ); - } - } - } - // and the models drawn on their own - for( auto const &model : m_modelcpudistances ) { - m_passcull->cpu_instance( model.first, model.second ); - } + Pass_Cull().dispatch( OpenGLMatrices.data( GL_MODELVIEW ) ); + // what the cpu drew of the sleepers and the models drawn on their own + Compare_Pass_Cull(); } } return m_instancecullpass; } -void opengl46_renderer::walk_instances_compare(scene::basic_cell::instance_bucket_key const &Key, std::vector const &Bandcounts) -{ - m_passcull->cpu_result( Key, Bandcounts ); -} - void opengl46_renderer::walk_instances_end() { - if( m_instancecullpass ) { - m_passcull->end_pass(); - m_instancecullpass = false; - m_passcull = nullptr; - } - m_sleeperspass = false; - m_modelspass = false; + End_Pass_Cull(); +} + +gfx::geometrybank_manager &opengl46_renderer::walk_geometry() +{ + return m_geometry; +} + +bool opengl46_renderer::walk_blending_enabled() const +{ + return m_blendingenabled; } @@ -6730,23 +6101,9 @@ void opengl46_renderer::Render_lamp_shadows() return; // vehicles near the camera cast the shadows of the lamps, and the occupied cab - m_lampshadowvehicles.clear(); + m_localshadows.lamp_vehicles(Global.pCamera.Pos); auto const vehiclesrange{static_cast(Global.gfx_lights_shadows_vehicles_range)}; - if (vehiclesrange > 0.0) - { - for (auto *vehicle : simulation::Vehicles.sequence()) - { - if (glm::length(vehicle->GetPosition() - Global.pCamera.Pos) <= vehiclesrange + vehicle->radius()) - m_lampshadowvehicles.emplace_back(vehicle); - } - } - TDynamicObject *cab{nullptr}; - if ((vehiclesrange > 0.0) && (false == FreeFlyModeFlag) && Global.render_cab && (simulation::Train != nullptr)) - { - cab = simulation::Train->Dynamic(); - if ((cab != nullptr) && ((false == cab->bDisplayCab) || (cab->mdKabina == nullptr) || (cab->mdKabina == cab->mdModel))) - cab = nullptr; - } + auto *cab{vehiclesrange > 0.0 ? m_localshadows.shadow_cab() : nullptr}; auto timing{false}; for (auto const idx : lamps) @@ -6764,14 +6121,9 @@ void opengl46_renderer::Render_lamp_shadows() if (false == slot.scenery) continue; // the vehicles by the lamp, within its reach - m_lampshadowslot.clear(); - for (auto *vehicle : m_lampshadowvehicles) - { - if (glm::length(vehicle->GetPosition() - slot.position) <= slot.range + vehicle->radius()) - m_lampshadowslot.emplace_back(vehicle); - } + auto const &slotvehicles{m_localshadows.slot_vehicles(slot)}; auto *slotcab{(cab != nullptr) && (glm::length(cab->GetPosition() - slot.position) <= slot.range + cab->radius()) ? cab : nullptr}; - if (m_lampshadowslot.empty() && (slotcab == nullptr)) + if (slotvehicles.empty() && (slotcab == nullptr)) { m_localshadows.no_vehicles(idx); continue; @@ -6812,7 +6164,7 @@ void opengl46_renderer::Render_lamp_shadow(std::size_t const Slot, bool const Ve m_cubeshadows = true; if (Vehicles) { - for (auto *vehicle : m_lampshadowslot) + for (auto *vehicle : m_localshadows.slot_vehicles()) { Render(vehicle); } @@ -6864,43 +6216,8 @@ void opengl46_renderer::Render_headlight_shadows() { // the headlights which shine, the most important first (in the order of Update_Lights()), whose beam may be seen within the // reach of the shadows - m_headlightshadowpicked.clear(); - auto const count{(Global.gfx_lights_headlights && (std::clamp(Global.gfx_lights_shadows, 0, 3) > 0)) ? std::clamp(Global.gfx_lights_shadows_headlights, 0, static_cast(gl::MAX_HEADLIGHT_SHADOWS)) : 0}; - if (count > 0) - { - m_headlightshadowcandidates.clear(); - auto const camera{m_colorpass.pass_camera.position()}; - auto const range{static_cast(Global.gfx_lights_shadows_headlights_range)}; - for (auto const &light : simulation::Lights.data) - { - if (light.intensity == 0.f) - continue; - // as in Update_Lights(): by day the light around takes the beam down to nothing - auto luminance{static_cast(Global.fLuminance)}; - if (light.owner->fShade > 0.f) - luminance *= light.owner->fShade; - auto const beam{light.color - glm::vec3{luminance}}; - if (std::max(beam.r, std::max(beam.g, beam.b)) <= 0.f) - continue; - // the part of the beam the shadows are cast in: from the end of the vehicle to the reach ahead of it - auto const &dimensions{light.owner->MoverParameters->Dim}; - auto const radius{range * 0.5 + std::max(dimensions.W, dimensions.H) + 2.0}; - auto const centre{light.position + glm::dvec3{light.direction} * (range * 0.5 + 2.0)}; - if (glm::length(centre - camera) - radius > range) - continue; - if (false == m_colorpass.pass_camera.visible(scene::bounding_area{centre, static_cast(radius)})) - continue; - m_headlightshadowcandidates.emplace_back(static_cast(glm::length2(camera - light.position)) / light.intensity, &light); - } - auto const picked{std::min(m_headlightshadowcandidates.size(), static_cast(count))}; - std::partial_sort(std::begin(m_headlightshadowcandidates), std::begin(m_headlightshadowcandidates) + picked, std::end(m_headlightshadowcandidates), - [](auto const &Left, auto const &Right) { return Left.first < Right.first; }); - for (std::size_t idx = 0; idx < picked; ++idx) - { - m_headlightshadowpicked.emplace_back(m_headlightshadowcandidates[idx].second); - } - } - m_localshadows.update_headlights(m_headlightshadowpicked, m_framestamp); + auto const &picked{m_localshadows.pick_headlights(simulation::Lights, m_colorpass.pass_camera)}; + m_localshadows.update_headlights(picked, m_framestamp); // the maps may have been made anew, in place of the ones bound for this frame m_localshadows.bind(); auto const &slots{m_localshadows.headlights()}; @@ -6908,23 +6225,17 @@ void opengl46_renderer::Render_headlight_shadows() return; // the occupied cab, as in Render_lamp_shadows() - TDynamicObject *cab{nullptr}; - if ((false == FreeFlyModeFlag) && Global.render_cab && (simulation::Train != nullptr)) - { - cab = simulation::Train->Dynamic(); - if ((cab != nullptr) && ((false == cab->bDisplayCab) || (cab->mdKabina == nullptr) || (cab->mdKabina == cab->mdModel))) - cab = nullptr; - } + auto *cab{m_localshadows.shadow_cab()}; auto timing{false}; for (auto const idx : slots) { auto const &slot{m_localshadows.headlight(idx)}; light_array::light_record const *light{nullptr}; - for (auto const *picked : m_headlightshadowpicked) + for (auto const *candidate : picked) { - if ((picked->owner == slot.owner) && (picked->index == slot.index)) + if ((candidate->owner == slot.owner) && (candidate->index == slot.index)) { - light = picked; + light = candidate; break; } } @@ -7115,154 +6426,35 @@ bool opengl46_renderer::Resolved_Depth_Target(viewport_config &vp) glm::vec3 opengl46_renderer::Shadow_Light_Vector() const { - // cap the vertical angle to keep shadows from getting too long - return glm::normalize(glm::vec3{m_sunlight.direction.x, std::min(m_sunlight.direction.y, Global.gfx_shadow_angle_min), m_sunlight.direction.z}); + return gfx::shadow_fit::light_vector(m_sunlight.direction); } glm::mat3 opengl46_renderer::Shadow_Light_Rotation() const { - // the rotation of the view of the shadow pass (setup_pass()) - return glm::mat3{glm::lookAt(glm::vec3{0.f}, Shadow_Light_Vector(), glm::vec3{0.f, 1.f, 0.f})}; + return gfx::shadow_fit::light_rotation(m_sunlight.direction); } std::array opengl46_renderer::Shadow_Fixed_Splits() const { - static_assert(gl::MAX_CASCADES == 3, "the fixed cascades are made for 3 of them"); - std::array splits{0.0f, Global.shadowtune.range / 32, Global.shadowtune.range / 8, Global.shadowtune.range}; - if (Global.shadowtune.map_size > 2048) - { - // increase coverage if our shadow map is large enough to produce decent results - for (std::size_t idx = 1; idx < splits.size(); ++idx) - splits[idx] *= Global.shadowtune.map_size / 2048; - } - return splits; + return gfx::shadow_fit::fixed_splits(); } char const *opengl46_renderer::Shadow_Fit_Unavailable(viewport_config const &Viewport) const { - if (false == Global.gfx_shadowmap_enabled) - return "no shadow maps"; - if (Global.bWireFrame) - return "wireframe"; - // the shadows follow the camera, the view shows another one - if (DebugCameraFlag) - return "debug camera"; - if (Global.gfx_skippipeline || (Viewport.msaa_fb == nullptr)) - return "no post processing"; - if (EditorModeFlag && Global.EditorOrtho) - return "orthographic view"; auto const shadowviews{std::count_if(m_viewports.begin(), m_viewports.end(), [](auto const &View) { return View->shadow; })}; - if ((false == Viewport.shadow) || (shadowviews != 1)) - return "the shadows cover more than one view"; - return nullptr; + return gfx::shadow_fit::unavailable((false == Global.gfx_skippipeline) && (Viewport.msaa_fb != nullptr), Viewport.shadow, static_cast(shadowviews)); } bool opengl46_renderer::Shadow_Fit(viewport_config const &Viewport, std::array &Splits, std::array>, gl::MAX_CASCADES> &Areas) { - // a measurement older than this many frames, made with the camera this many metres away or the view turned this many - // degrees from the one now, isn't used - constexpr unsigned int AGE_MAX{6}; - constexpr float SHIFT_MAX{25.f}; - constexpr float TURN_MAX{20.f}; - // metres an area grows by on each side: the vehicles move in the frames since the measurement too - constexpr float MARGIN{0.25f}; - - auto &state{m_shadowfitstate}; - state = shadow_fit_state{}; - if (false == Global.gfx_shadow_sdsm) - return false; - if ((state.reason = Shadow_Fit_Unavailable(Viewport)) != nullptr) - return false; - auto const *latest{m_shadowfit.latest()}; - if (latest == nullptr) + auto const *unavailable{Global.gfx_shadow_sdsm ? Shadow_Fit_Unavailable(Viewport) : nullptr}; + if (Global.gfx_shadow_sdsm && (unavailable == nullptr)) { - state.reason = "no measurement yet"; - return false; + // takes in what came back + m_shadowfit.latest(); } - auto const &data{latest->data}; - auto const &source{latest->source}; - state.age = m_framestamp - latest->frame; - state.depthmin = data.depthmin; - state.depthmax = data.depthmax; - if (state.age > AGE_MAX) - { - state.reason = "the measurement is too old"; - return false; - } - if (data.samples == 0) - { - state.reason = "only the sky in the view"; - return false; - } - if (source.range != Shadow_Fixed_Splits().back()) - { - state.reason = "the range of the shadows changed"; - return false; - } - // the shadows of this frame are made for its colour pass - auto const &camera{m_colorpass.pass_camera}; - auto const shift{glm::vec3{source.camera - camera.position()}}; - if (glm::length(shift) > SHIFT_MAX) - { - state.reason = "the camera jumped"; - return false; - } - auto const turnmatrix{glm::mat3{camera.modelview()} * glm::transpose(source.rotation)}; - auto const turn{std::acos(std::clamp((turnmatrix[0][0] + turnmatrix[1][1] + turnmatrix[2][2] - 1.f) * 0.5f, -1.f, 1.f))}; - state.turn = glm::degrees(turn); - if (state.turn > TURN_MAX) - { - state.reason = "the view turned too fast"; - return false; - } - auto const &projection{camera.projection()}; - if ((std::abs(projection[0][0] - source.projection[0][0]) > 0.02f * std::abs(projection[0][0])) - || (std::abs(projection[1][1] - source.projection[1][1]) > 0.02f * std::abs(projection[1][1]))) - { - state.reason = "the field of view changed"; - return false; - } - - // the first cascade from the camera, like the fixed ones. setup_pass() takes distances up to 1 m as shares of the far - // end, so the others start past that - Splits[0] = 0.f; - for (std::size_t split = 1; split < Splits.size(); ++split) - Splits[split] = std::max(data.splits[split], Splits[split - 1] + 1.5f); - if (false == std::isfinite(Splits.back())) - { - state.reason = "invalid measurement"; - return false; - } - - auto const inverselight{glm::transpose(source.light)}; - // the scenery stays where it was while the camera moved, the cab and the train the camera rides with moved along. an area - // covers both - auto const lightshift{source.light * shift}; - auto const chord{2.f * std::sin(turn * 0.5f)}; - for (std::size_t cascade = 0; cascade < gl::MAX_CASCADES; ++cascade) - { - // with nothing of the view in a cascade, its map covers the whole slice - if (data.counts[cascade] == 0) - continue; - auto const *bounds{&data.bounds[cascade * 8]}; - glm::vec3 low{opengl46_shadow_fit::unorder(bounds[0]), opengl46_shadow_fit::unorder(bounds[1]), opengl46_shadow_fit::unorder(bounds[2])}; - glm::vec3 high{opengl46_shadow_fit::unorder(bounds[4]), opengl46_shadow_fit::unorder(bounds[5]), opengl46_shadow_fit::unorder(bounds[6])}; - if ((false == std::isfinite(low.x + low.y + high.x + high.y)) || (low.x > high.x) || (low.y > high.y)) - continue; - low = glm::min(low, low + lightshift); - high = glm::max(high, high + lightshift); - // and what came into the view as it turned, out to the reach of the cascade - auto const reach{Splits[cascade + 1] + static_cast(gl::MAX_CASCADES - cascade)}; - auto const margin{reach * chord + MARGIN + 0.01f * std::max(high.x - low.x, high.y - low.y)}; - low -= glm::vec3{margin}; - high += glm::vec3{margin}; - auto &area{Areas[cascade].emplace()}; - for (int corner = 0; corner < 8; ++corner) - area[corner] = inverselight * glm::vec3{(corner & 1) ? high.x : low.x, (corner & 2) ? high.y : low.y, (corner & 4) ? high.z : low.z}; - } - state.fitted = true; - return true; + return m_shadowfit.fit(m_framestamp, m_colorpass.pass_camera, unavailable, Splits, Areas, m_shadowfitstate); } void opengl46_renderer::Render_cluster_heatmap(viewport_config &vp, gl::framebuffer *Target, glm::ivec2 const &Targetsize) diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index acd864ea..79175a91 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -216,14 +216,10 @@ class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { // the gpu culling of the instances, models and sleepers, the depth pass of the whole scene void walk_region_begin() override; void walk_editor_cursor() override; - bool walk_bucket(scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket, bool const Bucketrange, scene::bounding_area const &Area) override; - void walk_bucket_culled(scene::basic_cell::instance_bucket const &Bucket) override; - bool walk_queue_model(TAnimModel *Instance, bool const Inrange) override; - bool walk_queue_sleepers(TTrack *Track) override; - void walk_sleepers_drawn(TTrack const &Track, std::vector const &Lodbounds, std::vector> const &Drawn) override; bool walk_instances_begin() override; - void walk_instances_compare(scene::basic_cell::instance_bucket_key const &Key, std::vector const &Bandcounts) override; void walk_instances_end() override; + gfx::geometrybank_manager &walk_geometry() override; + bool walk_blending_enabled() const override; // types struct viewport_config { @@ -346,11 +342,11 @@ class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { // materials like the main view. no occlusion test, no depth prepass, one phase void Render_Instances_Indirect_Pass(); // the walk of the submodel trees of the groups culled by Cull, once per lod band, which records their draws - // (m_indirectdraws, m_indirectcommands), puts them in order for multi draws and has Cull fill in their commands + // (m_indirectdraws, m_indirectcommands; Walk_Indirect_Draws()), puts them in order for multi draws and has Cull fill in their + // commands void Record_Indirect_Draws( opengl46_instance_cull &Cull ); - // whether a model with its skins has any part the sun shadows draw: the same test Render_Instanced() makes. the answer - // is kept for the rest of the pass - bool Instances_Cast_Shadow( scene::basic_cell::instance_bucket_key const &Key, scene::basic_cell::instance_bucket const &Bucket ); + // the culling the pass under way feeds (Begin_Pass_Cull()), always one of this renderer + opengl46_instance_cull &Pass_Cull() { return static_cast( *m_passcull ); } // draws of the instances made, and the gl calls which made them struct indirect_counts { std::uint32_t draws { 0 }; @@ -369,18 +365,11 @@ class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { indirect_counts Render_Instances_Indirect_Colour( opengl46_instance_cull::command_set const Set ); // the recorded draws of a shadow pass with the shadow programs, the way Bind_Material_Shadow() picks them indirect_counts Render_Instances_Indirect_Shadow( opengl46_instance_cull::command_set const Set ); - // the texture Bind_Material_Shadow() binds for the alpha test of a material, or null_handle (no test) - texture_handle Indirect_Shadow_Texture( material_handle const Material ); - // multi draws of the recorded draws (Global.gfx_gpucull_multidraw): puts the draws in an order in which the ones with the - // same state follow one another, the draws of one entry kept in the order of the walk, then places their model blocks in - // the buffer of the frame (m_drawblockstream), in the same order - void Order_Indirect_Draws(); + // multi draws of the recorded draws (Global.gfx_gpucull_multidraw), put in order by Order_Indirect_Draws(): places their + // model blocks in the buffer of the frame (m_drawblockstream), in the same order void Upload_Indirect_Blocks(); // binds the model blocks of Count recorded draws from First for the programs of the multi draws (MODEL_DRAW_SSBO_BINDING) void Bind_Indirect_Blocks( std::size_t const First, std::size_t const Count ); - // whether two recorded draws can go in one multi draw of the colour pass: the same material and texture variant, geometry - // bank, primitive and blending - bool Indirect_Mergeable( std::size_t const First, std::size_t const Other ) const; // the program Bind_Material() binds for the material, nullptr when it binds none gl::program const *Indirect_Program( material_handle Material ) const; // the variant of a material program for multi draws (built with MODEL_DRAWS), made the first time it's asked for. @@ -431,14 +420,6 @@ class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { std::array>, gl::MAX_CASCADES> &Areas ); // whether the depth pass of the whole scene draws the material bound last (Bind_Material()) and the geometry bool Scene_Depth_Draws( gfx::geometry_handle const &Geometry ) const; - // queues the sleepers of a track in the gpu culling of the pass (Global.gfx_gpucull_sleepers), with the gates of - // Render_Sleepers(), making their records first when they're missing or out of date - void Queue_Sleepers( TTrack *Track ); - // queues a model instance drawn on its own in the gpu culling of the pass (Global.gfx_gpucull_models), as a bucket of - // its own, making its record first when it's missing or out of date. Inrange: the cell of the instance lies within the - // range of the pass. returns true when the instance isn't drawn by the cpu in the pass: it's drawn from the result of the - // culling, or it was culled and recorded by the depth pass of the whole scene, or it lies out of range - bool Queue_Model( TAnimModel *Instance, bool const Inrange ); // releases gl textures used only by vehicles farther than gfx.textures.releasedistance times the draw range void Update_Texture_Release(); // textures used by materials of the sub-models of specified model, replaceable skins excluded @@ -566,7 +547,6 @@ class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { // when Global.gfx_gpucull is set: the instances of the pass are drawn from its result (1), or the result is compared // with the one of the cpu (2) opengl46_instance_cull m_instancecull; - bool m_instancecullpass { false }; // the current pass feeds a culling: m_passcull unsigned int m_instancecullframe { ~0u }; // frame it last ran in // the cullings of the cascades of the sun shadows and of the faces of the environment map (Global.gfx_gpucull_shadows, // _reflections), with records and programs shared with m_instancecull @@ -578,42 +558,8 @@ class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { std::vector> m_lampcull; opengl46_instance_cull *m_lightshadowcull { nullptr }; // the one of the map of headlights or of a lamp being drawn std::size_t m_lampscenerydrawn { 0 }; // scenery parts of the maps of lamps drawn in this frame so far - opengl46_instance_cull *m_passcull { nullptr }; // the one the current pass feeds opengl46_instance_cull *m_indirectcull { &m_instancecull }; // the one the recorded draws (m_indirectdraws) were culled by int m_shadowcascade { -1 }; // the region is drawn for this cascade of the sun shadows - // Instances_Cast_Shadow() of the groups of the shadow pass under way - std::unordered_map m_shadowcasters; - // the sleepers of tracks culled on the gpu with the instances of the main view (Global.gfx_gpucull_sleepers): records made - // from TTrack::m_sleeper_local_transforms when the track is queued first, and again when they or the instance records - // (editor, m_instancerecordsversion) went out of date. the records of tracks not queued for a while are let go - struct sleeper_records { - scene::basic_cell::instance_bucket bucket; // the records only, no instances - scene::basic_cell::instance_bucket_key key; // the model, and the skin in the slot Render_Sleepers() sets it in - scene::bounding_area area; // of the locations of the sleepers - TModel3d const *model { nullptr }; // what the records were made from - material_handle skin { null_handle }; - glm::mat4 const *transforms { nullptr }; - std::size_t count { 0 }; - unsigned int frame { 0 }; // queued last - }; - std::unordered_map m_sleeperrecords; - // the model instances drawn on their own culled on the gpu (Global.gfx_gpucull_models), each one a bucket of its own with - // a group of its own: the record made when the instance is queued first, and again when the instance records went out of - // date (editor, events: m_instancerecordsversion). the records of instances not queued for a while are let go - struct model_records { - scene::basic_cell::instance_bucket bucket; // the instance alone - scene::basic_cell::instance_bucket_key key; // the model, the skins and the instance - scene::bounding_area area; // the location of the instance - unsigned int frame { 0 }; // queued last - }; - std::unordered_map m_modelrecords; - bool m_modelspass { false }; // the models drawn on their own of the pass under way go to its gpu culling - bool m_indirectcolourmodels { false }; // with m_indirectcolourpending: the depth pass culled and recorded them too - bool m_sleeperspass { false }; // the sleepers of the pass under way go to its gpu culling (the one of the main view) - bool m_indirectcoloursleepers { false }; // with m_indirectcolourpending: the depth pass culled and recorded the sleepers too - // counts per lod band of the sleepers Render_Sleepers() drew in the pass, by model and skin, for the comparison (they're - // drawn before the culling runs) - std::unordered_map, scene::basic_cell::instance_bucket_key_hash> m_sleeperbandcounts; // depth pyramid for the occlusion test of the gpu culling opengl46_hiz m_hiz; // the cascades of the sun shadows fitted to the depth of the main view (Global.gfx_shadow_sdsm) @@ -621,75 +567,10 @@ class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { // with a cascade fitted: the corners of the area its map is to cover, for setup_pass() std::array const *m_shadowfitarea { nullptr }; // what the last shadow pass did, for the debug panel - struct shadow_fit_state { - bool fitted { false }; - char const *reason { nullptr }; // why not, with gfx.shadow.sdsm on - unsigned int age { 0 }; // frames since the measurement - float depthmin { 0.f }; - float depthmax { 0.f }; - float turn { 0.f }; // of the view since the measurement, degrees - std::array splits {}; - // size of the area of the map of each cascade: of the whole slice of the view, and as drawn; metres - std::array slice {}; - std::array drawn {}; - } m_shadowfitstate; + gfx::shadow_fit::fit_state m_shadowfitstate; // the sizes setup_pass() found for the last shadow pass: of the whole slice of the view and as drawn float m_shadowslicesize { 0.f }; float m_shadowdrawnsize { 0.f }; - // a draw recorded from the walk of the submodel tree of a group in one lod band (Render_Instances_Indirect()), to be - // replayed once the instances of its command are known: the state the walk left for it and what it bound - struct indirect_draw { - gl::model_ubs ubs; - material_handle material; - TSubModel const *submodel; - gfx::geometry_handle geometry; - unsigned int type; // primitive - bool blending; // drawn with the transparency hack of submodels faded out - std::uint32_t group { 0 }; // the buffers of the geometry, which the draws of a multi draw share (geometrybank_manager::draw_group()) - }; - std::vector m_indirectdraws; - std::vector m_indirectcommands; // in the order of m_indirectdraws - // the walks Record_Indirect_Draws() made for the cascades of the sun shadows in the frame, for the other cascades to copy: the - // draws and commands recorded for a group (model and skins) in each of its lod bands, first none where no cascade walked it - struct cascade_band { - std::uint32_t first; - std::uint32_t count; - }; - struct cascade_record { - unsigned int frame { 0 }; - std::vector bands; - }; - std::unordered_map m_cascaderecords; - std::vector m_cascadedraws; - std::vector m_cascadecommands; - unsigned int m_cascaderecordframe { 0 }; - // the walks Record_Indirect_Draws() made in earlier frames (Global.gfx_gpucull_walkcache). what the walk of a group of instances - // in a lod band records depends on the model with its skins, the band and whether the pass draws shadows, not on the camera, - // so it's kept while the shared state of the model stays the same (TModel3d::state_version()) and used instead of walking the - // tree again. a walk meeting an animated submodel, one lit by the luminance or the stars isn't kept. the models drawn on their - // own (with an instance in the key) are walked every time - struct walk_band { - std::vector draws; // as recorded - float bandmin { 0.f }; - std::uint32_t modelversion { 0 }; - bool blending { false }; // the state the walk began in - bool valid { false }; - }; - struct walk_record { - unsigned int frame { 0 }; // used last - std::array, 2> bands; // of the colour passes (the main view, the environment map) and of the shadows - }; - std::unordered_map m_walkrecords; - int m_walkrankcutoff { 0 }; // Global.gfx_shadow_rank_cutoff the walks were made with - // what the walks of the frame did, for the panel: walks made, of them not kept, walks taken from earlier frames, and the cpu time - // of the walks and of putting the draws in order with their blocks uploaded - struct walk_counts { - std::uint32_t walked { 0 }; - std::uint32_t changing { 0 }; - std::uint32_t reused { 0 }; - double walktime { 0.0 }; - double ordertime { 0.0 }; - }; // cpu time of the parts of the culling and the draws of the instances of the main view (Render_Instances_Indirect() and // _Pending()), for the panel: the depth of the recorded walk of the scene drawn before the culling, the depth pyramids, the // culling passes, the walks with putting the draws in order, the depth and the colour of the instances @@ -702,47 +583,13 @@ class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { double colour { 0.0 }; }; cull_parts m_cullparts; - walk_counts m_walkcounts; - walk_counts m_lastwalkcounts; - unsigned int m_walkcountsframe { 0 }; - // appends the draws of a walk kept from an earlier frame to the recorded draws, with the commands of the entry under way - void Replay_Walk( walk_band const &Walk, bool const Shadows ); // multi draws of the recorded draws: whether they were recorded for them (ordered, their model blocks uploaded), and where // the blocks are bool m_indirectmultidraw { false }; GLuint m_indirectblockbuffer { 0 }; GLintptr m_indirectblockoffset { 0 }; std::uint64_t m_indirectblockgeneration { 0 }; - // working space of Order_Indirect_Draws() and Upload_Indirect_Blocks(), kept between frames - struct indirect_queue { - std::uint32_t next; // draw - std::uint32_t end; - std::uint32_t link; // next queue waiting for the same state - }; - struct indirect_state { - std::uint32_t head; - std::uint32_t tail; - }; - // what a multi draw has in common, and the draw which has it - struct indirect_state_key { - material_handle material; - std::uint32_t variant; // of the textures of the material - std::uint32_t bank; - unsigned int type; - std::uint32_t indexed; - bool blending; - std::uint32_t draw; - auto tie() const { - return std::tie( material, variant, bank, type, indexed, blending ); } - }; - std::vector m_indirectstatekeys; - std::vector m_indirectdrawstates; - std::vector m_indirectqueues; - std::vector m_indirectstates; - std::vector m_indirectstateorder; - std::vector m_indirectorder; - std::vector m_indirectdrawsordered; - std::vector m_indirectcommandsordered; + // working space of Upload_Indirect_Blocks(), kept between frames std::vector m_indirectblocks; // the model blocks of the multi draws, one buffer for each of the last frames like model_ubo (nullptr: no multi draws) std::unique_ptr m_drawblockstream; @@ -754,10 +601,6 @@ class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { std::unique_ptr m_shadow_multidraw_shader; // and of the sun shadows std::unique_ptr m_alpha_shadow_multidraw_shader; bool m_bindmultidraw { false }; // Bind_Material() binds the variant of the material program for multi draws - bool m_indirectrecording { false }; // draw() and Bind_Material() record instead of drawing - std::uint32_t m_indirectentry { 0 }; // entry (group and lod band) recorded - material_handle m_indirectmaterial { null_handle }; // what the last Bind_Material() bound while recording - TSubModel const *m_indirectsubmodel { nullptr }; // depth prepass: programs and the class of the material programs (see Prepass_Class()) std::unique_ptr m_prepass_shader; std::unique_ptr m_prepass_alpha_shader; @@ -768,7 +611,7 @@ class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { bool m_scenedepthpass { false }; // inside it: Bind_Material() and draw() work for the depth bool m_scenedepthskip { true }; // the material bound last isn't drawn in it bool m_scenedepthdrawn { false }; // it was drawn for the colour pass under way, which skips what was done in it already - bool m_indirectcolourpending { false }; // the instances were culled in it, the colour pass draws them from the recorded draws + // with m_indirectcolourpending: the instances were culled in it, the colour pass draws them from the recorded draws opengl46_instance_cull::command_set m_indirectcolourset { opengl46_instance_cull::command_set::first }; // with these commands debug_stats m_scenedepthstats; // of its last run double m_scenedepthcputime { 0.0 }; // ms @@ -894,12 +737,6 @@ class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { opengl46_local_shadows m_localshadows; // the shadow pass draws to a cube shadow map, with the programs of m_localshadows bool m_cubeshadows = false; - // vehicles near the camera which cast the shadows of scenery lamps, and those of them by the lamp being drawn - std::vector m_lampshadowvehicles; - std::vector m_lampshadowslot; - // headlights which may get shadow maps in this frame, and their importance - std::vector> m_headlightshadowcandidates; - std::vector m_headlightshadowpicked; double m_precipitationrotation; diff --git a/rendering/opengl46/opengl46shadowfit.cpp b/rendering/opengl46/opengl46shadowfit.cpp index f9c554dc..d84c557c 100644 --- a/rendering/opengl46/opengl46shadowfit.cpp +++ b/rendering/opengl46/opengl46shadowfit.cpp @@ -10,27 +10,10 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/opengl46/opengl46shadowfit.h" -#include - #include "model/Texture.h" #include "utilities/Globals.h" #include "utilities/utilities.h" -namespace -{ - -// the nearest distance the first cascade may start from and the least one the last reaches, metres: the cascades don't -// get any closer to the camera than this, however close the pixels are -float const SPLIT_NEAR_MIN{0.5f}; -float const SPLIT_FAR_MIN{16.f}; -// how far past its distances a cascade takes the pixels, this many metres and this share of the distance: the camera moves -// in the frame or two before the cascades are drawn from the measurement, and a pixel which came into another cascade since -// has to be within its map -float const MARGIN_METRES{1.f}; -float const MARGIN_SHARE{0.05f}; - -} // namespace - opengl46_shadow_fit::~opengl46_shadow_fit() { release(); @@ -97,20 +80,7 @@ void opengl46_shadow_fit::measure(GLuint const Depth, GLuint const Source, int c depth = m_depth; } - // numbers start from 1, 0 is no measurement - if (++m_measurement == 0) - ++m_measurement; - gl::shadow_fit_data data{}; - data.measurement = m_measurement; - data.depthmin = std::numeric_limits::infinity(); - for (std::size_t cascade = 0; cascade < gl::MAX_CASCADES; ++cascade) - { - for (std::size_t axis = 0; axis < 4; ++axis) - { - data.bounds[cascade * 8 + axis] = 0xFFFFFFFFu; - data.bounds[cascade * 8 + 4 + axis] = 0u; - } - } + auto const data{begin_measurement(View, Frame)}; m_buffer->update(data); m_buffer->bind_base(); @@ -124,11 +94,12 @@ void opengl46_shadow_fit::measure(GLuint const Depth, GLuint const Source, int c m_depthprogram->dispatch(groupsx, groupsy); glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - auto const farmin{std::min(SPLIT_FAR_MIN, View.range)}; + auto const bounds{bounds_params(View)}; + auto const margins{margin_params()}; glProgramUniformMatrix4fv(*m_boundsprogram, 0, 1, GL_FALSE, glm::value_ptr(inverseprojection)); glProgramUniformMatrix4fv(*m_boundsprogram, 4, 1, GL_FALSE, glm::value_ptr(viewtolight)); - glProgramUniform4f(*m_boundsprogram, 8, View.range, std::min(SPLIT_NEAR_MIN, farmin * 0.25f), farmin, std::clamp(Global.gfx_shadow_sdsm_split, 0.f, 1.f)); - glProgramUniform4f(*m_boundsprogram, 9, MARGIN_METRES, MARGIN_SHARE, 0.f, 0.f); + glProgramUniform4f(*m_boundsprogram, 8, bounds.x, bounds.y, bounds.z, bounds.w); + glProgramUniform4f(*m_boundsprogram, 9, margins.x, margins.y, margins.z, margins.w); m_boundsprogram->dispatch(groupsx, groupsy); // for the copy of the read back glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT); @@ -139,27 +110,14 @@ void opengl46_shadow_fit::measure(GLuint const Depth, GLuint const Source, int c m_timer.end(); m_readback->push(*m_buffer); - m_pending[m_measurement % PENDING] = pending{m_measurement, View, Frame}; } opengl46_shadow_fit::result const *opengl46_shadow_fit::latest() { gl::shadow_fit_data data; if (m_readback && m_readback->pop(&data)) - { - auto const &source{m_pending[data.measurement % PENDING]}; - if ((data.measurement != 0) && (source.measurement == data.measurement)) - m_latest = result{data, source.source, source.frame}; - } - return m_latest ? &(*m_latest) : nullptr; -} - -float opengl46_shadow_fit::unorder(std::uint32_t const Value) -{ - auto const bits{((Value & 0x80000000u) != 0u) ? (Value & 0x7FFFFFFFu) : ~Value}; - float result; - std::memcpy(&result, &bits, sizeof(result)); - return result; + arrived(data); + return gfx::shadow_fit::latest(); } std::string opengl46_shadow_fit::info_times() diff --git a/rendering/opengl46/opengl46shadowfit.h b/rendering/opengl46/opengl46shadowfit.h index a59bc66c..dfc162b4 100644 --- a/rendering/opengl46/opengl46shadowfit.h +++ b/rendering/opengl46/opengl46shadowfit.h @@ -19,34 +19,14 @@ http://mozilla.org/MPL/2.0/. #include "gl/shader.h" #include "gl/query.h" #include "gl/readback.h" +#include "rendering/common/shadowfit.h" -// the cascades of the sun shadows fitted to the depth of the main view (sample distribution shadow maps, gfx.shadow.sdsm). -// at the end of the colour pass of the main view two compute passes go over its resolved depth: the nearest and the farthest -// distance of the pixels which aren't the sky (shadow_fit_depth.comp), then the distances split between the cascades and, -// for each cascade, the box of the pixels it takes in the space of the light (shadow_fit_bounds.comp). the result comes back -// to the cpu a frame or two later without waiting (gl::readback), with the camera and the light it was measured with, and -// the renderer makes the cascades of the frame cover the boxes (opengl46_renderer::Shadow_Fit()) instead of the whole slices -// of the view between the fixed distances of shadowtune -class opengl46_shadow_fit +// the cascades of the sun shadows fitted to the depth of the main view (gfx.shadow.sdsm): the passes and the buffers of +// gfx::shadow_fit, which tells what they do. the result comes back to the cpu a frame or two later without waiting +// (gl::readback), and opengl46_renderer::Shadow_Fit() fits the cascades of the frame to it +class opengl46_shadow_fit : public gfx::shadow_fit { public: - // the view and the light a measurement was made with - struct view - { - glm::dvec3 camera; // position of the camera - glm::mat4 projection; // projection of the view - glm::mat3 rotation; // world to view space, rotation only - glm::mat3 light; // world to the space of the light, rotation only - float range{0.f}; // the farthest distance the cascades may reach - }; - // a measurement which came back - struct result - { - gl::shadow_fit_data data; - view source; - unsigned int frame{0}; // frame of the renderer it was made in - }; - opengl46_shadow_fit() = default; ~opengl46_shadow_fit(); opengl46_shadow_fit(const opengl46_shadow_fit &) = delete; @@ -63,30 +43,15 @@ class opengl46_shadow_fit // the time of the passes, for the debug panel std::string info_times(); - // the ends of the bounds, as the passes store them (shadow_fit_order() in shadow_fit_bounds.comp) - static float unorder(std::uint32_t const Value); - private: // the texture the depth is resolved to when there is no resolved depth to measure void allocate(int const Width, int const Height); void release(); - // a measurement on its way back, kept by its number - struct pending - { - std::uint32_t measurement{0}; // 0: none - view source; - unsigned int frame{0}; - }; - static constexpr std::size_t PENDING = 8; // more than the copies gl::readback keeps in flight - std::unique_ptr m_depthprogram; std::unique_ptr m_boundsprogram; std::unique_ptr m_buffer; std::unique_ptr m_readback; - std::array m_pending; - std::uint32_t m_measurement{0}; // number of the last measurement - std::optional m_latest; GLuint m_depth{0}; GLuint m_depthfb{0}; glm::ivec2 m_depthsize{0}; diff --git a/rendering/opengl46/opengl46sky.cpp b/rendering/opengl46/opengl46sky.cpp index c4608493..ed21906a 100644 --- a/rendering/opengl46/opengl46sky.cpp +++ b/rendering/opengl46/opengl46sky.cpp @@ -12,84 +12,10 @@ http://mozilla.org/MPL/2.0/. #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() { @@ -137,15 +63,7 @@ void opengl46_sky::init() 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); + m_atmosphere_ubo->update(m_light.atmosphere()); } void opengl46_sky::bind() @@ -155,75 +73,13 @@ void opengl46_sky::bind() 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) + bool mediumchanged{false}; + if (false == m_light.atmosphere_changed(Sundirection, mediumchanged)) 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->update(m_light.atmosphere()); m_atmosphere_ubo->bind_uniform(); m_atmosphere_result_ssbo->bind_base(); @@ -257,89 +113,23 @@ void opengl46_sky::update_atmosphere(glm::vec3 const &Sundirection) 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); - + m_light.begin_update(); // 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 - // with the shadows of the clouds the sun is held back where they are, not everywhere - auto const overcastsun{m_cloud_shadows ? 1.f : (1.f - 0.65f * m_overcast)}; - auto const sun{glm::vec3{result.sun_transmittance} * m_white_balance * (ATMOSPHERE_SUN_SCALE * Global.gfx_atmosphere_sun) * overcastsun}; - 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; + m_light.set_result(result); } + m_light.end_update(Sunlight, Fogcolor, Skydome, Sundirection, Sunangle); // 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}; + auto const &skydata{m_light.sky_data()}; if (!m_sky_data_uploaded || std::memcmp(&skydata, &m_sky_data, sizeof(skydata)) != 0) { m_sky_data = skydata; @@ -351,14 +141,12 @@ void opengl46_sky::update(opengl46_light &Sunlight, glm::vec3 &Fogcolor, CSkyDom void opengl46_sky::setup_sky(gl::model_ubs &Model) const { glBindTextureUnit(gl::SKY_VIEW_TEX, m_skyview_table); - // the volumetric clouds make the overcast sky themselves - Model.param[0] = glm::vec4{m_weight, m_sky_scale, m_clouds ? 0.f : 0.75f * m_overcast, m_sky_flat_luminance}; - Model.param[1] = glm::vec4{m_white_balance, 0.f}; + m_light.sky_params(Model.param[0], Model.param[1]); } glm::vec3 opengl46_sky::sun_disc_color(glm::vec3 const &Oldcolor) const { - return glm::mix(Oldcolor, m_sun_disc_color, m_weight); + return m_light.sun_disc_color(Oldcolor); } void opengl46_sky::prefilter(gl::cubemap const &Source) @@ -402,19 +190,5 @@ std::string opengl46_sky::info_times() 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)); + m_light.draw_debug_ui(); } diff --git a/rendering/opengl46/opengl46sky.h b/rendering/opengl46/opengl46sky.h index ef74d386..7cc44a2c 100644 --- a/rendering/opengl46/opengl46sky.h +++ b/rendering/opengl46/opengl46sky.h @@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/. #include "gl/query.h" #include "gl/readback.h" #include "rendering/opengl46/opengl46light.h" +#include "rendering/common/skylight.h" class CSkyDome; @@ -32,7 +33,8 @@ class CSkyDome; // 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 +// all of it is switched by Global.gfx_ambient_sh, gfx_atmosphere and gfx_envmap_ggx; off, the renderer works as before. +// what isn't the work of the gpu is the light of the sky shared with the vulkan renderer (gfx::sky_light) class opengl46_sky { public: @@ -52,7 +54,7 @@ class opengl46_sky // 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; + return m_light.atmosphere_weight(); } // the sky-view table of the atmosphere, its white balance, brightness and the overcast it was drawn with, for the colours of // the fog (opengl46_weather) @@ -62,22 +64,21 @@ class opengl46_sky } glm::vec3 const &white_balance() const { - return m_white_balance; + return m_light.white_balance(); } float sky_scale() const { - return m_sky_scale; + return m_light.sky_scale(); } float overcast() const { - return m_overcast; + return m_light.overcast(); } // the volumetric clouds are drawn over the sky (it doesn't take the overcast look then), and their shadows hold back the // sunlight (it isn't dimmed by the overcast then) void set_clouds(bool const Clouds, bool const Shadows) { - m_clouds = Clouds; - m_cloud_shadows = Shadows; + m_light.set_clouds(Clouds, Shadows); } // 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; @@ -96,11 +97,6 @@ class opengl46_sky 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 @@ -121,31 +117,11 @@ class opengl46_sky 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}; - bool m_clouds{false}; - bool m_cloud_shadows{false}; - 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{}; + // the light of the sky, shared with the vulkan renderer + gfx::sky_light m_light; // prefiltered environment map std::unique_ptr m_prefilter_program; diff --git a/rendering/opengl46/opengl46terrain.cpp b/rendering/opengl46/opengl46terrain.cpp index 294a7338..9b72f590 100644 --- a/rendering/opengl46/opengl46terrain.cpp +++ b/rendering/opengl46/opengl46terrain.cpp @@ -17,13 +17,6 @@ http://mozilla.org/MPL/2.0/. namespace { -int constexpr initial_capacity{8}; -int constexpr copy_attempts{600}; // frames to wait for the textures of a layer to load before it's left with what it has -float constexpr default_repeat{8.f}; - -// array textures: diffuse, normal maps, specular/gloss maps -std::array constexpr array_sizes{1024, 512, 256}; - void bind_for_upload(opengl_texture const &Texture) { ::glActiveTexture(GL_TEXTURE0); @@ -38,11 +31,8 @@ void set_anisotropy(GLenum const Target) } // namespace -bool opengl46_terrain_materials::init() +bool opengl46_terrain_materials::gpu_init() { - if (m_state != 0) - return m_state > 0; - m_state = -1; try { m_copy = std::make_unique(gl::shader("copy.frag")); @@ -53,151 +43,56 @@ bool opengl46_terrain_materials::init() return false; } m_framebuffer = std::make_unique(); - - m_white = make_texture("internal_src:terrain/white", GL_TEXTURE_2D); - { - auto const &white{m_textures.texture(m_white)}; - std::uint8_t const texel[4]{255, 255, 255, 255}; - bind_for_upload(white); - ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, texel); - } - char const *names[]{"internal_src:terrain/layers_diffuse", "internal_src:terrain/layers_normal", "internal_src:terrain/layers_specgloss"}; - for (std::size_t i = 0; i < m_arrays.size(); ++i) - { - auto &array{m_arrays[i]}; - array.handle = make_texture(names[i], GL_TEXTURE_2D_ARRAY); - array.size = array_sizes[i]; - array.srgb = (i == 0); - array.format = (array.srgb ? GL_SRGB8_ALPHA8 : GL_RGBA8); - make_storage(array, initial_capacity); - } - m_capacity = initial_capacity; - opengl_texture::reset_unit_cache(); - - // the shader is checked now, so a broken one makes the terrain fall back on plain materials instead of the failure indicator - try - { - m_program = GfxRenderer->Fetch_Shader("terrain"); - } - catch (gl::shader_exception const &e) - { - ErrorLog("heightmap terrain: can't use the terrain shader: " + std::string(e.what())); - return false; - } - m_state = 1; return true; } -texture_handle opengl46_terrain_materials::make_texture(std::string const &Name, GLenum const Target) +void opengl46_terrain_materials::gpu_texture(texture_source &Texture, texture_kind const Kind) { - auto const handle{m_textures.create(Name, false, GL_RGBA)}; - auto &texture{m_textures.texture(handle)}; + auto &texture{static_cast(Texture)}; if (texture.id == opengl_texture::invalid_id) ::glGenTextures(1, &texture.id); - texture.target = Target; - // the content is supplied here, the texture manager isn't to load, release or replace it - texture.is_ready = true; - texture.is_static = true; - texture.is_rendertarget = true; - texture.is_texstub = false; - texture.data_state = resource_state::good; - return handle; + texture.target = (Kind == texture_kind::layers ? GL_TEXTURE_2D_ARRAY : GL_TEXTURE_2D); } -void opengl46_terrain_materials::make_storage(texture_array &Array, int const Layers) +void opengl46_terrain_materials::gpu_white(texture_source &Texture) { - auto &texture{m_textures.texture(Array.handle)}; + auto const &white{static_cast(Texture)}; + std::uint8_t const texel[4]{255, 255, 255, 255}; + bind_for_upload(white); + ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, texel); +} + +void opengl46_terrain_materials::gpu_storage(texture_array const &Array, int const Layers, bool const Fresh) +{ + auto &texture{m_gltextures.texture(Array.handle)}; + if (Fresh) + { + ::glDeleteTextures(1, &texture.id); + ::glGenTextures(1, &texture.id); + } + auto const format{Array.srgb ? GL_SRGB8_ALPHA8 : GL_RGBA8}; bind_for_upload(texture); ::glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); ::glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); ::glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT); ::glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT); set_anisotropy(GL_TEXTURE_2D_ARRAY); - ::glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, Array.format, Array.size, Array.size, Layers, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + ::glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, format, Array.size, Array.size, Layers, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); texture.data_width = Array.size; texture.data_height = Array.size; texture.layers = Layers; - texture.data_format = Array.format; + texture.data_format = format; texture.data_components = GL_RGBA; } -void opengl46_terrain_materials::read_material(layer &Layer) +void opengl46_terrain_materials::gpu_blank(slot const &Slot) { - auto const &material{m_materials.material(Layer.material)}; - Layer.repeat = (Layer.size > 0.f ? Layer.size : (material.size.x > 0.f ? material.size.x : default_repeat)); - Layer.normalmap = (material.shader && material.shader->texture_conf.count("normalmap") > 0); - Layer.specgloss = (material.shader && material.shader->texture_conf.count("specgloss") > 0); - Layer.reflection = (std::isfinite(material.params[1].z) ? material.params[1].z : 0.f); -} - -int opengl46_terrain_materials::layer_index(gfx::terrain_layer const &Layer) -{ - for (auto const &existing : m_layers) - if (existing.material == Layer.material && existing.size == Layer.size) - return existing.index; - - layer entry; - entry.material = Layer.material; - entry.size = Layer.size; - entry.index = static_cast(m_layers.size()); - read_material(entry); - m_layers.push_back(entry); - m_pending = true; - - if (static_cast(m_layers.size()) > m_capacity) - { - // more room: the arrays are made anew, and every layer copied again - auto const capacity{m_capacity * 2}; - for (auto &array : m_arrays) - { - auto &texture{m_textures.texture(array.handle)}; - ::glDeleteTextures(1, &texture.id); - ::glGenTextures(1, &texture.id); - make_storage(array, capacity); - } - m_capacity = capacity; - for (auto &existing : m_layers) - { - existing.copied = false; - existing.attempts = 0; - } - opengl_texture::reset_unit_cache(); - } - return entry.index; -} - -std::size_t opengl46_terrain_materials::acquire_slot(material_handle const Reuse) -{ - if (Reuse != null_handle) - { - auto const lookup{m_slotof.find(Reuse)}; - if (lookup != m_slotof.end()) - { - m_slots[lookup->second].used = true; - return lookup->second; - } - } - if (false == m_free.empty()) - { - auto const index{m_free.back()}; - m_free.pop_back(); - m_slots[index].used = true; - return index; - } - auto const index{m_slots.size()}; - auto const suffix{std::to_string(index)}; - slot entry; - entry.weights[0] = make_texture("internal_src:terrain/weights0_" + suffix, GL_TEXTURE_2D); - entry.weights[1] = make_texture("internal_src:terrain/weights1_" + suffix, GL_TEXTURE_2D); - entry.table = make_texture("internal_src:terrain/table_" + suffix, GL_TEXTURE_2D); - entry.holes = make_texture("internal_src:terrain/holes_" + suffix, GL_TEXTURE_2D); - // the textures get their content before the material is made of them, which checks they're there std::uint8_t const blank[4]{0, 0, 0, 0}; - for (auto const handle : entry.weights) + for (auto const handle : Slot.weights) { - bind_for_upload(m_textures.texture(handle)); + bind_for_upload(m_gltextures.texture(handle)); ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); @@ -206,7 +101,7 @@ std::size_t opengl46_terrain_materials::acquire_slot(material_handle const Reuse ::glGenerateMipmap(GL_TEXTURE_2D); } { - bind_for_upload(m_textures.texture(entry.table)); + bind_for_upload(m_gltextures.texture(Slot.table)); ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); std::array const zeros{}; @@ -214,7 +109,7 @@ std::size_t opengl46_terrain_materials::acquire_slot(material_handle const Reuse } { // led between the samples, so the edges of the holes run smoothly across the cells - bind_for_upload(m_textures.texture(entry.holes)); + bind_for_upload(m_gltextures.texture(Slot.holes)); ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); @@ -224,281 +119,89 @@ std::size_t opengl46_terrain_materials::acquire_slot(material_handle const Reuse ::glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, 1, 1, 0, GL_RED, GL_UNSIGNED_BYTE, &none); ::glPixelStorei(GL_UNPACK_ALIGNMENT, 4); } - opengl_texture::reset_unit_cache(); - auto const name = [&](texture_handle const Handle) { return "internal_src:" + m_textures.texture(Handle).name; }; - std::string const definition{ - "shader: terrain\n" - "texture1: " + name(m_white) + "\n" - "texture2: " + name(entry.weights[0]) + "\n" - "texture3: " + name(entry.weights[1]) + "\n" - "texture4: " + name(m_arrays[0].handle) + "\n" - "texture5: " + name(m_arrays[1].handle) + "\n" - "texture6: " + name(m_arrays[2].handle) + "\n" - "texture7: " + name(entry.table) + "\n" - "texture8: " + name(entry.holes) + "\n"}; - entry.material = m_materials.create_from_text("internal_src:terrain/chunk_" + suffix, definition); - if (entry.material == null_handle || false == m_materials.material(entry.material).is_good) - { - ErrorLog("heightmap terrain: can't make the material of a chunk"); - m_state = -1; - return static_cast(-1); - } - entry.used = true; - m_slots.push_back(entry); - m_slotof.emplace(entry.material, index); - return index; } -material_handle opengl46_terrain_materials::make(material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement, - int const HoleSamples, std::uint8_t const *Holes) +void opengl46_terrain_materials::gpu_weights(texture_handle const Texture, int const Side, std::uint8_t const *Texels) { - if (Layers.empty() || false == init()) - return null_handle; - auto const count{std::min(Layers.size(), max_layers)}; - std::array indices{}; - for (std::size_t i = 0; i < count; ++i) - { - if (Layers[i].material == null_handle) - return null_handle; - indices[i] = layer_index(Layers[i]); - } - auto const slotindex{acquire_slot(Reuse)}; - if (slotindex == static_cast(-1)) - return null_handle; - auto &entry{m_slots[slotindex]}; - - // weights, four layers to a texture - auto const painted{Weights != nullptr && Samples > 0 && count > 1}; - auto const side{painted ? Samples + 1 : 1}; - auto const samples{static_cast(side) * side}; - std::vector texels(samples * 4); - for (std::size_t texture = 0; texture < 2; ++texture) - { - std::fill(texels.begin(), texels.end(), 0); - for (std::size_t channel = 0; channel < 4; ++channel) - { - auto const layer{texture * 4 + channel}; - if (layer >= count) - break; - if (false == painted) - { - if (layer == 0) - texels[channel] = 255; - continue; - } - auto const *source{Weights + layer * samples}; - for (std::size_t i = 0; i < samples; ++i) - texels[i * 4 + channel] = source[i]; - } - bind_for_upload(m_textures.texture(entry.weights[texture])); - ::glPixelStorei(GL_UNPACK_ALIGNMENT, 4); - ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, side, side, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data()); - ::glGenerateMipmap(GL_TEXTURE_2D); - } - entry.count = count; - entry.layers = indices; - for (std::size_t i = 0; i < count; ++i) - entry.rotations[i] = Layers[i].rotation; - write_table(entry); - - // the holes: the cells cut out entirely are left out of the mesh, the program of the holes cuts the rest of the openings - auto const cut{Holes != nullptr && HoleSamples > 0}; - { - auto const holeside{cut ? HoleSamples + 1 : 1}; - std::uint8_t const none{0}; - bind_for_upload(m_textures.texture(entry.holes)); - ::glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - ::glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, holeside, holeside, 0, GL_RED, GL_UNSIGNED_BYTE, cut ? Holes : &none); - ::glPixelStorei(GL_UNPACK_ALIGNMENT, 4); - opengl_texture::reset_unit_cache(); - } - auto &material{m_materials.material(entry.material)}; - material.params[2] = glm::vec4(Placement.x, Placement.y, 1.f / std::max(1.f, Placement.z), static_cast(side - 1)); - if (m_holesprogram == nullptr && cut) - { - try - { - m_holesprogram = GfxRenderer->Fetch_Shader("terrain_holes"); - } - catch (gl::shader_exception const &e) - { - ErrorLog("heightmap terrain: can't use the shader of the holes: " + std::string(e.what())); - } - } - // the program of the holes discards fragments, which the depth prepass of the plain one never does - material.shader = (cut && m_holesprogram != nullptr) ? m_holesprogram : m_program; - return entry.material; + bind_for_upload(m_gltextures.texture(Texture)); + ::glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, Side, Side, 0, GL_RGBA, GL_UNSIGNED_BYTE, Texels); + ::glGenerateMipmap(GL_TEXTURE_2D); } -void opengl46_terrain_materials::write_table(slot const &Slot) +void opengl46_terrain_materials::gpu_table(texture_handle const Texture, float const *Table) +{ + bind_for_upload(m_gltextures.texture(Texture)); + ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast(max_layers), 2, 0, GL_RGBA, GL_FLOAT, Table); +} + +void opengl46_terrain_materials::gpu_holes(texture_handle const Texture, int const Side, std::uint8_t const *Samples) +{ + bind_for_upload(m_gltextures.texture(Texture)); + ::glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + ::glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, Side, Side, 0, GL_RED, GL_UNSIGNED_BYTE, Samples); + ::glPixelStorei(GL_UNPACK_ALIGNMENT, 4); +} + +void opengl46_terrain_materials::gpu_changed() { - // what the layers are: index in the arrays, repeat, which maps they have, reflection - std::array table{}; - for (std::size_t i = 0; i < Slot.count; ++i) - { - auto const &layer{m_layers[Slot.layers[i]]}; - table[i * 4 + 0] = static_cast(layer.index); - table[i * 4 + 1] = 1.f / std::max(0.01f, layer.repeat); - table[i * 4 + 2] = layer.normalmap ? 1.f : 0.f; - table[i * 4 + 3] = layer.specgloss ? 1.f : 0.f; - table[(max_layers + i) * 4 + 0] = layer.reflection; - // the textures turned by the layer of the chunk (the same material can be turned differently in another layer) - auto const angle{glm::radians(static_cast(Slot.rotations[i]))}; - table[(max_layers + i) * 4 + 1] = static_cast(std::cos(angle)); - table[(max_layers + i) * 4 + 2] = static_cast(std::sin(angle)); - } - bind_for_upload(m_textures.texture(Slot.table)); - ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast(max_layers), 2, 0, GL_RGBA, GL_FLOAT, table.data()); opengl_texture::reset_unit_cache(); } -void opengl46_terrain_materials::on_environment_change() +void opengl46_terrain_materials::gpu_begin_copies() { - if (m_state <= 0) - return; - bool changed{false}; - for (auto &layer : m_layers) - { - auto const &material{m_materials.material(layer.material)}; - if (false == (material.update_on_season_change || material.update_on_weather_change)) - continue; - // the reloaded material can have other textures, other maps and another size - read_material(layer); - layer.copied = false; - layer.attempts = 0; - changed = true; - } - if (false == changed) - return; - m_pending = true; - // the tables of the chunks hold what the layers have, freed slots get theirs anew when they're used again - for (auto const &entry : m_slots) - if (entry.used) - write_table(entry); -} - -void opengl46_terrain_materials::release(material_handle const Material) -{ - auto const lookup{m_slotof.find(Material)}; - if (lookup == m_slotof.end() || false == m_slots[lookup->second].used) - return; - m_slots[lookup->second].used = false; - m_free.push_back(lookup->second); -} - -bool opengl46_terrain_materials::copy_layer(layer &Layer) -{ - auto const &material{m_materials.material(Layer.material)}; - auto const slot = [&](char const *Name, int const Fallback) { - if (material.shader) - { - auto const lookup{material.shader->texture_conf.find(Name)}; - if (lookup != material.shader->texture_conf.end()) - return static_cast(lookup->second.id); - return -1; - } - return Fallback; - }; - std::array const slots{slot("diffuse", 0), Layer.normalmap ? slot("normalmap", 1) : -1, Layer.specgloss ? slot("specgloss", 2) : -1}; - // all maps of the layer have to be ready, so they go together - std::array sources{}; - for (std::size_t i = 0; i < slots.size(); ++i) - { - if (slots[i] < 0 || material.textures[slots[i]] == null_handle) - continue; - auto &texture{m_textures.mark_as_used(material.textures[slots[i]])}; - if (false == texture.is_ready) - { - // a texture which is on the card already may have dropped its data after the upload (create() would refuse it then), - // one released by the sweep or never read has to be read first - if (texture.data_state == resource_state::none) - { - texture.reload_on_use = false; - texture.load(); - } - if (texture.data_state == resource_state::failed) - continue; - if (false == texture.create()) - { - if (++Layer.attempts < copy_attempts) - return false; - WriteLog("heightmap terrain: texture \"" + texture.name + "\" isn't ready, the terrain goes without it"); - continue; // waited long enough, the layer goes without it - } - } - sources[i] = &texture; - } - // defaults for maps the layer doesn't have: grey, flat, without specular reflections - std::array, 3> const defaults{{{0.5f, 0.5f, 0.5f, 1.f}, {0.5f, 0.5f, 1.f, 1.f}, {0.f, 1.f, 0.f, 1.f}}}; - for (std::size_t i = 0; i < m_arrays.size(); ++i) - { - auto const &array{m_arrays[i]}; - auto &target{m_textures.texture(array.handle)}; - m_framebuffer->attach(target, GL_COLOR_ATTACHMENT0, Layer.index); - m_framebuffer->setup_drawing(1); - ::glViewport(0, 0, array.size, array.size); - if (sources[i] == nullptr) - { - ::glClearBufferfv(GL_COLOR, 0, defaults[i].data()); - continue; - } - if (array.srgb) - ::glEnable(GL_FRAMEBUFFER_SRGB); - else - ::glDisable(GL_FRAMEBUFFER_SRGB); - m_copy->apply(*sources[i], m_framebuffer.get()); - } - Layer.copied = true; - return true; -} - -void opengl46_terrain_materials::update() -{ - if (false == m_pending || m_state <= 0) - return; - - auto const blend{::glIsEnabled(GL_BLEND)}; - auto const scissor{::glIsEnabled(GL_SCISSOR_TEST)}; - auto const cull{::glIsEnabled(GL_CULL_FACE)}; - auto const depth{::glIsEnabled(GL_DEPTH_TEST)}; - auto const srgb{::glIsEnabled(GL_FRAMEBUFFER_SRGB)}; + m_saved.blend = ::glIsEnabled(GL_BLEND); + m_saved.scissor = ::glIsEnabled(GL_SCISSOR_TEST); + m_saved.cull = ::glIsEnabled(GL_CULL_FACE); + m_saved.depth = ::glIsEnabled(GL_DEPTH_TEST); + m_saved.srgb = ::glIsEnabled(GL_FRAMEBUFFER_SRGB); ::glDisable(GL_BLEND); ::glDisable(GL_SCISSOR_TEST); ::glDisable(GL_CULL_FACE); +} - bool copied{false}; - m_pending = false; - for (auto &layer : m_layers) +void opengl46_terrain_materials::gpu_copy(texture_array const &Array, int const Layer, texture_source *Source, std::array const &Default) +{ + auto &target{m_gltextures.texture(Array.handle)}; + m_framebuffer->attach(target, GL_COLOR_ATTACHMENT0, Layer); + m_framebuffer->setup_drawing(1); + ::glViewport(0, 0, Array.size, Array.size); + if (Source == nullptr) { - if (layer.copied) - continue; - if (copy_layer(layer)) - copied = true; - else - m_pending = true; + ::glClearBufferfv(GL_COLOR, 0, Default.data()); + return; } - if (copied) + if (Array.srgb) + ::glEnable(GL_FRAMEBUFFER_SRGB); + else + ::glDisable(GL_FRAMEBUFFER_SRGB); + m_copy->apply(static_cast(*Source), m_framebuffer.get()); +} + +void opengl46_terrain_materials::gpu_end_copies(bool const Copied) +{ + if (Copied) { - for (auto const &array : m_arrays) + for (auto const &array : arrays()) { - bind_for_upload(m_textures.texture(array.handle)); + bind_for_upload(m_gltextures.texture(array.handle)); ::glGenerateMipmap(GL_TEXTURE_2D_ARRAY); } } // leave the state as the rest of the renderer knows it gl::framebuffer::unbind(); - if (blend) + if (m_saved.blend) ::glEnable(GL_BLEND); - if (scissor) + if (m_saved.scissor) ::glEnable(GL_SCISSOR_TEST); - if (cull) + if (m_saved.cull) ::glEnable(GL_CULL_FACE); - if (depth) + if (m_saved.depth) ::glEnable(GL_DEPTH_TEST); else ::glDisable(GL_DEPTH_TEST); - if (srgb) + if (m_saved.srgb) ::glEnable(GL_FRAMEBUFFER_SRGB); else ::glDisable(GL_FRAMEBUFFER_SRGB); diff --git a/rendering/opengl46/opengl46terrain.h b/rendering/opengl46/opengl46terrain.h index 48ee9a02..ba93e2e9 100644 --- a/rendering/opengl46/opengl46terrain.h +++ b/rendering/opengl46/opengl46terrain.h @@ -9,91 +9,44 @@ http://mozilla.org/MPL/2.0/. #pragma once -#include #include -#include -#include -#include "rendering/renderer.h" -#include "model/material.h" -#include "model/Texture.h" +#include "rendering/common/terrainmaterials.h" #include "gl/framebuffer.h" #include "gl/postfx.h" -// materials of the heightmap terrain for the opengl 4.6 renderer (shader mat_terrain, mat_terrain_holes for chunks with holes). -// the textures of the layer materials are copied into texture arrays shared by all chunks; each chunk gets a material -// of its own, with textures holding the weights of its layers and a table of what its layers are -class opengl46_terrain_materials +// materials of the heightmap terrain for the opengl 4.6 renderer (shader mat_terrain, mat_terrain_holes for chunks with holes): +// the textures of gfx::terrain_materials as opengl textures, the layer maps copied into the arrays by a draw of the copy shader +class opengl46_terrain_materials : public gfx::terrain_materials { public: - opengl46_terrain_materials(material_manager &Materials, texture_manager &Textures) : m_materials(Materials), m_textures(Textures) {} + opengl46_terrain_materials(material_manager &Materials, texture_manager &Textures) : gfx::terrain_materials(Materials, Textures), m_gltextures(Textures) {} - // see gfx_renderer::Terrain_Material. returns null_handle if the terrain shader can't be used - material_handle make(material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement, - int const HoleSamples = 0, std::uint8_t const *Holes = nullptr); - void release(material_handle const Material); - // copies the textures of new layers into the arrays; to be called before the frame is drawn - void update(); - // the layer materials which change with the season or the weather were reloaded, their textures are copied again - void on_environment_change(); + protected: + bool gpu_init() override; + void gpu_texture(texture_source &Texture, texture_kind const Kind) override; + void gpu_white(texture_source &Texture) override; + void gpu_storage(texture_array const &Array, int const Layers, bool const Fresh) override; + void gpu_blank(slot const &Slot) override; + void gpu_weights(texture_handle const Texture, int const Side, std::uint8_t const *Texels) override; + void gpu_table(texture_handle const Texture, float const *Table) override; + void gpu_holes(texture_handle const Texture, int const Side, std::uint8_t const *Samples) override; + void gpu_begin_copies() override; + void gpu_copy(texture_array const &Array, int const Layer, texture_source *Source, std::array const &Default) override; + void gpu_end_copies(bool const Copied) override; + void gpu_changed() override; private: - static constexpr std::size_t max_layers{8}; - - struct layer - { - material_handle material{null_handle}; - float size{0.f}; // as given by the chunk - float repeat{8.f}; // metres the textures are repeated at - int index{0}; // in the arrays - bool normalmap{false}; - bool specgloss{false}; - float reflection{0.f}; - bool copied{false}; - int attempts{0}; - }; - struct texture_array - { - texture_handle handle{null_handle}; - int size{0}; - GLint format{0}; - bool srgb{false}; - }; - struct slot - { - material_handle material{null_handle}; - std::array weights{null_handle, null_handle}; - texture_handle table{null_handle}; - texture_handle holes{null_handle}; // where the ground is cut, R8; a single texel of 0 without holes - bool used{false}; - // what the table was made of, kept to make it again when a layer changes - std::size_t count{0}; - std::array layers{}; - std::array rotations{}; - }; - - bool init(); - texture_handle make_texture(std::string const &Name, GLenum const Target); - void make_storage(texture_array &Array, int const Layers); - void read_material(layer &Layer); - int layer_index(gfx::terrain_layer const &Layer); - void write_table(slot const &Slot); - bool copy_layer(layer &Layer); - std::size_t acquire_slot(material_handle const Reuse); - - material_manager &m_materials; - texture_manager &m_textures; - int m_state{0}; // 0: not set up yet, 1: ready, -1: can't be used - texture_handle m_white{null_handle}; - std::array m_arrays; // diffuse, normal maps, specular/gloss - int m_capacity{0}; // layers the arrays have room for - std::vector m_layers; - bool m_pending{false}; - std::vector m_slots; - std::vector m_free; - std::unordered_map m_slotof; - std::shared_ptr m_program; // of the chunks, mat_terrain - std::shared_ptr m_holesprogram; // of the chunks with holes, mat_terrain_holes + texture_manager &m_gltextures; std::unique_ptr m_copy; std::unique_ptr m_framebuffer; + // the state of opengl the copies change, given back after them + struct + { + GLboolean blend{GL_FALSE}; + GLboolean scissor{GL_FALSE}; + GLboolean cull{GL_FALSE}; + GLboolean depth{GL_FALSE}; + GLboolean srgb{GL_FALSE}; + } m_saved; }; diff --git a/rendering/opengl46/opengl46weather.cpp b/rendering/opengl46/opengl46weather.cpp index b9772246..dcbacb77 100644 --- a/rendering/opengl46/opengl46weather.cpp +++ b/rendering/opengl46/opengl46weather.cpp @@ -11,14 +11,11 @@ http://mozilla.org/MPL/2.0/. #include "rendering/opengl46/opengl46weather.h" #include "rendering/opengl46/opengl46sky.h" -#include "environment/starcatalog.h" #include "environment/weather.h" #include "simulation/simulationenvironment.h" #include "model/Texture.h" #include "utilities/Globals.h" -#include "utilities/Timer.h" #include "utilities/utilities.h" -#include "widgets/weatherpanel.h" namespace { @@ -27,47 +24,6 @@ namespace int const SHAPE_NOISE_SIZE{128}; int const DETAIL_NOISE_SIZE{32}; int const COVERAGE_NOISE_SIZE{512}; -// how many times the cell of a genus fits in the tile of the shape noise, and its spread in the tile of the coverage noise -double const SHAPE_TILE_CELLS{4.0}; -double const COVERAGE_TILE_SPREADS{4.0}; -// size of the patches of the ground fog -float const FOG_PATCH_SIZE{300.f}; -// the haze the atmosphere draws in the sky already, as an extinction: about the haze of a day with 30 km of visibility -float const SKY_HAZE_EXTINCTION{3.f / 30000.f}; -// near end of the fog volume (view depth, metres) -float const FOG_VOLUME_NEAR{0.5f}; -// bands the panorama is drawn in, one a frame -int const PANORAMA_BANDS{8}; -// brightness of a star of magnitude 0 in the units of the scene -float const STAR_BRIGHTNESS{4.f}; -// stars dimmer than this aren't drawn -float const STAR_LIMIT{6.5f}; - -float luminance(glm::vec3 const &Color) -{ - return glm::dot(Color, glm::vec3{0.2126f, 0.7152f, 0.0722f}); -} - -// the position in a tiling noise, of a period in metres, kept small for the single precision of the shaders -double wrap(double const Value, double const Period) -{ - return Value - Period * std::floor(Value / Period); -} - -// the colour of a star of a colour temperature, linear, of luminance 1, somewhat paler than the black body: the eye sees most -// stars white -glm::vec3 star_color(float const Temperature) -{ - auto const t{std::clamp(Temperature, 1500.f, 40000.f) / 100.f}; - glm::vec3 color; - color.r = (t <= 66.f) ? 255.f : 329.698727446f * std::pow(t - 60.f, -0.1332047592f); - color.g = (t <= 66.f) ? 99.4708025861f * std::log(t) - 161.1195681661f : 288.1221695283f * std::pow(t - 60.f, -0.0755148492f); - color.b = (t >= 66.f) ? 255.f : (t <= 19.f) ? 0.f : 138.5177312231f * std::log(t - 10.f) - 305.0447927307f; - color = glm::pow(glm::clamp(color / 255.f, glm::vec3{0.f}, glm::vec3{1.f}), glm::vec3{2.2f}); - color /= std::max(luminance(color), 1e-3f); - return glm::mix(color, glm::vec3{1.f}, 0.5f); -} - std::unique_ptr make_compute(std::string const &Name) { gl::shader shader(Name); @@ -118,26 +74,6 @@ GLuint opengl46_weather::create_texture(GLenum const Target, GLenum const Format return texture; } -opengl46_weather::quality_level opengl46_weather::quality(int const Clouds, int const Fog) -{ - quality_level level; - switch (std::clamp(Clouds, 1, 4)) - { - case 1: level = {4, 32, 3, false, {512, 128}, {}}; break; - case 2: level = {2, 48, 4, true, {1024, 256}, {}}; break; - case 3: level = {2, 80, 6, true, {1024, 256}, {}}; break; - default: level = {2, 128, 8, true, {2048, 512}, {}}; break; - } - switch (std::clamp(Fog, 1, 4)) - { - case 1: level.fog_grid = {80, 45, 48}; break; - case 2: level.fog_grid = {160, 90, 64}; break; - case 3: level.fog_grid = {160, 90, 96}; break; - default: level.fog_grid = {240, 135, 128}; break; - } - return level; -} - void opengl46_weather::init() { m_noise_program = make_compute("clouds_noise.comp"); @@ -150,32 +86,12 @@ void opengl46_weather::init() m_sky_program = make_program("quad.vert", "sky_weather.frag"); m_stars_program = make_program("stars.vert", "stars.frag"); - // a grey fog and no clouds until the first update - for (auto &color : m_weather.fog_horizon) - color = glm::vec4{0.5f, 0.5f, 0.5f, 1.f}; - m_weather.fog_haze = glm::vec4{3.f / 10000.f, 1.f / 800.f, SKY_HAZE_EXTINCTION, 1.f}; - m_weather.fog_light = glm::vec4{0.f, 1.f, 0.6f, 1.f / 5000.f}; m_weather_ubo = std::make_unique(sizeof(gl::weather_ubs), gl::WEATHER_UBO_BINDING); - m_weather_ubo->update(m_weather); + m_weather_ubo->update(m_state.block()); make_noise(); - // the stars, in the equatorial frame - std::vector stars; - auto const *catalogue{star_catalogue::stars()}; - for (std::size_t i = 0; i < star_catalogue::size(); ++i) - { - auto const &star{catalogue[i]}; - auto const magnitude{static_cast(star.magnitude) * 0.01f}; - if (magnitude > STAR_LIMIT) - continue; - auto const ascension{glm::radians(static_cast(star.right_ascension) * 0.01f)}; - auto const declination{glm::radians(static_cast(star.declination) * 0.01f)}; - stars.emplace_back(std::cos(declination) * std::cos(ascension), std::cos(declination) * std::sin(ascension), std::sin(declination), magnitude); - // a phase of the twinkling which doesn't repeat from star to star - auto const phase{std::fmod(static_cast(i) * 0.6180339887f, 1.f)}; - stars.emplace_back(star_color(static_cast(star.temperature) * 10.f), phase); - } + auto const stars{gfx::weather_state::star_vertices()}; m_stars_count = static_cast(stars.size() / 2); m_stars_vao.emplace(); m_stars_vao->bind(); @@ -249,105 +165,15 @@ void opengl46_weather::bind_noise() const void opengl46_weather::upload_header() { // the colours of the horizon are left alone when the gpu writes them - auto const size{m_horizon_on_gpu ? offsetof(gl::weather_ubs, fog_horizon) : sizeof(gl::weather_ubs)}; - m_weather_ubo->update(reinterpret_cast(&m_weather), 0, static_cast(size)); + auto const size{m_state.horizon_on_gpu() ? offsetof(gl::weather_ubs, fog_horizon) : sizeof(gl::weather_ubs)}; + m_weather_ubo->update(reinterpret_cast(&m_state.block()), 0, static_cast(size)); m_weather_ubo->bind_uniform(); } void opengl46_weather::update(opengl46_sky const &Sky, glm::vec3 const &Keylight, glm::vec3 const &Keycolor, glm::vec3 const &Ambient, glm::vec3 const &Fogcolor, glm::dvec3 const &Camera) { - auto const &weather{simulation::Environment.weather()}; - auto const &conditions{weather.conditions()}; - m_fog_volume_valid = false; - m_clouds_view_valid = false; - ++m_frame; - - auto const height{static_cast(Camera.y) - weather.ground()}; - auto const time{weather.time()}; - auto const sun{glm::normalize(Keylight)}; - - // the fog. the visibility is where a dark object fades to 5%: the extinction is 3 over it - auto const haze{3.f / conditions.visibility}; - auto const hazerate{1.f / conditions.haze_height}; - m_weather.fog_haze = glm::vec4{haze, hazerate, SKY_HAZE_EXTINCTION, 1.f}; - if (conditions.groundfog_thickness > 0.f) - { - auto const thickness{conditions.groundfog_thickness}; - m_weather.fog_ground = glm::vec4{std::max(3.f / conditions.groundfog_visibility - haze, 0.f), 1.f / (0.25f * thickness), 0.75f * thickness, conditions.fog_noise}; - } - else - m_weather.fog_ground = glm::vec4{0.f, 1.f, 0.f, conditions.fog_noise}; - // the patches drift with the wind, slower than the clouds - auto const drift{glm::dvec2{weather.cloud_offset()} * 0.3}; - auto const patch{static_cast(FOG_PATCH_SIZE)}; - m_weather.fog_noise = glm::vec4{static_cast(wrap(Camera.x - drift.x, patch)), static_cast(wrap(Camera.y, patch)), static_cast(wrap(Camera.z - drift.y, patch)), 1.f / FOG_PATCH_SIZE}; - // the extinction at the height of the camera tells how far things are hidden - auto const extinction{haze * std::exp(-hazerate * std::max(height, 0.f)) + - m_weather.fog_ground.x * std::exp(-m_weather.fog_ground.y * std::max(height - m_weather.fog_ground.z, 0.f))}; - // 2% left at 1.3 visibilities; the draw range is twice this - m_fog_range = std::clamp(3.f / std::max(extinction, 1e-7f) / 1.5f, 5.f, 1e6f); - - // the share of the light of the fog the sun gives, which the shadows hold back - auto const sunlight{luminance(Keycolor) * 1.5f * std::max(sun.y, 0.f)}; - auto const skylight{luminance(Ambient) * 0.65f}; - m_sun_share = (sunlight + skylight > 1e-6f) ? sunlight / (sunlight + skylight) : 0.f; - - // clouds - auto const layers{std::min(conditions.clouds.size(), gl::MAX_CLOUD_LAYERS)}; - m_clouds_drawn = (Global.gfx_clouds > 0) && (layers > 0); - m_average_transmittance = 1.f; - auto const velocity{weather.cloud_velocity()}; - auto const speed{glm::length(velocity)}; - auto const wind{speed > 1e-3f ? glm::dvec2{velocity / speed} : glm::dvec2{1.0, 0.0}}; - m_weather.cloud_wind = glm::vec4{static_cast(wind.x), static_cast(wind.y), speed, m_cloud_detail_scale}; - for (std::size_t i = 0; i < gl::MAX_CLOUD_LAYERS; ++i) - { - auto &target{m_weather.cloud_layers[i]}; - if (i >= layers) - { - target = gl::cloud_layer_ubs{}; - continue; - } - auto const &layer{conditions.clouds[i]}; - auto const &genus{cloud_genus_info(layer.type)}; - target.heights = glm::vec4{layer.base, layer.top, 1.f / std::max(layer.top - layer.base, 1.f), static_cast(layer.type)}; - auto const shapetile{static_cast(genus.cell) * SHAPE_TILE_CELLS}; - auto const coveragetile{static_cast(genus.spread) * COVERAGE_TILE_SPREADS}; - target.shape = glm::vec4{layer.coverage, genus.extinction * layer.density, static_cast(1.0 / shapetile), genus.erosion}; - target.profile = glm::vec4{genus.bottom, genus.top, genus.top_variation, genus.anvil}; - // the higher, the faster the wind carries them - auto const offset{glm::dvec2{weather.cloud_offset()} * (1.0 + static_cast(layer.base) / 8000.0)}; - // the camera's place in the noise, in the space stretched along the wind as the shaders take it - auto const place{glm::dvec2{Camera.x, Camera.z} - offset}; - auto const along{glm::dot(place, wind)}; - auto const stretched{(place - wind * along) + wind * (along / std::max(static_cast(genus.stretch), 1.0))}; - target.motion = glm::vec4{static_cast(wrap(stretched.x, shapetile)), static_cast(wrap(stretched.y, shapetile)), static_cast(1.0 / coveragetile), genus.stretch}; - // the clouds change their shapes slowly; the seed moves the layers apart in the noise - auto const evolution{std::fmod(static_cast(conditions.seed) * 0.6180339887 + static_cast(i) * 0.37 + time * 0.00002, 1.0)}; - target.extra = glm::vec4{genus.flat ? 1.f : 0.f, static_cast(evolution), static_cast(wrap(stretched.x, coveragetile)), static_cast(wrap(stretched.y, coveragetile))}; - m_average_transmittance *= 1.f - std::clamp(layer.coverage * genus.opacity * std::min(layer.density, 1.5f), 0.f, 1.f); - } - if (Global.gfx_clouds <= 0) - m_average_transmittance = 1.f; - m_weather.fog_light = glm::vec4{m_sun_share, m_average_transmittance, 0.6f, fog_density()}; - m_weather.weather_camera = glm::vec4{height, static_cast(std::fmod(time, 100000.0)), m_clouds_drawn ? 1.f : 0.f, m_clouds_drawn ? static_cast(layers) : 0.f}; - m_weather.weather_sun_direction = glm::vec4{sun, sun.y > -0.02f ? 1.f : 0.f}; - m_weather.weather_sun_color = glm::vec4{Keycolor, m_cloud_sun_scale}; - m_weather.weather_ambient = glm::vec4{Ambient * 1.3f, m_cloud_sky_scale}; - m_weather.fog_volume_params = glm::vec4{0.f, FOG_VOLUME_NEAR, 1.f, 1.f}; - m_weather.fog_volume_target = glm::vec4{1.f, 1.f, Global.gfx_fog_range, 1.f - Sky.atmosphere_weight()}; - - // the colours of the horizon: from the atmosphere on the gpu, the old fog colour otherwise - m_horizon_on_gpu = (Sky.atmosphere_weight() > 0.f) && (Sky.skyview_table() != 0); - if (false == m_horizon_on_gpu) - { - for (auto &color : m_weather.fog_horizon) - color = glm::vec4{Fogcolor, 1.f}; - } - - // the shadows of the clouds, on a map snapped to its texels so it doesn't crawl as the camera moves - auto const shadows{m_clouds_drawn && Global.gfx_clouds_shadows && (sun.y > 0.02f)}; - m_weather.cloud_shadow_params = glm::vec4{0.f}; + m_state.update(Sky.atmosphere_weight(), Sky.skyview_table() != 0, Keylight, Keycolor, Ambient, Fogcolor, Camera); + auto const shadows{m_state.shadows_wanted()}; if (shadows) { if (m_shadow_size != Global.gfx_clouds_shadows_size) @@ -356,14 +182,11 @@ void opengl46_weather::update(opengl46_sky const &Sky, glm::vec3 const &Keylight m_shadow_size = Global.gfx_clouds_shadows_size; m_shadow_map = create_texture(GL_TEXTURE_2D, GL_R16F, glm::ivec3{m_shadow_size, m_shadow_size, 1}, GL_LINEAR, GL_CLAMP_TO_EDGE); } - auto const extent{static_cast(Global.gfx_clouds_shadows_range)}; - auto const texel{extent / static_cast(m_shadow_size)}; - auto const centre{glm::dvec2{std::round(Camera.x / texel) * texel - Camera.x, std::round(Camera.z / texel) * texel - Camera.z}}; - m_weather.cloud_shadow_params = glm::vec4{static_cast(centre.x), static_cast(centre.y), static_cast(1.0 / extent), 0.f}; + m_state.place_shadows(m_shadow_size, Camera); } upload_header(); - if (m_horizon_on_gpu) + if (m_state.horizon_on_gpu()) { glBindTextureUnit(gl::SKY_VIEW_TEX, Sky.skyview_table()); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, gl::WEATHER_SCRATCH_SSBO_BINDING, *m_weather_ubo); @@ -383,35 +206,32 @@ void opengl46_weather::update(opengl46_sky const &Sky, glm::vec3 const &Keylight bind_noise(); glBindImageTexture(0, m_shadow_map, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_R16F); glProgramUniform1i(*m_shadow_program, 1, m_shadow_size); - glProgramUniform3f(*m_shadow_program, 2, m_weather.cloud_shadow_params.x, m_weather.cloud_shadow_params.y, Global.gfx_clouds_shadows_range); + glProgramUniform3f(*m_shadow_program, 2, m_state.block().cloud_shadow_params.x, m_state.block().cloud_shadow_params.y, Global.gfx_clouds_shadows_range); glProgramUniform1i(*m_shadow_program, 3, 8); m_shadow_program->dispatch(groups(m_shadow_size), groups(m_shadow_size), 1); glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_R16F); m_shadow_timer.end(); - m_shadows_valid = true; - m_weather.cloud_shadow_params.w = 1.f; + m_state.shadows_drawn(true); upload_header(); } else - m_shadows_valid = false; + m_state.shadows_drawn(false); // a band of the panorama of the clouds, all of it the first time - if (m_clouds_drawn) + if (m_state.clouds_drawn()) { - auto const level{quality(Global.gfx_clouds, Global.gfx_fog_quality)}; + auto const level{gfx::weather_state::quality(Global.gfx_clouds, Global.gfx_fog_quality)}; if (m_panorama_size != level.panorama) { release(m_panorama); m_panorama_size = level.panorama; m_panorama = create_texture(GL_TEXTURE_2D, GL_RGBA16F, glm::ivec3{m_panorama_size, 1}, GL_LINEAR, GL_CLAMP_TO_EDGE); glTextureParameteri(m_panorama, GL_TEXTURE_WRAP_S, GL_REPEAT); - m_panorama_full = false; - m_panorama_band = 0; + m_state.panorama_resized(); } - auto const bandrows{(m_panorama_size.y + PANORAMA_BANDS - 1) / PANORAMA_BANDS}; - auto const first{m_panorama_full ? m_panorama_band * bandrows : 0}; - auto const rows{m_panorama_full ? bandrows : m_panorama_size.y}; + int first, rows; + m_state.panorama_band(m_panorama_size, first, rows); m_panorama_timer.begin(); bind_noise(); glBindImageTexture(0, m_panorama, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA16F); @@ -419,7 +239,7 @@ void opengl46_weather::update(opengl46_sky const &Sky, glm::vec3 const &Keylight glBindImageTexture(1, m_panorama, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA16F); glProgramUniform1i(*m_march_program, 0, 1); glProgramUniform2i(*m_march_program, 1, m_panorama_size.x, m_panorama_size.y); - glProgramUniform1i(*m_march_program, 4, m_frame); + glProgramUniform1i(*m_march_program, 4, m_state.frame()); // the panorama is seen blurred: fewer samples glProgramUniform4f(*m_march_program, 5, static_cast(std::max(level.steps / 2, 24)), static_cast(std::max(level.light_steps - 1, 2)), Global.gfx_clouds_distance, 0.f); glProgramUniform2i(*m_march_program, 6, first, rows); @@ -428,17 +248,14 @@ void opengl46_weather::update(opengl46_sky const &Sky, glm::vec3 const &Keylight glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA16F); glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA16F); m_panorama_timer.end(); - m_panorama_band = m_panorama_full ? (m_panorama_band + 1) % PANORAMA_BANDS : 0; - m_panorama_full = true; } gl::program::unbind(); - m_report = to_metar(conditions); } void opengl46_weather::prepare_view(glm::ivec2 const &Size, quality_level const &Level) { auto const cloudsize{glm::max((Size + Level.divisor - 1) / Level.divisor, glm::ivec2{1})}; - if (m_clouds_drawn && (cloudsize != m_view_size || m_clouds_color == 0)) + if (m_state.clouds_drawn() && (cloudsize != m_view_size || m_clouds_color == 0)) { for (auto *texture : {&m_clouds_color, &m_clouds_depth, &m_clouds_history[0], &m_clouds_history[1]}) release(*texture); @@ -447,7 +264,7 @@ void opengl46_weather::prepare_view(glm::ivec2 const &Size, quality_level const m_clouds_depth = create_texture(GL_TEXTURE_2D, GL_R32F, glm::ivec3{m_view_size, 1}, GL_NEAREST, GL_CLAMP_TO_EDGE); for (auto &history : m_clouds_history) history = create_texture(GL_TEXTURE_2D, GL_RGBA16F, glm::ivec3{m_view_size, 1}, GL_LINEAR, GL_CLAMP_TO_EDGE); - m_previous_clouds.valid = false; + m_state.previous_clouds.valid = false; } if ((Global.gfx_fog == 1) && (Level.fog_grid != m_fog_grid || m_fog_integrated == 0)) { @@ -458,23 +275,23 @@ void opengl46_weather::prepare_view(glm::ivec2 const &Size, quality_level const scattering = create_texture(GL_TEXTURE_3D, GL_RGBA16F, m_fog_grid, GL_LINEAR, GL_CLAMP_TO_EDGE); // one slice more: the fog through the whole volume without the clouds, which the sky takes (fog_integrate.comp) m_fog_integrated = create_texture(GL_TEXTURE_3D, GL_RGBA16F, m_fog_grid + glm::ivec3{0, 0, 1}, GL_LINEAR, GL_CLAMP_TO_EDGE); - m_previous_fog.valid = false; + m_state.previous_fog.valid = false; } m_view_level = Level; } void opengl46_weather::render_view(glm::mat4 const &Projection, glm::mat4 const &View, glm::dvec3 const &Camera, glm::ivec2 const &Size, int const Cablamp) { - auto const level{quality(Global.gfx_clouds, Global.gfx_fog_quality)}; + auto const level{gfx::weather_state::quality(Global.gfx_clouds, Global.gfx_fog_quality)}; prepare_view(Size, level); auto const rotation{glm::mat4{glm::mat3{View}}}; auto const inverseprojection{glm::inverse(Projection)}; auto const viewtoworld{glm::transpose(rotation)}; auto const projectionview{Projection * rotation}; - // a jump of the camera (another vehicle, the free camera) leaves nothing to blend with - auto const continuity = [&](view_state const &Previous) { return Previous.valid && glm::length(Previous.camera - Camera) < 50.0; }; + auto &previousclouds{m_state.previous_clouds}; + auto &previousfog{m_state.previous_fog}; - if (m_clouds_drawn) + if (m_state.clouds_drawn()) { m_clouds_timer.begin(); bind_noise(); @@ -484,7 +301,7 @@ void opengl46_weather::render_view(glm::mat4 const &Projection, glm::mat4 const glProgramUniform2i(*m_march_program, 1, m_view_size.x, m_view_size.y); glProgramUniformMatrix4fv(*m_march_program, 2, 1, GL_FALSE, glm::value_ptr(inverseprojection)); glProgramUniformMatrix4fv(*m_march_program, 3, 1, GL_FALSE, glm::value_ptr(viewtoworld)); - glProgramUniform1i(*m_march_program, 4, m_frame); + glProgramUniform1i(*m_march_program, 4, m_state.frame()); glProgramUniform4f(*m_march_program, 5, static_cast(level.steps), static_cast(level.light_steps), Global.gfx_clouds_distance, level.detail ? 1.f : 0.f); glProgramUniform2i(*m_march_program, 6, 0, m_view_size.y); m_march_program->dispatch(groups(m_view_size.x), groups(m_view_size.y), 1); @@ -499,10 +316,10 @@ void opengl46_weather::render_view(glm::mat4 const &Projection, glm::mat4 const glProgramUniform2i(*m_temporal_program, 1, m_view_size.x, m_view_size.y); glProgramUniformMatrix4fv(*m_temporal_program, 2, 1, GL_FALSE, glm::value_ptr(inverseprojection)); glProgramUniformMatrix4fv(*m_temporal_program, 3, 1, GL_FALSE, glm::value_ptr(viewtoworld)); - glProgramUniformMatrix4fv(*m_temporal_program, 4, 1, GL_FALSE, glm::value_ptr(m_previous_clouds.projection_view)); - auto const previous{glm::vec3{m_previous_clouds.camera - Camera}}; + glProgramUniformMatrix4fv(*m_temporal_program, 4, 1, GL_FALSE, glm::value_ptr(previousclouds.projection_view)); + auto const previous{glm::vec3{previousclouds.camera - Camera}}; glProgramUniform3f(*m_temporal_program, 5, previous.x, previous.y, previous.z); - glProgramUniform1f(*m_temporal_program, 6, continuity(m_previous_clouds) ? 0.15f : 1.f); + glProgramUniform1f(*m_temporal_program, 6, gfx::weather_state::continuity(previousclouds, Camera) ? 0.15f : 1.f); m_temporal_program->dispatch(groups(m_view_size.x), groups(m_view_size.y), 1); glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA16F); @@ -510,31 +327,18 @@ void opengl46_weather::render_view(glm::mat4 const &Projection, glm::mat4 const glBindTextureUnit(gl::CLOUD_SHAPE_TEX, m_shape_noise); m_clouds_current = next; m_clouds_timer.end(); - m_previous_clouds = {projectionview, Camera, {}, true}; - m_clouds_view_valid = true; + previousclouds = {projectionview, Camera, {}, true}; + m_state.clouds_made(); } else - m_previous_clouds.valid = false; + previousclouds.valid = false; if (Global.gfx_fog == 1) { m_fog_timer.begin(); - auto const farthest{std::max(Global.gfx_fog_range, FOG_VOLUME_NEAR * 4.f)}; - // the cells are sampled at another point in every frame: along the depth in the Halton sequence of 2, across the view in - // those of 3 and 5 (fog_inject.comp moves the point of every cell by a value of its own) - auto const halton = [](int Index, int const Base) { - float value{0.f}, fraction{1.f}; - for (; Index > 0; Index /= Base) - { - fraction /= static_cast(Base); - value += fraction * static_cast(Index % Base); - } - return value; - }; - auto const temporal{Global.gfx_fog_temporal && continuity(m_previous_fog)}; - auto const sample{(m_frame % 64) + 1}; - glm::vec3 const jitter{temporal ? glm::vec3{halton(sample, 3), halton(sample, 5), halton(sample, 2)} : glm::vec3{0.5f}}; - glm::vec4 const slicing{FOG_VOLUME_NEAR, farthest, static_cast(m_fog_grid.z) / std::log((farthest + gl::FOG_VOLUME_BIAS) / (FOG_VOLUME_NEAR + gl::FOG_VOLUME_BIAS)), jitter.z}; + auto const temporal{Global.gfx_fog_temporal && gfx::weather_state::continuity(previousfog, Camera)}; + auto const jitter{m_state.fog_jitter(temporal)}; + auto const slicing{gfx::weather_state::fog_slicing(m_fog_grid.z, jitter)}; bind_noise(); auto const next{1 - m_fog_current}; glBindImageTexture(0, m_fog_scattering[next], 0, GL_TRUE, 0, GL_WRITE_ONLY, GL_RGBA16F); @@ -543,14 +347,14 @@ void opengl46_weather::render_view(glm::mat4 const &Projection, glm::mat4 const glProgramUniformMatrix4fv(*m_inject_program, 1, 1, GL_FALSE, glm::value_ptr(inverseprojection)); glProgramUniformMatrix4fv(*m_inject_program, 2, 1, GL_FALSE, glm::value_ptr(viewtoworld)); glProgramUniform4fv(*m_inject_program, 3, 1, glm::value_ptr(slicing)); - glProgramUniformMatrix4fv(*m_inject_program, 4, 1, GL_FALSE, glm::value_ptr(m_previous_fog.projection_view)); - auto const previous{glm::vec3{m_previous_fog.camera - Camera}}; + glProgramUniformMatrix4fv(*m_inject_program, 4, 1, GL_FALSE, glm::value_ptr(previousfog.projection_view)); + auto const previous{glm::vec3{previousfog.camera - Camera}}; glProgramUniform3f(*m_inject_program, 5, previous.x, previous.y, previous.z); auto const sunshadows{Global.gfx_fog_shadows && Global.gfx_shadowmap_enabled}; auto const share{temporal ? std::clamp(Global.gfx_fog_temporal_share, 0.02f, 1.f) : 1.f}; glProgramUniform4f(*m_inject_program, 6, share, Global.gfx_fog_lights, sunshadows ? 1.f : 0.f, Global.gfx_fog_shadows ? 1.f : 0.f); glProgramUniform1i(*m_inject_program, 7, Cablamp); - glProgramUniform2f(*m_inject_program, 8, m_previous_fog.slicing.x, m_previous_fog.slicing.z); + glProgramUniform2f(*m_inject_program, 8, previousfog.slicing.x, previousfog.slicing.z); glProgramUniform2f(*m_inject_program, 9, jitter.x, jitter.y); m_inject_program->dispatch(groups(m_fog_grid.x), groups(m_fog_grid.y), static_cast(m_fog_grid.z)); glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); @@ -565,50 +369,34 @@ void opengl46_weather::render_view(glm::mat4 const &Projection, glm::mat4 const glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA16F); m_fog_current = next; - m_fog_slicing = slicing; m_fog_timer.end(); - m_previous_fog = {projectionview, Camera, slicing, true}; - m_fog_volume_valid = true; + previousfog = {projectionview, Camera, slicing, true}; + m_state.fog_made(slicing, m_fog_grid); } else - m_previous_fog.valid = false; + previousfog.valid = false; gl::program::unbind(); } void opengl46_weather::setup_pass(bool const Volume, glm::ivec2 const &Size) { - auto const volume{Volume && m_fog_volume_valid && (Global.gfx_fog == 1)}; - m_weather.fog_volume_params = volume ? glm::vec4{1.f, m_fog_slicing.x, m_fog_slicing.z, static_cast(m_fog_grid.z)} : glm::vec4{0.f, FOG_VOLUME_NEAR, 1.f, 1.f}; - m_weather.fog_volume_target.x = 1.f / static_cast(std::max(Size.x, 1)); - m_weather.fog_volume_target.y = 1.f / static_cast(std::max(Size.y, 1)); - m_weather.fog_volume_target.z = volume ? m_fog_slicing.y : Global.gfx_fog_range; + m_state.setup_pass(Volume, Size); upload_header(); bind(); } void opengl46_weather::draw_stars(glm::mat4 const &View, float const Sunangle, double const Siderealtime, double const Latitude) { - if (!Global.gfx_stars || !m_stars_vao || m_stars_count == 0) + if (!m_stars_vao || m_stars_count == 0) return; - // the stars come out between the civil and the nautical twilight - auto const night{1.f - glm::smoothstep(-12.f, -3.f, Sunangle)}; - if (night <= 0.f) + glm::mat4 toworld, toview; + glm::vec4 params; + if (false == m_state.stars(Sunangle, Siderealtime, Latitude, View, toworld, toview, params)) return; - // the equatorial frame (z to the pole, x to the vernal equinox) to the sky of the observer (x west, y up, z north) - auto const theta{glm::radians(Siderealtime)}; - auto const phi{glm::radians(Latitude)}; - auto const st{static_cast(std::sin(theta))}, ct{static_cast(std::cos(theta))}; - auto const sp{static_cast(std::sin(phi))}, cp{static_cast(std::cos(phi))}; - glm::mat3 equatorial; - equatorial[0] = glm::vec3{st, cp * ct, -sp * ct}; - equatorial[1] = glm::vec3{-ct, cp * st, -sp * st}; - equatorial[2] = glm::vec3{0.f, sp, cp}; - auto const toworld{glm::mat4{equatorial}}; - auto const toview{glm::mat4{glm::mat3{View}}}; m_stars_program->bind(); glProgramUniformMatrix4fv(*m_stars_program, 0, 1, GL_FALSE, glm::value_ptr(toworld)); glProgramUniformMatrix4fv(*m_stars_program, 1, 1, GL_FALSE, glm::value_ptr(toview)); - glProgramUniform4f(*m_stars_program, 2, STAR_BRIGHTNESS * Global.gfx_stars_brightness * night, static_cast(std::fmod(Timer::GetTime(), 10000.0)), 2.5f, 0.f); + glProgramUniform4fv(*m_stars_program, 2, 1, glm::value_ptr(params)); glEnable(GL_PROGRAM_POINT_SIZE); glBlendFunc(GL_ONE, GL_ONE); m_stars_vao->bind(); @@ -620,9 +408,9 @@ void opengl46_weather::draw_stars(glm::mat4 const &View, float const Sunangle, d void opengl46_weather::draw_sky(glm::mat4 const &Projection, glm::mat4 const &View, bool const Mainview) { int mode{0}; - if (m_clouds_drawn) + if (m_state.clouds_drawn()) { - if (Mainview && m_clouds_view_valid) + if (Mainview && m_state.clouds_view_valid()) { mode = 1; glBindTextureUnit(gl::CLOUD_TEX, m_clouds_history[m_clouds_current]); @@ -674,28 +462,5 @@ std::string opengl46_weather::info_times() void opengl46_weather::draw_debug_ui() { - char const *fogmodes[]{"analytic (along the ray, as the old fog)", "volumetric"}; - ImGui::Combo("fog (gfx.fog)", &Global.gfx_fog, fogmodes, IM_ARRAYSIZE(fogmodes)); - char const *levels[]{"low", "medium", "high", "ultra"}; - auto fogquality{std::clamp(Global.gfx_fog_quality, 1, 4) - 1}; - if (ImGui::Combo("fog volume quality (gfx.fog.quality)", &fogquality, levels, IM_ARRAYSIZE(levels))) - Global.gfx_fog_quality = fogquality + 1; - ImGui::SliderFloat("fog volume depth, m (gfx.fog.range)", &Global.gfx_fog_range, 100.f, 10000.f, "%.0f", ImGuiSliderFlags_Logarithmic); - ImGui::SliderFloat("lamps in the fog (gfx.fog.lights)", &Global.gfx_fog_lights, 0.f, 4.f); - ImGui::Checkbox("shadows of the sun and headlights in the fog (gfx.fog.shadows)", &Global.gfx_fog_shadows); - ImGui::Checkbox("fog blended over frames (gfx.fog.temporal)", &Global.gfx_fog_temporal); - ImGui::SliderFloat("share of a new frame in the fog (gfx.fog.temporal.share)", &Global.gfx_fog_temporal_share, 0.02f, 1.f); - char const *cloudlevels[]{"none", "low", "medium", "high", "ultra"}; - ImGui::Combo("clouds (gfx.clouds)", &Global.gfx_clouds, cloudlevels, IM_ARRAYSIZE(cloudlevels)); - ImGui::SliderFloat("cloud distance, m (gfx.clouds.distance)", &Global.gfx_clouds_distance, 5000.f, 200000.f, "%.0f", ImGuiSliderFlags_Logarithmic); - ImGui::Checkbox("cloud shadows (gfx.clouds.shadows)", &Global.gfx_clouds_shadows); - ImGui::SliderFloat("cloud shadow map, m (gfx.clouds.shadows.range)", &Global.gfx_clouds_shadows_range, 1000.f, 100000.f, "%.0f", ImGuiSliderFlags_Logarithmic); - ImGui::SliderFloat("cloud sunlight (tuning, not saved)", &m_cloud_sun_scale, 0.25f, 3.f); - ImGui::SliderFloat("cloud sky light (tuning, not saved)", &m_cloud_sky_scale, 0.f, 3.f); - ImGui::SliderFloat("cloud edge detail (tuning, not saved)", &m_cloud_detail_scale, 0.f, 3.f); - ImGui::Checkbox("stars (gfx.stars)", &Global.gfx_stars); - ImGui::SliderFloat("star brightness", &Global.gfx_stars_brightness, 0.f, 8.f); - ImGui::Text("fog range %.0f m, sun share of the fog light %.2f, clouds let through %.2f on average", m_fog_range, m_sun_share, m_average_transmittance); - ImGui::Separator(); - ui::weather_controls(simulation::Environment.weather()); + m_state.draw_debug_ui(); } diff --git a/rendering/opengl46/opengl46weather.h b/rendering/opengl46/opengl46weather.h index ad7ec839..d31327d5 100644 --- a/rendering/opengl46/opengl46weather.h +++ b/rendering/opengl46/opengl46weather.h @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "gl/vao.h" #include "gl/buffer.h" #include "gl/query.h" +#include "rendering/common/weatherstate.h" class opengl46_sky; @@ -64,22 +65,22 @@ class opengl46_weather // true when the volumetric clouds are drawn (the sky leaves its own overcast look then) bool clouds_drawn() const { - return m_clouds_drawn; + return m_state.clouds_drawn(); } // true when the clouds cast shadows (the sunlight isn't dimmed by the overcast then, the shadows do it) bool cloud_shadows() const { - return m_shadows_valid; + return m_state.cloud_shadows(); } // fog density of a regular draw (model_ubs.fog_density): 1 / the fog range float fog_density() const { - return 1.f / m_fog_range; + return m_state.fog_density(); } // distance the fog hides things at, for the draw range: a dark object fades to about 2% there float fog_range() const { - return m_fog_range; + return m_state.fog_range(); } // lines for the gpu times of the debug panel std::string info_times(); @@ -87,16 +88,7 @@ class opengl46_weather void draw_debug_ui(); private: - struct quality_level - { - int divisor; // of the resolution of the clouds of the main view - int steps; // samples through a layer at most - int light_steps; // samples towards the sun - bool detail; // with the small noise - glm::ivec2 panorama; // size of the panorama - glm::ivec3 fog_grid; // cells of the fog volume - }; - static quality_level quality(int const Clouds, int const Fog); + using quality_level = gfx::weather_state::quality_level; static GLuint create_texture(GLenum Target, GLenum Format, glm::ivec3 const &Size, GLint Filter, GLint Wrap); static void release(GLuint &Texture); @@ -119,8 +111,8 @@ class opengl46_weather std::unique_ptr m_stars_program; std::unique_ptr m_weather_ubo; - gl::weather_ubs m_weather{}; - bool m_horizon_on_gpu{false}; + // the block and the decisions of the frame, shared with the vulkan renderer + gfx::weather_state m_state; // noise GLuint m_shape_noise{0}; @@ -130,7 +122,6 @@ class opengl46_weather // shadows of the clouds GLuint m_shadow_map{0}; int m_shadow_size{0}; - bool m_shadows_valid{false}; gl::gpu_timer m_shadow_timer; // clouds of the main view @@ -140,51 +131,21 @@ class opengl46_weather GLuint m_clouds_depth{0}; std::array m_clouds_history{0, 0}; int m_clouds_current{0}; // the history holding the latest result - bool m_clouds_view_valid{false}; // render_view() made the clouds of the frame gl::gpu_timer m_clouds_timer; // panorama GLuint m_panorama{0}; glm::ivec2 m_panorama_size{0}; - int m_panorama_band{0}; - bool m_panorama_full{false}; // every band was drawn once since it was made gl::gpu_timer m_panorama_timer; - bool m_clouds_drawn{false}; // fog volume glm::ivec3 m_fog_grid{0}; std::array m_fog_scattering{0, 0}; int m_fog_current{0}; GLuint m_fog_integrated{0}; - bool m_fog_volume_valid{false}; - float m_fog_range{5000.f}; - glm::vec4 m_fog_slicing{0.f}; gl::gpu_timer m_fog_timer; - // the camera of the previous frame of the main view, for the blending over frames - struct view_state - { - glm::mat4 projection_view{1.f}; // world axes to clip space, camera at the origin - glm::dvec3 camera{0.0}; - glm::vec4 slicing{0.f}; - bool valid{false}; - }; - view_state m_previous_clouds; - view_state m_previous_fog; - int m_frame{0}; - // stars std::optional m_stars_vao; std::optional m_stars_buffer; int m_stars_count{0}; - - // tuning of the look of the clouds in the debug window, not saved: scales of their sunlight, of their sky light and of the - // erosion of their edges by the detail noise (weather_sun_color.w, weather_ambient.w, cloud_wind.w) - float m_cloud_sun_scale{1.f}; - float m_cloud_sky_scale{1.f}; - float m_cloud_detail_scale{1.f}; - // what the debug window shows - float m_sun_share{0.f}; - float m_average_transmittance{1.f}; - std::string m_report; - char m_report_input[256]{}; }; diff --git a/rendering/vulkan14/vulkan14ao.cpp b/rendering/vulkan14/vulkan14ao.cpp new file mode 100644 index 00000000..eb8b8819 --- /dev/null +++ b/rendering/vulkan14/vulkan14ao.cpp @@ -0,0 +1,214 @@ +/* +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/. +*/ + +// the ambient occlusion of the vulkan renderer (the stage P3 of the plan, opengl46_ao and Render_ambient_occlusion() of the +// opengl renderer): ground truth ambient occlusion at half the resolution in two compute kernels (gtao_main.comp, +// gtao_denoise.comp) or the older ssao (postfx_ssao.frag and its blur), multiplied into the image of the scene +// (postfx_ssao_apply.frag) with the fog in front of the surfaces taken into account + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14context.h" +#include "rendering/vulkan14/vulkan14commands.h" +#include "utilities/Globals.h" + +#include + +namespace +{ + +// work group of the compute passes, 8x8 (gtao_*.comp) +std::uint32_t const GROUP_SIZE{8}; + +struct gtao_constants +{ + glm::mat4 inv_projection; + glm::vec2 params; + glm::vec2 pad; + glm::vec4 clipw_row; + glm::ivec2 quality; +}; +static_assert(offsetof(gtao_constants, clipw_row) == 80, "the layout of the constants of gtao_main.comp"); + +VkPipelineStageFlags2 const compute_stage{VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT}; +VkPipelineStageFlags2 const sampled_stage{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT}; +VkAccessFlags2 const sampled_access{VK_ACCESS_2_SHADER_SAMPLED_READ_BIT}; + +} // namespace + +bool vulkan14_renderer::init_ao(std::string &Reason) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto const device{ctx.device.handle()}; + // the noise of the older ssao: 4x4 random rotations in the plane of the surface, drawn after the hemisphere kernel the opengl + // renderer makes with the same generator (the shader makes the kernel itself), so they are the same numbers + { + std::uniform_real_distribution rnd(0.0f, 1.0f); + std::default_random_engine gen(42); // NOSONAR fixed seed on purpose, the same as the opengl renderer + for (int i = 0; i < 32; i++) + { + glm::vec3 s(rnd(gen) * 2.0f - 1.0f, rnd(gen) * 2.0f - 1.0f, rnd(gen)); + s = glm::normalize(s) * rnd(gen); + } + std::vector noise; + for (int i = 0; i < 16; i++) + { + noise.emplace_back(glm::normalize(glm::vec3(rnd(gen) * 2.0f - 1.0f, rnd(gen) * 2.0f - 1.0f, 0.0f)), 1.f); + } + vk14::image_desc desc; + desc.format = VK_FORMAT_R32G32B32A32_SFLOAT; + desc.extent = {4, 4, 1}; + desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + scene.ssao_noise = std::make_shared(); + if ((false == scene.ssao_noise->create(ctx.memory, desc, "ssao noise")) || (false == ctx.uploads.upload(scene.ssao_noise, noise.data(), noise.size() * sizeof(glm::vec4), {{0, {4, 4, 1}}}, false))) + { + Reason = "no image of the noise of the ssao"; + return false; + } + } + try + { + auto kernel = [&](vk14::compute_kernel &Kernel, char const *Shader, char const *Name) { + if (false == Kernel.create(device, ctx.pipelines, ctx.shaders.get(Shader), scene.pass_layout, {}, Name)) + { + Reason = std::string("no compute pipeline of ") + Name; + return false; + } + return true; + }; + if ((false == kernel(scene.gtao_main, "gtao_main.comp", "gtao")) || (false == kernel(scene.gtao_denoise, "gtao_denoise.comp", "gtao denoise"))) + { + return false; + } + scene.ao_apply = std::make_shared(); + scene.ao_apply->vertex = ctx.shaders.get("quad.vert", "#define QUAD_SCENE 1\n"); + scene.ao_apply->fragment = ctx.shaders.get("postfx_ssao_apply.frag"); + scene.ao_apply->fragment_name = "postfx_ssao_apply.frag"; + scene.ao_apply->vertices = vk14::vertex_kind::none; + scene.ao_apply->name = "quad.vert + postfx_ssao_apply.frag"; + for (auto const *name : {"postfx_ssao.frag", "postfx_ssao_blur.frag"}) + { + scene.post_fragments.emplace(name, ctx.shaders.get(name)); + } + } + catch (gfx::shader_exception const &e) + { + Reason = std::string("invalid shader: ") + e.what(); + return false; + } + return true; +} + +void vulkan14_renderer::destroy_ao() +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto const device{ctx.device.handle()}; + scene.gtao_main.destroy(device); + scene.gtao_denoise.destroy(device); + scene.ao_apply.reset(); + scene.ssao_noise.reset(); +} + +// the ambient occlusion of the viewport from the depth of its scene (resolved), multiplied into its colour +// (opengl46_renderer::Render_ambient_occlusion()). Mode: Global.gfx_postfx_ao. outside a rendering +void vulkan14_renderer::render_ao(std::size_t const Index, int const Mode) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &targets{scene.targets[Index]}; + auto const commands{ctx.commands}; + if ((false == bool(targets.ssao)) || (false == bool(targets.ssao_blur))) + { + return; + } + // the projection the depth was drawn with + auto const &projection{m_colorpass.pass_camera.projection()}; + auto const inverseprojection{glm::inverse(projection)}; + auto const &depth{*targets.depth}; + if (Mode == 1) + { + // the older ssao, which reads the projection from its constants + vk14::draw_constants constants; + for (int column = 0; column < 4; ++column) + { + constants.data[column] = projection[column]; + } + post_pass("postfx_ssao.frag", *targets.ssao, {targets.depth.get(), scene.ssao_noise.get()}, constants, nullptr, 2u); + post_pass("postfx_ssao_blur.frag", *targets.ssao_blur, {targets.ssao.get()}, vk14::draw_constants{}); + } + else + { + auto const width{targets.ssao->desc().extent.width}; + auto const height{targets.ssao->desc().extent.height}; + auto const groups_x{(width + GROUP_SIZE - 1) / GROUP_SIZE}; + auto const groups_y{(height + GROUP_SIZE - 1) / GROUP_SIZE}; + // horizon search + vk14::barriers(commands, {vk14::use_of(*targets.depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access), + vk14::use_of(*targets.ssao, VK_IMAGE_LAYOUT_GENERAL, compute_stage, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT)}); + gtao_constants constants{}; + constants.inv_projection = inverseprojection; + constants.params = glm::vec2{Global.gfx_postfx_ao_radius, projection[0][0] * 0.5f * static_cast(width)}; + constants.clipw_row = glm::vec4{projection[0][3], projection[1][3], projection[2][3], projection[3][3]}; + constants.quality = glm::ivec2{Global.gfx_postfx_ao_slices, Global.gfx_postfx_ao_steps}; + auto const depthimage{vk14::image_descriptor(ctx.sampling.clamp_nearest, depth.view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)}; + scene.gtao_main.dispatch(commands, {depthimage, vk14::storage_image(targets.ssao->view())}, constants, groups_x, groups_y); + // smoothing + vk14::barriers(commands, {vk14::use_of(*targets.ssao, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access), + vk14::use_of(*targets.ssao_blur, VK_IMAGE_LAYOUT_GENERAL, compute_stage, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT)}); + scene.gtao_denoise.dispatch(commands, {depthimage, vk14::image_descriptor(ctx.sampling.clamp_nearest, targets.ssao->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL), vk14::storage_image(targets.ssao_blur->view())}, + inverseprojection, groups_x, groups_y); + } + // result = image * occlusion, drawn over the colour of the scene with the pass set and the weather of its colour pass + vk14::barriers(commands, {vk14::use_of(*targets.ssao_blur, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access), vk14::use_of(*targets.depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access), + vk14::use_of(*targets.colour, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT)}); + VkRenderingAttachmentInfo attachment{}; + attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; + attachment.imageView = targets.colour->view(); + attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + VkRenderingInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + rendering.renderArea = {{0, 0}, targets.extent}; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &attachment; + commands->begin_rendering(&rendering); + auto &draw{*m_draw}; + auto const set{m_viewports[Index].main ? vk14::scene_state::set_colour : vk14::scene_state::set_plain}; + start_draws(targets.post_formats, static_cast(targets.extent.width), static_cast(targets.extent.height), set, false); + draw.glare = true; // no depth + draw.cullface = false; + if (bind_scene_pipeline(*scene.ao_apply, targets.post_formats, vk14::blend_mode::multiply, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP)) + { + apply_draw_state(); + draw.block = push_draw_block(); + if (draw.block != ~0u) + { + bind_pass_set(); + std::array images; + images.fill(VkDescriptorImageInfo{ctx.sampling.clamp_nearest, scene.white->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}); + images[0] = VkDescriptorImageInfo{ctx.sampling.clamp_nearest, depth.view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + images[1] = VkDescriptorImageInfo{ctx.sampling.clamp_nearest, targets.ssao_blur->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + scene.material_set.push(commands, scene.scene_layout, images.data()); + vk14::draw_constants constants; + constants.draw = draw.block; + for (int column = 0; column < 4; ++column) + { + constants.data[column] = inverseprojection[column]; + } + constants.data[4] = glm::vec4{(Mode == 1 ? 1.f : 0.f), Global.gfx_postfx_ao_strength, Global.gfx_postfx_ao_bounce, 0.f}; + push_constants(&constants); + commands->draw(4, 1, 0, 0); + } + } + commands->end_rendering(); + end_draws(); +} diff --git a/rendering/vulkan14/vulkan14bindings.h b/rendering/vulkan14/vulkan14bindings.h index ebca9d2a..fc1ccd40 100644 --- a/rendering/vulkan14/vulkan14bindings.h +++ b/rendering/vulkan14/vulkan14bindings.h @@ -47,7 +47,31 @@ constexpr std::uint32_t CLUSTER_INDEX_SSBO = 6; constexpr std::uint32_t SHADOW_TEX = 7; constexpr std::uint32_t ENV_TEX = 8; constexpr std::uint32_t HEADLIGHT_TEX = 9; -constexpr std::uint32_t PASS_BINDING_COUNT = 10; +// the light of the sky (the stage P1): the directional part of the ambient light (gl::sky_data), the parameters of the +// atmosphere (gl::atmosphere_ubs), its sky-view table, the environment map prefiltered with the GGX lobe +constexpr std::uint32_t SKY_SSBO = 10; +constexpr std::uint32_t ATMOSPHERE_UBO = 11; +constexpr std::uint32_t SKY_VIEW_TEX = 12; +constexpr std::uint32_t ENV_GGX_TEX = 13; +// the weather (the stage P2): its block (gl::weather_ubs without the colours of the horizon, dynamic offset: it differs from +// pass to pass), the colours of the fog towards the horizon (written by fog_horizon.comp or by the cpu), the fog volume of the +// main view, the shadows of the clouds and the noises of the clouds +constexpr std::uint32_t WEATHER_UBO = 14; +constexpr std::uint32_t WEATHER_HORIZON_UBO = 15; +constexpr std::uint32_t FOG_VOLUME_TEX = 16; +constexpr std::uint32_t CLOUD_SHADOW_TEX = 17; +constexpr std::uint32_t CLOUD_SHAPE_TEX = 18; +constexpr std::uint32_t CLOUD_DETAIL_TEX = 19; +constexpr std::uint32_t CLOUD_COVERAGE_TEX = 20; +// the shadows of the lamps (the stage P4): the cube maps of the cab lamp and of the scenery lamps, the maps of the headlights +constexpr std::uint32_t LOCAL_SHADOW_TEX = 21; +constexpr std::uint32_t LOCAL_SHADOW_LAMPS_TEX = 22; +constexpr std::uint32_t HEADLIGHT_SHADOW_TEX = 23; +// the depth of the opaque scene drawn before its colour (the stage P3: the contact shadows; the soft particles of P6) +constexpr std::uint32_t SCENE_DEPTH_TEX = 24; +constexpr std::uint32_t PASS_BINDING_COUNT = 25; +// the offsets of the bindings above with dynamic offsets, in the order of the bindings +constexpr std::uint32_t PASS_DYNAMIC_OFFSETS = 6; // the textures of a material take the bindings of their units (#pragma texture(name, unit, ...)), 0 .. MAX_TEXTURES - 1 constexpr std::uint32_t MATERIAL_TEXTURE_FIRST = 0; @@ -59,9 +83,16 @@ constexpr std::uint32_t CL_INDEX_SSBO = 3; constexpr std::uint32_t CL_COUNTER_SSBO = 4; constexpr std::uint32_t CL_LIGHT_SSBO = 5; -// the post-processing and the copies of whole images: set 0 pushed, the images it reads from binding 0 on +// the post-processing and the copies of whole images: set 0 pushed, the images it reads from binding 0 on, and the exposure +// worked out on the gpu (gl::exposure_data, the stage P1) constexpr std::uint32_t POST_SET = 0; constexpr std::uint32_t POST_TEXTURE_FIRST = 0; +constexpr std::uint32_t POST_TEXTURES = 4; +constexpr std::uint32_t POST_EXPOSURE_SSBO = 4; + +// the compute kernels (vk14::compute_kernel): set 0 the pass set of the scene, set 1 pushed with the dispatch, its bindings +// those the shader declares from 0 on +constexpr std::uint32_t COMPUTE_SET = 1; // the push constants of the graphics pipelines of the scene and of the post-processing (vk14::draw_constants): the draw, and // the parameters of the special shaders (the celestial bodies, the post-processing) diff --git a/rendering/vulkan14/vulkan14commands.cpp b/rendering/vulkan14/vulkan14commands.cpp new file mode 100644 index 00000000..5038a6d7 --- /dev/null +++ b/rendering/vulkan14/vulkan14commands.cpp @@ -0,0 +1,1004 @@ +/* +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/vulkan14/vulkan14commands.h" + +#include +#include +#include + +namespace vk14 +{ + +namespace +{ + +enum call_type : std::uint32_t +{ + call_begin_rendering, + call_end_rendering, + call_pipeline_barrier, + call_bind_pipeline, + call_bind_descriptor_sets, + call_push_descriptors, + call_push_constants, + call_bind_vertex_buffers, + call_bind_index_buffer, + call_draw, + call_draw_indexed, + call_draw_indirect, + call_draw_indexed_indirect, + call_dispatch, + call_dispatch_indirect, + call_set_viewport, + call_set_scissor, + call_set_cull_mode, + call_set_front_face, + call_set_topology, + call_set_depth_test, + call_set_depth_write, + call_set_depth_compare, + call_set_depth_bias_enable, + call_set_depth_bias, + call_set_primitive_restart, + call_set_stencil_test, + call_fill_buffer, + call_update_buffer, + call_copy_buffer, + call_copy_image, + call_copy_buffer_to_image, + call_copy_image_to_buffer, + call_blit_image, + call_clear_color_image, + call_clear_depth_stencil_image, + call_write_timestamp, + call_reset_query_pool, + call_begin_query, + call_end_query, + call_begin_label, + call_end_label +}; + +struct call_header +{ + std::uint32_t type; + std::uint32_t size; // bytes of the call with its header and its data, a multiple of 8 +}; +static_assert(sizeof(call_header) == 8, "the header of a call of a stream"); + +std::size_t constexpr no_data{~static_cast(0)}; + +struct rendering_call +{ + VkRenderingInfo info; + std::size_t colours; + std::size_t depth; + std::size_t stencil; +}; +struct barrier_call +{ + VkDependencyFlags flags; + std::uint32_t memory_count, buffer_count, image_count; + std::size_t memory, buffers, images; +}; +struct pipeline_call +{ + VkPipelineBindPoint bind_point; + VkPipeline pipeline; +}; +struct sets_call +{ + VkBindDescriptorSetsInfo info; + std::size_t sets; + std::size_t offsets; +}; +struct pushed_call +{ + VkPushDescriptorSetWithTemplateInfo info; + std::size_t data; +}; +struct constants_call +{ + VkPushConstantsInfo info; + std::size_t data; +}; +struct vertex_buffers_call +{ + std::uint32_t first, count; + std::size_t buffers, offsets; +}; +struct index_buffer_call +{ + VkBuffer buffer; + VkDeviceSize offset; + VkIndexType type; +}; +struct draw_call +{ + std::uint32_t vertices, instances, first_vertex, first_instance; +}; +struct draw_indexed_call +{ + std::uint32_t indices, instances, first_index; + std::int32_t vertex_offset; + std::uint32_t first_instance; +}; +struct indirect_call +{ + VkBuffer buffer; + VkDeviceSize offset; + std::uint32_t count, stride; +}; +struct dispatch_call +{ + std::uint32_t x, y, z; +}; +struct viewports_call +{ + std::uint32_t first, count; + std::size_t data; +}; +struct value_call +{ + std::uint32_t value; +}; +struct bias_call +{ + float constant, clamp, slope; +}; +struct fill_call +{ + VkBuffer buffer; + VkDeviceSize offset, size; + std::uint32_t data; +}; +struct update_call +{ + VkBuffer buffer; + VkDeviceSize offset, size; + std::size_t data; +}; +// the copies and the blit: the info with its regions +template struct copy_call +{ + Info_ info; + std::size_t regions; +}; +struct clear_call +{ + VkImage image; + VkImageLayout layout; + VkClearColorValue colour; + VkClearDepthStencilValue depth; + std::uint32_t count; + std::size_t ranges; +}; +struct query_call +{ + VkPipelineStageFlags2 stage; + VkQueryPool pool; + std::uint32_t query, count; + VkQueryControlFlags flags; +}; +struct label_call +{ + float colour[4]; + std::size_t name; +}; + +bool compute_stages(VkShaderStageFlags const Stages) +{ + return (Stages & VK_SHADER_STAGE_COMPUTE_BIT) != 0; +} +bool graphics_stages(VkShaderStageFlags const Stages) +{ + return (Stages & ~static_cast(VK_SHADER_STAGE_COMPUTE_BIT)) != 0; +} + +} // namespace + +void command_stream::clear() +{ + m_data.clear(); + m_chunks.clear(); + m_chunks.emplace_back(); + m_state.fill(no_call); + m_pushes.clear(); + m_rendering = no_call; + m_queries = 0; + m_work = 0; + m_calls = 0; + m_finished = false; +} + +std::size_t command_stream::add(std::uint32_t const Type, std::size_t const Size) +{ + if (m_chunks.empty()) + { + clear(); + } + auto const call{m_data.size()}; + auto const bytes{(header_size + Size + 7) & ~static_cast(7)}; + m_data.resize(call + bytes); + auto &header{*reinterpret_cast(m_data.data() + call)}; + header.type = Type; + header.size = static_cast(bytes); + ++m_calls; + return call; +} + +std::size_t command_stream::append(void const *Data, std::size_t const Bytes) +{ + if ((Data == nullptr) || (Bytes == 0)) + { + return no_data; + } + auto const offset{m_data.size()}; + m_data.resize(offset + ((Bytes + 7) & ~static_cast(7))); + std::memcpy(m_data.data() + offset, Data, Bytes); + return offset; +} + +void command_stream::close(std::size_t const Call) +{ + reinterpret_cast(m_data.data() + Call)->size = static_cast(m_data.size() - Call); +} + +void command_stream::work() +{ + if ((++m_work >= chunk_work) && (m_queries == 0)) + { + cut(); + } +} + +void command_stream::cut() +{ + auto ¤t{m_chunks.back()}; + current.end = m_data.size(); + current.suspended = m_rendering; + chunk next; + next.first = m_data.size(); + next.resumed = m_rendering; + // the state the next chunk starts with: the last call of each kind, in the order they were made + for (auto const call : m_state) + { + if (call != no_call) + { + next.state.push_back(call); + } + } + next.state.insert(next.state.end(), m_pushes.begin(), m_pushes.end()); + std::sort(next.state.begin(), next.state.end()); + next.state.erase(std::unique(next.state.begin(), next.state.end()), next.state.end()); + m_chunks.push_back(std::move(next)); + m_work = 0; +} + +void command_stream::finish() +{ + if (m_chunks.empty()) + { + clear(); + } + m_chunks.back().end = m_data.size(); + m_chunks.back().suspended = no_call; + // a chunk left without calls + if ((m_chunks.size() > 1) && (m_chunks.back().first == m_chunks.back().end)) + { + m_chunks.pop_back(); + m_chunks.back().suspended = no_call; + } + m_finished = true; +} + +// the calls + +void command_stream::begin_rendering(VkRenderingInfo const *Info) +{ + auto const call{add(call_begin_rendering, sizeof(rendering_call))}; + auto const colours{append(Info->pColorAttachments, sizeof(VkRenderingAttachmentInfo) * Info->colorAttachmentCount)}; + auto const depth{append(Info->pDepthAttachment, Info->pDepthAttachment != nullptr ? sizeof(VkRenderingAttachmentInfo) : 0)}; + auto const stencil{append(Info->pStencilAttachment, Info->pStencilAttachment != nullptr ? sizeof(VkRenderingAttachmentInfo) : 0)}; + auto &data{payload(call)}; + data.info = *Info; + assert(Info->pNext == nullptr); + data.info.pNext = nullptr; + data.info.pColorAttachments = nullptr; + data.info.pDepthAttachment = nullptr; + data.info.pStencilAttachment = nullptr; + data.colours = colours; + data.depth = depth; + data.stencil = stencil; + close(call); + m_rendering = call; +} + +void command_stream::end_rendering() +{ + add(call_end_rendering, 0); + m_rendering = no_call; + work(); +} + +void command_stream::pipeline_barrier2(VkDependencyInfo const *Dependency) +{ + assert(Dependency->pNext == nullptr); + auto const call{add(call_pipeline_barrier, sizeof(barrier_call))}; + auto const memory{append(Dependency->pMemoryBarriers, sizeof(VkMemoryBarrier2) * Dependency->memoryBarrierCount)}; + auto const buffers{append(Dependency->pBufferMemoryBarriers, sizeof(VkBufferMemoryBarrier2) * Dependency->bufferMemoryBarrierCount)}; + auto const images{append(Dependency->pImageMemoryBarriers, sizeof(VkImageMemoryBarrier2) * Dependency->imageMemoryBarrierCount)}; + auto &data{payload(call)}; + data.flags = Dependency->dependencyFlags; + data.memory_count = Dependency->memoryBarrierCount; + data.buffer_count = Dependency->bufferMemoryBarrierCount; + data.image_count = Dependency->imageMemoryBarrierCount; + data.memory = memory; + data.buffers = buffers; + data.images = images; + close(call); +} + +void command_stream::bind_pipeline(VkPipelineBindPoint const BindPoint, VkPipeline const Pipeline) +{ + auto const call{add(call_bind_pipeline, sizeof(pipeline_call))}; + payload(call) = pipeline_call{BindPoint, Pipeline}; + remember(BindPoint == VK_PIPELINE_BIND_POINT_COMPUTE ? pipeline_compute : pipeline_graphics, call); +} + +void command_stream::bind_descriptor_sets2(VkBindDescriptorSetsInfo const *Info) +{ + auto const call{add(call_bind_descriptor_sets, sizeof(sets_call))}; + auto const sets{append(Info->pDescriptorSets, sizeof(VkDescriptorSet) * Info->descriptorSetCount)}; + auto const offsets{append(Info->pDynamicOffsets, sizeof(std::uint32_t) * Info->dynamicOffsetCount)}; + auto &data{payload(call)}; + data.info = *Info; + assert(Info->pNext == nullptr); + data.info.pNext = nullptr; + data.info.pDescriptorSets = nullptr; + data.info.pDynamicOffsets = nullptr; + data.sets = sets; + data.offsets = offsets; + close(call); + assert(Info->firstSet + Info->descriptorSetCount <= set_slots); + for (std::uint32_t set = Info->firstSet; (set < Info->firstSet + Info->descriptorSetCount) && (set < set_slots); ++set) + { + // a bind for the stages of both kinds stands for both + if (compute_stages(Info->stageFlags)) + { + remember(sets_compute + set, call); + } + if (graphics_stages(Info->stageFlags)) + { + remember(sets_graphics + set, call); + } + } +} + +void command_stream::push_descriptor_set_with_template2(VkPushDescriptorSetWithTemplateInfo const *Info, std::size_t const Size, VkPipelineBindPoint const BindPoint) +{ + auto const call{add(call_push_descriptors, sizeof(pushed_call))}; + auto const data{append(Info->pData, Size)}; + auto &call_data{payload(call)}; + call_data.info = *Info; + assert(Info->pNext == nullptr); + call_data.info.pNext = nullptr; + call_data.info.pData = nullptr; + call_data.data = data; + close(call); + assert(Info->set < set_slots); + if (Info->set < set_slots) + { + remember((BindPoint == VK_PIPELINE_BIND_POINT_COMPUTE ? pushed_compute : pushed_graphics) + Info->set, call); + } +} + +void command_stream::push_constants2(VkPushConstantsInfo const *Info) +{ + auto const call{add(call_push_constants, sizeof(constants_call))}; + auto const data{append(Info->pValues, Info->size)}; + auto &call_data{payload(call)}; + call_data.info = *Info; + assert(Info->pNext == nullptr); + call_data.info.pNext = nullptr; + call_data.info.pValues = nullptr; + call_data.data = data; + close(call); + // a push of a part of the constants leaves the rest as the pushes before it made them: the last push of each range and stages + // is kept, the pushes are made again in their order + m_pushes.erase(std::remove_if(m_pushes.begin(), m_pushes.end(), + [&](std::size_t const Push) { + auto const &info{payload(Push).info}; + return (info.offset == Info->offset) && (info.size == Info->size) && (info.stageFlags == Info->stageFlags); + }), + m_pushes.end()); + m_pushes.push_back(call); +} + +void command_stream::bind_vertex_buffers(std::uint32_t const First, std::uint32_t const Count, VkBuffer const *Buffers, VkDeviceSize const *Offsets) +{ + auto const call{add(call_bind_vertex_buffers, sizeof(vertex_buffers_call))}; + auto const buffers{append(Buffers, sizeof(VkBuffer) * Count)}; + auto const offsets{append(Offsets, sizeof(VkDeviceSize) * Count)}; + payload(call) = vertex_buffers_call{First, Count, buffers, offsets}; + close(call); + assert(First + Count <= vertex_slots); + for (std::uint32_t binding = First; (binding < First + Count) && (binding < vertex_slots); ++binding) + { + remember(vertex_buffers + binding, call); + } +} + +void command_stream::bind_index_buffer(VkBuffer const Buffer, VkDeviceSize const Offset, VkIndexType const Type) +{ + auto const call{add(call_bind_index_buffer, sizeof(index_buffer_call))}; + payload(call) = index_buffer_call{Buffer, Offset, Type}; + remember(index_buffer, call); +} + +void command_stream::draw(std::uint32_t const Vertices, std::uint32_t const Instances, std::uint32_t const FirstVertex, std::uint32_t const FirstInstance) +{ + auto const call{add(call_draw, sizeof(draw_call))}; + payload(call) = draw_call{Vertices, Instances, FirstVertex, FirstInstance}; + work(); +} + +void command_stream::draw_indexed(std::uint32_t const Indices, std::uint32_t const Instances, std::uint32_t const FirstIndex, std::int32_t const VertexOffset, std::uint32_t const FirstInstance) +{ + auto const call{add(call_draw_indexed, sizeof(draw_indexed_call))}; + payload(call) = draw_indexed_call{Indices, Instances, FirstIndex, VertexOffset, FirstInstance}; + work(); +} + +void command_stream::draw_indirect(VkBuffer const Buffer, VkDeviceSize const Offset, std::uint32_t const Count, std::uint32_t const Stride) +{ + auto const call{add(call_draw_indirect, sizeof(indirect_call))}; + payload(call) = indirect_call{Buffer, Offset, Count, Stride}; + work(); +} + +void command_stream::draw_indexed_indirect(VkBuffer const Buffer, VkDeviceSize const Offset, std::uint32_t const Count, std::uint32_t const Stride) +{ + auto const call{add(call_draw_indexed_indirect, sizeof(indirect_call))}; + payload(call) = indirect_call{Buffer, Offset, Count, Stride}; + work(); +} + +void command_stream::dispatch(std::uint32_t const X, std::uint32_t const Y, std::uint32_t const Z) +{ + auto const call{add(call_dispatch, sizeof(dispatch_call))}; + payload(call) = dispatch_call{X, Y, Z}; + work(); +} + +void command_stream::dispatch_indirect(VkBuffer const Buffer, VkDeviceSize const Offset) +{ + auto const call{add(call_dispatch_indirect, sizeof(indirect_call))}; + payload(call) = indirect_call{Buffer, Offset, 0, 0}; + work(); +} + +void command_stream::set_viewport(std::uint32_t const First, std::uint32_t const Count, VkViewport const *Viewports) +{ + auto const call{add(call_set_viewport, sizeof(viewports_call))}; + auto const data{append(Viewports, sizeof(VkViewport) * Count)}; + payload(call) = viewports_call{First, Count, data}; + close(call); + remember(viewport, call); +} + +void command_stream::set_scissor(std::uint32_t const First, std::uint32_t const Count, VkRect2D const *Scissors) +{ + auto const call{add(call_set_scissor, sizeof(viewports_call))}; + auto const data{append(Scissors, sizeof(VkRect2D) * Count)}; + payload(call) = viewports_call{First, Count, data}; + close(call); + remember(scissor, call); +} + +#define VK14_VALUE_CALL(Name, Type, Call, Slot) \ + void command_stream::Name(Type const Value) \ + { \ + auto const call{add(Call, sizeof(value_call))}; \ + payload(call) = value_call{static_cast(Value)}; \ + remember(Slot, call); \ + } +VK14_VALUE_CALL(set_cull_mode, VkCullModeFlags, call_set_cull_mode, cull_mode) +VK14_VALUE_CALL(set_front_face, VkFrontFace, call_set_front_face, front_face) +VK14_VALUE_CALL(set_primitive_topology, VkPrimitiveTopology, call_set_topology, topology) +VK14_VALUE_CALL(set_depth_test_enable, VkBool32, call_set_depth_test, depth_test) +VK14_VALUE_CALL(set_depth_write_enable, VkBool32, call_set_depth_write, depth_write) +VK14_VALUE_CALL(set_depth_compare_op, VkCompareOp, call_set_depth_compare, depth_compare) +VK14_VALUE_CALL(set_depth_bias_enable, VkBool32, call_set_depth_bias_enable, depth_bias_enable) +VK14_VALUE_CALL(set_primitive_restart_enable, VkBool32, call_set_primitive_restart, primitive_restart) +VK14_VALUE_CALL(set_stencil_test_enable, VkBool32, call_set_stencil_test, stencil_test) +#undef VK14_VALUE_CALL + +void command_stream::set_depth_bias(float const Constant, float const Clamp, float const Slope) +{ + auto const call{add(call_set_depth_bias, sizeof(bias_call))}; + payload(call) = bias_call{Constant, Clamp, Slope}; + remember(depth_bias, call); +} + +void command_stream::fill_buffer(VkBuffer const Buffer, VkDeviceSize const Offset, VkDeviceSize const Size, std::uint32_t const Data) +{ + auto const call{add(call_fill_buffer, sizeof(fill_call))}; + payload(call) = fill_call{Buffer, Offset, Size, Data}; + work(); +} + +void command_stream::update_buffer(VkBuffer const Buffer, VkDeviceSize const Offset, VkDeviceSize const Size, void const *Data) +{ + auto const call{add(call_update_buffer, sizeof(update_call))}; + auto const data{append(Data, static_cast(Size))}; + payload(call) = update_call{Buffer, Offset, Size, data}; + close(call); + work(); +} + +void command_stream::copy_buffer2(VkCopyBufferInfo2 const *Info) +{ + auto const call{add(call_copy_buffer, sizeof(copy_call))}; + auto const regions{append(Info->pRegions, sizeof(VkBufferCopy2) * Info->regionCount)}; + auto &data{payload>(call)}; + data.info = *Info; + assert(Info->pNext == nullptr); + data.info.pNext = nullptr; + data.info.pRegions = nullptr; + data.regions = regions; + close(call); + work(); +} + +void command_stream::copy_image2(VkCopyImageInfo2 const *Info) +{ + auto const call{add(call_copy_image, sizeof(copy_call))}; + auto const regions{append(Info->pRegions, sizeof(VkImageCopy2) * Info->regionCount)}; + auto &data{payload>(call)}; + data.info = *Info; + assert(Info->pNext == nullptr); + data.info.pNext = nullptr; + data.info.pRegions = nullptr; + data.regions = regions; + close(call); + work(); +} + +void command_stream::copy_buffer_to_image2(VkCopyBufferToImageInfo2 const *Info) +{ + auto const call{add(call_copy_buffer_to_image, sizeof(copy_call))}; + auto const regions{append(Info->pRegions, sizeof(VkBufferImageCopy2) * Info->regionCount)}; + auto &data{payload>(call)}; + data.info = *Info; + assert(Info->pNext == nullptr); + data.info.pNext = nullptr; + data.info.pRegions = nullptr; + data.regions = regions; + close(call); + work(); +} + +void command_stream::copy_image_to_buffer2(VkCopyImageToBufferInfo2 const *Info) +{ + auto const call{add(call_copy_image_to_buffer, sizeof(copy_call))}; + auto const regions{append(Info->pRegions, sizeof(VkBufferImageCopy2) * Info->regionCount)}; + auto &data{payload>(call)}; + data.info = *Info; + assert(Info->pNext == nullptr); + data.info.pNext = nullptr; + data.info.pRegions = nullptr; + data.regions = regions; + close(call); + work(); +} + +void command_stream::blit_image2(VkBlitImageInfo2 const *Info) +{ + auto const call{add(call_blit_image, sizeof(copy_call))}; + auto const regions{append(Info->pRegions, sizeof(VkImageBlit2) * Info->regionCount)}; + auto &data{payload>(call)}; + data.info = *Info; + assert(Info->pNext == nullptr); + data.info.pNext = nullptr; + data.info.pRegions = nullptr; + data.regions = regions; + close(call); + work(); +} + +void command_stream::clear_color_image(VkImage const Image, VkImageLayout const Layout, VkClearColorValue const *Color, std::uint32_t const Count, VkImageSubresourceRange const *Ranges) +{ + auto const call{add(call_clear_color_image, sizeof(clear_call))}; + auto const ranges{append(Ranges, sizeof(VkImageSubresourceRange) * Count)}; + auto &data{payload(call)}; + data.image = Image; + data.layout = Layout; + data.colour = *Color; + data.depth = {}; + data.count = Count; + data.ranges = ranges; + close(call); + work(); +} + +void command_stream::clear_depth_stencil_image(VkImage const Image, VkImageLayout const Layout, VkClearDepthStencilValue const *Value, std::uint32_t const Count, VkImageSubresourceRange const *Ranges) +{ + auto const call{add(call_clear_depth_stencil_image, sizeof(clear_call))}; + auto const ranges{append(Ranges, sizeof(VkImageSubresourceRange) * Count)}; + auto &data{payload(call)}; + data.image = Image; + data.layout = Layout; + data.colour = {}; + data.depth = *Value; + data.count = Count; + data.ranges = ranges; + close(call); + work(); +} + +void command_stream::write_timestamp2(VkPipelineStageFlags2 const Stage, VkQueryPool const Pool, std::uint32_t const Query) +{ + auto const call{add(call_write_timestamp, sizeof(query_call))}; + payload(call) = query_call{Stage, Pool, Query, 0, 0}; +} + +void command_stream::reset_query_pool(VkQueryPool const Pool, std::uint32_t const First, std::uint32_t const Count) +{ + auto const call{add(call_reset_query_pool, sizeof(query_call))}; + payload(call) = query_call{VK_PIPELINE_STAGE_2_NONE, Pool, First, Count, 0}; +} + +void command_stream::begin_query(VkQueryPool const Pool, std::uint32_t const Query, VkQueryControlFlags const Flags) +{ + auto const call{add(call_begin_query, sizeof(query_call))}; + payload(call) = query_call{VK_PIPELINE_STAGE_2_NONE, Pool, Query, 0, Flags}; + // a query doesn't go on in another command buffer: no cut while it's active + ++m_queries; +} + +void command_stream::end_query(VkQueryPool const Pool, std::uint32_t const Query) +{ + auto const call{add(call_end_query, sizeof(query_call))}; + payload(call) = query_call{VK_PIPELINE_STAGE_2_NONE, Pool, Query, 0, 0}; + if (m_queries > 0) + { + --m_queries; + } +} + +void command_stream::begin_debug_utils_label(VkDebugUtilsLabelEXT const *Label) +{ + auto const call{add(call_begin_label, sizeof(label_call))}; + auto const name{append(Label->pLabelName, (Label->pLabelName != nullptr) ? std::strlen(Label->pLabelName) + 1 : 0)}; + auto &data{payload(call)}; + std::copy(std::begin(Label->color), std::end(Label->color), data.colour); + data.name = name; + close(call); +} + +void command_stream::end_debug_utils_label() +{ + add(call_end_label, 0); +} + +// the replay + +void command_stream::execute(std::size_t const Call, VkCommandBuffer const Buffer, VkRenderingFlags const Flags) const +{ + auto const &header{*reinterpret_cast(m_data.data() + Call)}; + switch (header.type) + { + case call_begin_rendering: + { + auto const &data{payload(Call)}; + auto info{data.info}; + info.flags |= Flags; + info.pColorAttachments = (data.colours != no_data ? at(data.colours) : nullptr); + info.pDepthAttachment = (data.depth != no_data ? at(data.depth) : nullptr); + info.pStencilAttachment = (data.stencil != no_data ? at(data.stencil) : nullptr); + vkCmdBeginRendering(Buffer, &info); + break; + } + case call_end_rendering: + vkCmdEndRendering(Buffer); + break; + case call_pipeline_barrier: + { + auto const &data{payload(Call)}; + VkDependencyInfo dependency{}; + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency.dependencyFlags = data.flags; + dependency.memoryBarrierCount = data.memory_count; + dependency.pMemoryBarriers = (data.memory != no_data ? at(data.memory) : nullptr); + dependency.bufferMemoryBarrierCount = data.buffer_count; + dependency.pBufferMemoryBarriers = (data.buffers != no_data ? at(data.buffers) : nullptr); + dependency.imageMemoryBarrierCount = data.image_count; + dependency.pImageMemoryBarriers = (data.images != no_data ? at(data.images) : nullptr); + vkCmdPipelineBarrier2(Buffer, &dependency); + break; + } + case call_bind_pipeline: + { + auto const &data{payload(Call)}; + vkCmdBindPipeline(Buffer, data.bind_point, data.pipeline); + break; + } + case call_bind_descriptor_sets: + { + auto const &data{payload(Call)}; + auto info{data.info}; + info.pDescriptorSets = (data.sets != no_data ? at(data.sets) : nullptr); + info.pDynamicOffsets = (data.offsets != no_data ? at(data.offsets) : nullptr); + vkCmdBindDescriptorSets2(Buffer, &info); + break; + } + case call_push_descriptors: + { + auto const &data{payload(Call)}; + auto info{data.info}; + info.pData = (data.data != no_data ? m_data.data() + data.data : nullptr); + vkCmdPushDescriptorSetWithTemplate2(Buffer, &info); + break; + } + case call_push_constants: + { + auto const &data{payload(Call)}; + auto info{data.info}; + info.pValues = (data.data != no_data ? m_data.data() + data.data : nullptr); + vkCmdPushConstants2(Buffer, &info); + break; + } + case call_bind_vertex_buffers: + { + auto const &data{payload(Call)}; + vkCmdBindVertexBuffers(Buffer, data.first, data.count, at(data.buffers), at(data.offsets)); + break; + } + case call_bind_index_buffer: + { + auto const &data{payload(Call)}; + vkCmdBindIndexBuffer(Buffer, data.buffer, data.offset, data.type); + break; + } + case call_draw: + { + auto const &data{payload(Call)}; + vkCmdDraw(Buffer, data.vertices, data.instances, data.first_vertex, data.first_instance); + break; + } + case call_draw_indexed: + { + auto const &data{payload(Call)}; + vkCmdDrawIndexed(Buffer, data.indices, data.instances, data.first_index, data.vertex_offset, data.first_instance); + break; + } + case call_draw_indirect: + { + auto const &data{payload(Call)}; + vkCmdDrawIndirect(Buffer, data.buffer, data.offset, data.count, data.stride); + break; + } + case call_draw_indexed_indirect: + { + auto const &data{payload(Call)}; + vkCmdDrawIndexedIndirect(Buffer, data.buffer, data.offset, data.count, data.stride); + break; + } + case call_dispatch: + { + auto const &data{payload(Call)}; + vkCmdDispatch(Buffer, data.x, data.y, data.z); + break; + } + case call_dispatch_indirect: + { + auto const &data{payload(Call)}; + vkCmdDispatchIndirect(Buffer, data.buffer, data.offset); + break; + } + case call_set_viewport: + { + auto const &data{payload(Call)}; + vkCmdSetViewport(Buffer, data.first, data.count, at(data.data)); + break; + } + case call_set_scissor: + { + auto const &data{payload(Call)}; + vkCmdSetScissor(Buffer, data.first, data.count, at(data.data)); + break; + } + case call_set_cull_mode: + vkCmdSetCullMode(Buffer, static_cast(payload(Call).value)); + break; + case call_set_front_face: + vkCmdSetFrontFace(Buffer, static_cast(payload(Call).value)); + break; + case call_set_topology: + vkCmdSetPrimitiveTopology(Buffer, static_cast(payload(Call).value)); + break; + case call_set_depth_test: + vkCmdSetDepthTestEnable(Buffer, static_cast(payload(Call).value)); + break; + case call_set_depth_write: + vkCmdSetDepthWriteEnable(Buffer, static_cast(payload(Call).value)); + break; + case call_set_depth_compare: + vkCmdSetDepthCompareOp(Buffer, static_cast(payload(Call).value)); + break; + case call_set_depth_bias_enable: + vkCmdSetDepthBiasEnable(Buffer, static_cast(payload(Call).value)); + break; + case call_set_depth_bias: + { + auto const &data{payload(Call)}; + vkCmdSetDepthBias(Buffer, data.constant, data.clamp, data.slope); + break; + } + case call_set_primitive_restart: + vkCmdSetPrimitiveRestartEnable(Buffer, static_cast(payload(Call).value)); + break; + case call_set_stencil_test: + vkCmdSetStencilTestEnable(Buffer, static_cast(payload(Call).value)); + break; + case call_fill_buffer: + { + auto const &data{payload(Call)}; + vkCmdFillBuffer(Buffer, data.buffer, data.offset, data.size, data.data); + break; + } + case call_update_buffer: + { + auto const &data{payload(Call)}; + vkCmdUpdateBuffer(Buffer, data.buffer, data.offset, data.size, m_data.data() + data.data); + break; + } + case call_copy_buffer: + { + auto const &data{payload>(Call)}; + auto info{data.info}; + info.pRegions = at(data.regions); + vkCmdCopyBuffer2(Buffer, &info); + break; + } + case call_copy_image: + { + auto const &data{payload>(Call)}; + auto info{data.info}; + info.pRegions = at(data.regions); + vkCmdCopyImage2(Buffer, &info); + break; + } + case call_copy_buffer_to_image: + { + auto const &data{payload>(Call)}; + auto info{data.info}; + info.pRegions = at(data.regions); + vkCmdCopyBufferToImage2(Buffer, &info); + break; + } + case call_copy_image_to_buffer: + { + auto const &data{payload>(Call)}; + auto info{data.info}; + info.pRegions = at(data.regions); + vkCmdCopyImageToBuffer2(Buffer, &info); + break; + } + case call_blit_image: + { + auto const &data{payload>(Call)}; + auto info{data.info}; + info.pRegions = at(data.regions); + vkCmdBlitImage2(Buffer, &info); + break; + } + case call_clear_color_image: + { + auto const &data{payload(Call)}; + vkCmdClearColorImage(Buffer, data.image, data.layout, &data.colour, data.count, at(data.ranges)); + break; + } + case call_clear_depth_stencil_image: + { + auto const &data{payload(Call)}; + vkCmdClearDepthStencilImage(Buffer, data.image, data.layout, &data.depth, data.count, at(data.ranges)); + break; + } + case call_write_timestamp: + { + auto const &data{payload(Call)}; + vkCmdWriteTimestamp2(Buffer, data.stage, data.pool, data.query); + break; + } + case call_reset_query_pool: + { + auto const &data{payload(Call)}; + vkCmdResetQueryPool(Buffer, data.pool, data.query, data.count); + break; + } + case call_begin_query: + { + auto const &data{payload(Call)}; + vkCmdBeginQuery(Buffer, data.pool, data.query, data.flags); + break; + } + case call_end_query: + { + auto const &data{payload(Call)}; + vkCmdEndQuery(Buffer, data.pool, data.query); + break; + } + case call_begin_label: + { + if (vkCmdBeginDebugUtilsLabelEXT != nullptr) + { + auto const &data{payload(Call)}; + VkDebugUtilsLabelEXT label{}; + label.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT; + label.pLabelName = (data.name != no_data ? at(data.name) : ""); + std::copy(std::begin(data.colour), std::end(data.colour), label.color); + vkCmdBeginDebugUtilsLabelEXT(Buffer, &label); + } + break; + } + case call_end_label: + if (vkCmdEndDebugUtilsLabelEXT != nullptr) + { + vkCmdEndDebugUtilsLabelEXT(Buffer); + } + break; + default: + break; + } +} + +void command_stream::replay(std::size_t const Chunk, VkCommandBuffer const Buffer) const +{ + if (Chunk >= m_chunks.size()) + { + return; + } + auto const &chunk{m_chunks[Chunk]}; + if (chunk.resumed != no_call) + { + // the rendering the chunk before suspended, and the state it left + execute(chunk.resumed, Buffer, VK_RENDERING_RESUMING_BIT | (chunk.suspended == chunk.resumed ? VK_RENDERING_SUSPENDING_BIT : 0)); + } + for (auto const call : chunk.state) + { + execute(call, Buffer); + } + for (auto call = chunk.first; call < chunk.end;) + { + auto const &header{*reinterpret_cast(m_data.data() + call)}; + // the rendering going on in the next chunk is suspended + execute(call, Buffer, ((header.type == call_begin_rendering) && (call == chunk.suspended)) ? VK_RENDERING_SUSPENDING_BIT : 0); + call += header.size; + } + if (chunk.suspended != no_call) + { + vkCmdEndRendering(Buffer); + } +} + +void command_stream::replay(VkCommandBuffer const Buffer) const +{ + // the chunks one after another, the cuts made whole again: each call as it was made + for (auto call = std::size_t{0}; call < m_data.size();) + { + auto const &header{*reinterpret_cast(m_data.data() + call)}; + execute(call, Buffer); + call += header.size; + } +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14commands.h b/rendering/vulkan14/vulkan14commands.h new file mode 100644 index 00000000..e9147b12 --- /dev/null +++ b/rendering/vulkan14/vulkan14commands.h @@ -0,0 +1,186 @@ +/* +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/. +*/ + +// the commands of a frame kept to be recorded into command buffers later (the stage M1 of the plan): the renderer makes the calls +// of vkCmd* on a stream, which keeps them with their data in the order they were made. the stream is cut into chunks as it's +// made, every so many draws: a chunk is recorded into a command buffer of its own, by a worker thread, and the command buffers go +// to the queue in one submit in the order of the chunks. a chunk which starts inside a rendering begins it again resumed +// (VK_RENDERING_RESUMING_BIT, the one before it ended it suspended) and sets again the state the draws before it left: the +// pipelines, the descriptor sets, the pushed descriptors and constants, the vertex and index buffers and the dynamic state, as +// the last calls of each kind made them. where the cuts are doesn't depend on the number of the threads, so the gpu gets the +// same commands with any of them + +#pragma once + +#include "rendering/vulkan14/vulkan14common.h" + +#include +#include +#include + +namespace vk14 +{ + +class command_stream +{ + public: + // the draws, dispatches and copies a chunk holds at least before it's cut (inside a rendering, between two draws) + static constexpr std::uint32_t chunk_work{256}; + + // the calls, with the arguments vkCmd* takes (the command buffer left out); the data they point to is copied. chains of pNext + // aren't kept: none of the calls is made with one + void begin_rendering(VkRenderingInfo const *Info); + void end_rendering(); + void pipeline_barrier2(VkDependencyInfo const *Dependency); + void bind_pipeline(VkPipelineBindPoint BindPoint, VkPipeline Pipeline); + void bind_descriptor_sets2(VkBindDescriptorSetsInfo const *Info); + // Size: the bytes of the data of the template; BindPoint: the one the template was made for + void push_descriptor_set_with_template2(VkPushDescriptorSetWithTemplateInfo const *Info, std::size_t Size, VkPipelineBindPoint BindPoint); + void push_constants2(VkPushConstantsInfo const *Info); + void bind_vertex_buffers(std::uint32_t First, std::uint32_t Count, VkBuffer const *Buffers, VkDeviceSize const *Offsets); + void bind_index_buffer(VkBuffer Buffer, VkDeviceSize Offset, VkIndexType Type); + void draw(std::uint32_t Vertices, std::uint32_t Instances, std::uint32_t FirstVertex, std::uint32_t FirstInstance); + void draw_indexed(std::uint32_t Indices, std::uint32_t Instances, std::uint32_t FirstIndex, std::int32_t VertexOffset, std::uint32_t FirstInstance); + void draw_indirect(VkBuffer Buffer, VkDeviceSize Offset, std::uint32_t Count, std::uint32_t Stride); + void draw_indexed_indirect(VkBuffer Buffer, VkDeviceSize Offset, std::uint32_t Count, std::uint32_t Stride); + void dispatch(std::uint32_t X, std::uint32_t Y, std::uint32_t Z); + void dispatch_indirect(VkBuffer Buffer, VkDeviceSize Offset); + void set_viewport(std::uint32_t First, std::uint32_t Count, VkViewport const *Viewports); + void set_scissor(std::uint32_t First, std::uint32_t Count, VkRect2D const *Scissors); + void set_cull_mode(VkCullModeFlags Mode); + void set_front_face(VkFrontFace Face); + void set_primitive_topology(VkPrimitiveTopology Topology); + void set_depth_test_enable(VkBool32 Enable); + void set_depth_write_enable(VkBool32 Enable); + void set_depth_compare_op(VkCompareOp Op); + void set_depth_bias_enable(VkBool32 Enable); + void set_depth_bias(float Constant, float Clamp, float Slope); + void set_primitive_restart_enable(VkBool32 Enable); + void set_stencil_test_enable(VkBool32 Enable); + void fill_buffer(VkBuffer Buffer, VkDeviceSize Offset, VkDeviceSize Size, std::uint32_t Data); + void update_buffer(VkBuffer Buffer, VkDeviceSize Offset, VkDeviceSize Size, void const *Data); + void copy_buffer2(VkCopyBufferInfo2 const *Info); + void copy_image2(VkCopyImageInfo2 const *Info); + void copy_buffer_to_image2(VkCopyBufferToImageInfo2 const *Info); + void copy_image_to_buffer2(VkCopyImageToBufferInfo2 const *Info); + void blit_image2(VkBlitImageInfo2 const *Info); + void clear_color_image(VkImage Image, VkImageLayout Layout, VkClearColorValue const *Color, std::uint32_t Count, VkImageSubresourceRange const *Ranges); + void clear_depth_stencil_image(VkImage Image, VkImageLayout Layout, VkClearDepthStencilValue const *Value, std::uint32_t Count, VkImageSubresourceRange const *Ranges); + void write_timestamp2(VkPipelineStageFlags2 Stage, VkQueryPool Pool, std::uint32_t Query); + void reset_query_pool(VkQueryPool Pool, std::uint32_t First, std::uint32_t Count); + void begin_query(VkQueryPool Pool, std::uint32_t Query, VkQueryControlFlags Flags); + void end_query(VkQueryPool Pool, std::uint32_t Query); + void begin_debug_utils_label(VkDebugUtilsLabelEXT const *Label); + void end_debug_utils_label(); + + // the stream is made anew + void clear(); + bool empty() const + { + return m_chunks.empty() || (m_data.empty()); + } + // the stream is ended: no calls after it + void finish(); + // the chunks, valid after finish() + std::size_t chunks() const + { + return m_chunks.size(); + } + // the calls recorded since the start + std::size_t calls() const + { + return m_calls; + } + // the chunk recorded into Buffer (begun already, ended by the caller) + void replay(std::size_t Chunk, VkCommandBuffer Buffer) const; + // all of it into one command buffer + void replay(VkCommandBuffer Buffer) const; + + private: + // the calls of the state a chunk sets again; the dynamic states have a slot each + enum state_slot : std::uint32_t + { + pipeline_graphics, + pipeline_compute, + viewport, + scissor, + cull_mode, + front_face, + topology, + depth_test, + depth_write, + depth_compare, + depth_bias_enable, + depth_bias, + primitive_restart, + stencil_test, + index_buffer, + sets_graphics, // set_slots of each + sets_compute = sets_graphics + 8, + pushed_graphics = sets_compute + 8, + pushed_compute = pushed_graphics + 8, + vertex_buffers = pushed_compute + 8, // vertex_slots + slot_count = vertex_buffers + 8 + }; + static constexpr std::size_t no_call{~static_cast(0)}; + static constexpr std::uint32_t set_slots{8}; + static constexpr std::uint32_t vertex_slots{8}; + struct chunk + { + std::size_t first{0}; // offset of its first call in the data + std::size_t end{0}; // past its last one + // the begin of the rendering the chunk starts inside of, resumed, and the calls of the state set again, in their order + std::size_t resumed{no_call}; + std::vector state; + // the begin of the rendering the chunk ends inside of, suspended + std::size_t suspended{no_call}; + }; + + // room for a call of Type with Size bytes of its own data after its header, aligned; the offset of the call + std::size_t add(std::uint32_t Type, std::size_t Size); + template Type_ &payload(std::size_t Call) + { + return *reinterpret_cast(m_data.data() + Call + header_size); + } + template Type_ const &payload(std::size_t Call) const + { + return *reinterpret_cast(m_data.data() + Call + header_size); + } + template Type_ const *at(std::size_t Offset) const + { + return reinterpret_cast(m_data.data() + Offset); + } + // copies Bytes of Data after the call made last (aligned), returns their offset in the data + std::size_t append(void const *Data, std::size_t Bytes); + // the call made last ends here + void close(std::size_t Call); + void remember(std::uint32_t Slot, std::size_t Call) + { + m_state[Slot] = Call; + } + // a draw, dispatch or copy was added: the chunk is cut after it when it has enough of them + void work(); + void cut(); + // Flags: added to those of a begin of a rendering + void execute(std::size_t Call, VkCommandBuffer Buffer, VkRenderingFlags Flags = 0) const; + + static constexpr std::size_t header_size{8}; + std::vector m_data; + std::vector m_chunks; + std::array m_state{}; + // the pushes of the constants: the last one of each range and stages, in their order + std::vector m_pushes; + std::size_t m_rendering{no_call}; // the begin of the rendering under way + std::uint32_t m_queries{0}; // queries active + std::uint32_t m_work{0}; // of the chunk under way + std::size_t m_calls{0}; + bool m_finished{false}; +}; + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14common.cpp b/rendering/vulkan14/vulkan14common.cpp index 936f9395..731bb80c 100644 --- a/rendering/vulkan14/vulkan14common.cpp +++ b/rendering/vulkan14/vulkan14common.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/vulkan14/vulkan14common.h" +#include "rendering/vulkan14/vulkan14commands.h" #include "utilities/Logs.h" namespace vk14 @@ -143,7 +144,7 @@ void name_object(VkDevice Device, VkObjectType Type, std::uint64_t Handle, std:: vkSetDebugUtilsObjectNameEXT(Device, &info); } -void begin_label(VkCommandBuffer Commands, char const *Name) +void begin_label(command_stream *Commands, char const *Name) { if ((false == debug_utils_enabled) || (vkCmdBeginDebugUtilsLabelEXT == nullptr)) { @@ -152,16 +153,16 @@ void begin_label(VkCommandBuffer Commands, char const *Name) VkDebugUtilsLabelEXT label{}; label.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT; label.pLabelName = Name; - vkCmdBeginDebugUtilsLabelEXT(Commands, &label); + Commands->begin_debug_utils_label(&label); } -void end_label(VkCommandBuffer Commands) +void end_label(command_stream *Commands) { if ((false == debug_utils_enabled) || (vkCmdEndDebugUtilsLabelEXT == nullptr)) { return; } - vkCmdEndDebugUtilsLabelEXT(Commands); + Commands->end_debug_utils_label(); } void log_info(std::string const &Message) diff --git a/rendering/vulkan14/vulkan14common.h b/rendering/vulkan14/vulkan14common.h index be3ca65b..d8f57b79 100644 --- a/rendering/vulkan14/vulkan14common.h +++ b/rendering/vulkan14/vulkan14common.h @@ -22,6 +22,9 @@ extern "C" GLFWAPI int glfwGetPhysicalDevicePresentationSupport(VkInstance insta namespace vk14 { +// the commands of a frame, recorded later into command buffers (vulkan14commands.h) +class command_stream; + // human readable VkResult, for the log char const *result_string(VkResult Result); // "1.4.309" @@ -48,8 +51,8 @@ template std::uint64_t handle_value(Handle_ Handle) // a name of an object for the messages of the validation layer and for captures; nothing without debug utils void name_object(VkDevice Device, VkObjectType Type, std::uint64_t Handle, std::string const &Name); // a label of a part of the commands (a pass), for captures and the messages of the validation layer -void begin_label(VkCommandBuffer Commands, char const *Name); -void end_label(VkCommandBuffer Commands); +void begin_label(command_stream *Commands, char const *Name); +void end_label(command_stream *Commands); // the log of the renderer, with the "vulkan: " prefix void log_info(std::string const &Message); diff --git a/rendering/vulkan14/vulkan14compute.cpp b/rendering/vulkan14/vulkan14compute.cpp new file mode 100644 index 00000000..e1c7ba64 --- /dev/null +++ b/rendering/vulkan14/vulkan14compute.cpp @@ -0,0 +1,395 @@ +/* +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/vulkan14/vulkan14compute.h" +#include "rendering/vulkan14/vulkan14commands.h" + +#include +#include + +namespace vk14 +{ + +std::vector reflect_bindings(std::vector const &Spirv, std::uint32_t const Set, VkShaderStageFlags const Stages) +{ + std::uint32_t constexpr op_constant{43}; + std::uint32_t constexpr op_type_image{25}; + std::uint32_t constexpr op_type_sampler{26}; + std::uint32_t constexpr op_type_sampled_image{27}; + std::uint32_t constexpr op_type_array{28}; + std::uint32_t constexpr op_type_runtime_array{29}; + std::uint32_t constexpr op_type_struct{30}; + std::uint32_t constexpr op_type_pointer{32}; + std::uint32_t constexpr op_variable{59}; + std::uint32_t constexpr op_decorate{71}; + std::uint32_t constexpr decoration_block{2}; + std::uint32_t constexpr decoration_buffer_block{3}; + std::uint32_t constexpr decoration_binding{33}; + std::uint32_t constexpr decoration_set{34}; + std::uint32_t constexpr storage_uniform_constant{0}; + std::uint32_t constexpr storage_uniform{2}; + std::uint32_t constexpr storage_storage_buffer{12}; + + struct type_info + { + std::uint32_t opcode{0}; + std::uint32_t operand1{0}; // pointer: the type pointed to; array: the element; sampled image: the image + std::uint32_t operand2{0}; // pointer: the storage class; array: the length; image: sampled + std::uint32_t dim{0}; + }; + std::unordered_map types; + std::unordered_map constants; + std::unordered_map sets; + std::unordered_map bindingsof; + std::unordered_map blocks; // struct -> decoration + std::vector> variables; // id, pointer type + + for (std::size_t word = 5; word < Spirv.size();) + { + auto const count{Spirv[word] >> 16}; + auto const opcode{Spirv[word] & 0xffff}; + if ((count == 0) || (word + count > Spirv.size())) + { + break; + } + auto const *op{&Spirv[word]}; + switch (opcode) + { + case op_decorate: + if (count >= 3) + { + if ((op[2] == decoration_set) && (count >= 4)) + { + sets[op[1]] = op[3]; + } + else if ((op[2] == decoration_binding) && (count >= 4)) + { + bindingsof[op[1]] = op[3]; + } + else if ((op[2] == decoration_block) || (op[2] == decoration_buffer_block)) + { + blocks[op[1]] = op[2]; + } + } + break; + case op_constant: + if (count >= 4) + { + constants[op[2]] = op[3]; + } + break; + case op_type_image: + if (count >= 9) + { + types[op[1]] = {opcode, 0, op[7], op[3]}; + } + break; + case op_type_sampler: + types[op[1]] = {opcode, 0, 0, 0}; + break; + case op_type_sampled_image: + types[op[1]] = {opcode, op[2], 0, 0}; + break; + case op_type_array: + types[op[1]] = {opcode, op[2], op[3], 0}; + break; + case op_type_runtime_array: + types[op[1]] = {opcode, op[2], 0, 0}; + break; + case op_type_struct: + types[op[1]] = {opcode, 0, 0, 0}; + break; + case op_type_pointer: + types[op[1]] = {opcode, op[3], op[2], 0}; + break; + case op_variable: + if (count >= 4) + { + variables.emplace_back(op[2], op[1]); + } + break; + default: + break; + } + word += count; + } + + std::map result; + for (auto const &[id, pointertype] : variables) + { + auto const set{sets.find(id)}; + auto const binding{bindingsof.find(id)}; + if ((set == sets.end()) || (binding == bindingsof.end()) || (set->second != Set)) + { + continue; + } + auto const pointer{types.find(pointertype)}; + if (pointer == types.end()) + { + continue; + } + auto const storage{pointer->second.operand2}; + auto type{pointer->second.operand1}; + std::uint32_t descriptors{1}; + auto lookup{types.find(type)}; + if ((lookup != types.end()) && (lookup->second.opcode == op_type_array)) + { + auto const length{constants.find(lookup->second.operand2)}; + descriptors = (length != constants.end() ? length->second : 1); + type = lookup->second.operand1; + lookup = types.find(type); + } + if (lookup == types.end()) + { + continue; + } + auto descriptortype{VK_DESCRIPTOR_TYPE_MAX_ENUM}; + switch (lookup->second.opcode) + { + case op_type_sampled_image: + descriptortype = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + break; + case op_type_image: + descriptortype = (lookup->second.operand2 == 2 ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE : VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE); + break; + case op_type_sampler: + descriptortype = VK_DESCRIPTOR_TYPE_SAMPLER; + break; + case op_type_struct: + { + auto const block{blocks.find(type)}; + if (storage == storage_storage_buffer) + { + descriptortype = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + } + else if (storage == storage_uniform) + { + descriptortype = ((block != blocks.end()) && (block->second == decoration_buffer_block) ? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER : VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER); + } + break; + } + default: + break; + } + if ((descriptortype == VK_DESCRIPTOR_TYPE_MAX_ENUM) || ((storage != storage_uniform_constant) && (storage != storage_uniform) && (storage != storage_storage_buffer))) + { + continue; + } + result[binding->second] = VkDescriptorSetLayoutBinding{binding->second, descriptortype, descriptors, Stages, nullptr}; + } + std::vector bindings; + for (auto const &entry : result) + { + bindings.push_back(entry.second); + } + return bindings; +} + +bool compute_kernel::create(VkDevice Device, pipeline_library &Pipelines, std::shared_ptr const &Shader, VkDescriptorSetLayout Passlayout, std::vector const &Specialization, std::string const &Name) +{ + m_name = Name; + auto const bindings{reflect_bindings(Shader->spirv, COMPUTE_PUSH_SET, VK_SHADER_STAGE_COMPUTE_BIT)}; + for (std::size_t i = 0; i < bindings.size(); ++i) + { + // the data of the template is read in the order of the bindings, one descriptor each + if ((bindings[i].binding != i) || (bindings[i].descriptorCount != 1)) + { + log_error("the compute shader of " + Name + " has to take the bindings of its pushed set one by one from 0"); + return false; + } + } + m_count = bindings.size(); + std::vector sets{Passlayout}; + if (m_count > 0) + { + if (false == m_set.create(Device, bindings, COMPUTE_PUSH_SET, VK_PIPELINE_BIND_POINT_COMPUTE, sizeof(descriptor), Name)) + { + return false; + } + sets.push_back(m_set.layout); + } + VkPushConstantRange const constants{VK_SHADER_STAGE_COMPUTE_BIT, 0, COMPUTE_PUSH_CONSTANTS}; + m_layout = make_pipeline_layout(Device, sets, {constants}, Name); + if (m_layout == VK_NULL_HANDLE) + { + return false; + } + if ((m_count > 0) && (false == m_set.create_template(Device, m_layout, Name))) + { + return false; + } + m_pipeline = Pipelines.compute(Shader, m_layout, Specialization, Name); + return m_pipeline != VK_NULL_HANDLE; +} + +void compute_kernel::destroy(VkDevice Device) +{ + m_set.destroy(Device); + if (m_layout != VK_NULL_HANDLE) + { + vkDestroyPipelineLayout(Device, m_layout, nullptr); + m_layout = VK_NULL_HANDLE; + } + // the pipeline belongs to the pipeline library + m_pipeline = VK_NULL_HANDLE; +} + +void compute_kernel::bind(command_stream *Commands, descriptor const *Descriptors, void const *Constants, std::uint32_t const Size) const +{ + Commands->bind_pipeline(VK_PIPELINE_BIND_POINT_COMPUTE, m_pipeline); + if ((m_count > 0) && (Descriptors != nullptr)) + { + m_set.push(Commands, m_layout, Descriptors); + } + if ((Size > 0) && (Constants != nullptr)) + { + VkPushConstantsInfo info{}; + info.sType = VK_STRUCTURE_TYPE_PUSH_CONSTANTS_INFO; + info.layout = m_layout; + info.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + info.offset = 0; + info.size = Size; + info.pValues = Constants; + Commands->push_constants2(&info); + } +} + +void compute_kernel::dispatch(command_stream *Commands, descriptor const *Descriptors, void const *Constants, std::uint32_t const Size, std::uint32_t const X, std::uint32_t const Y, std::uint32_t const Z) const +{ + if ((m_pipeline == VK_NULL_HANDLE) || (X == 0) || (Y == 0) || (Z == 0)) + { + return; + } + bind(Commands, Descriptors, Constants, Size); + Commands->dispatch(X, Y, Z); +} + +void compute_kernel::dispatch_indirect(command_stream *Commands, descriptor const *Descriptors, void const *Constants, std::uint32_t const Size, VkBuffer Buffer, VkDeviceSize const Offset) const +{ + if (m_pipeline == VK_NULL_HANDLE) + { + return; + } + bind(Commands, Descriptors, Constants, Size); + Commands->dispatch_indirect(Buffer, Offset); +} + +void memory_barrier(command_stream *Commands, VkPipelineStageFlags2 const SrcStage, VkAccessFlags2 const SrcAccess, VkPipelineStageFlags2 const DstStage, VkAccessFlags2 const DstAccess) +{ + VkMemoryBarrier2 barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; + barrier.srcStageMask = SrcStage; + barrier.srcAccessMask = SrcAccess; + barrier.dstStageMask = DstStage; + barrier.dstAccessMask = DstAccess; + VkDependencyInfo dependency{}; + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency.memoryBarrierCount = 1; + dependency.pMemoryBarriers = &barrier; + Commands->pipeline_barrier2(&dependency); +} + +void compute_barrier(command_stream *Commands) +{ + memory_barrier(Commands, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_CLEAR_BIT, + VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_2_VERTEX_INPUT_BIT | VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | + VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_CLEAR_BIT, + VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_2_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_2_UNIFORM_READ_BIT | + VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT); +} + +// the buffer written with the commands of the frame, between its reads by the shaders of the frames before and after +void update_buffer(command_stream *Commands, buffer const &Target, void const *Data, VkDeviceSize const Size, VkDeviceSize const Offset) +{ + memory_barrier(Commands, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_UNIFORM_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_TRANSFER_READ_BIT, + VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT); + Commands->update_buffer(Target.handle(), Offset, Size, Data); + memory_barrier(Commands, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_UNIFORM_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT); +} + +// readback + +bool readback_ring::create(allocator const &Allocator, VkDeviceSize const Size, std::uint32_t const Frames, std::string const &Name) +{ + m_size = Size; + m_slots.clear(); + m_slots.resize(Frames); + for (std::uint32_t i = 0; i < Frames; ++i) + { + if (false == m_slots[i].data.create(Allocator, Size, VK_BUFFER_USAGE_TRANSFER_DST_BIT, memory_kind::readback, Name + " " + std::to_string(i))) + { + return false; + } + } + return true; +} + +void readback_ring::destroy() +{ + m_slots.clear(); +} + +void readback_ring::push(command_stream *Commands, std::uint32_t const Slot, std::uint64_t const Value, VkBuffer Source, VkDeviceSize const Offset, VkDeviceSize const Size) +{ + if (Slot >= m_slots.size()) + { + return; + } + auto &slot{m_slots[Slot]}; + VkBufferCopy2 region{}; + region.sType = VK_STRUCTURE_TYPE_BUFFER_COPY_2; + region.srcOffset = Offset; + region.dstOffset = 0; + region.size = std::min(Size, m_size); + VkCopyBufferInfo2 copy{}; + copy.sType = VK_STRUCTURE_TYPE_COPY_BUFFER_INFO_2; + copy.srcBuffer = Source; + copy.dstBuffer = slot.data.handle(); + copy.regionCount = 1; + copy.pRegions = ®ion; + memory_barrier(Commands, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_CLEAR_BIT | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, + VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); + Commands->copy_buffer2(©); + memory_barrier(Commands, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_HOST_BIT, VK_ACCESS_2_HOST_READ_BIT); + slot.value = Value; + slot.pending = true; +} + +bool readback_ring::pop(std::uint64_t const Completed, void *Data, VkDeviceSize const Size) +{ + slot *newest{nullptr}; + for (auto &slot : m_slots) + { + if (slot.pending && (slot.value <= Completed) && ((newest == nullptr) || (slot.value > newest->value))) + { + newest = &slot; + } + } + if (newest == nullptr) + { + return false; + } + for (auto &slot : m_slots) + { + if (slot.pending && (slot.value <= newest->value)) + { + slot.pending = false; + } + } + auto const size{std::min(Size, m_size)}; + newest->data.invalidate(0, size); + std::memcpy(Data, newest->data.mapped(), static_cast(size)); + m_popped = newest->value; + m_poppedslot = static_cast(newest - m_slots.data()); + return true; +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14compute.h b/rendering/vulkan14/vulkan14compute.h new file mode 100644 index 00000000..acc9f2ae --- /dev/null +++ b/rendering/vulkan14/vulkan14compute.h @@ -0,0 +1,156 @@ +/* +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/. +*/ + +// the compute passes of the vulkan renderer (the stages P of the plan): a kernel is a compute shader of shaders/vk14 with its +// pipeline. its layout is the pass set of the scene (set 0, the persistent one, for the shaders which read the lights, the +// clusters, the shadow maps or the weather of the pass) and a pushed set of its own (set 1), whose bindings are read from the +// SPIR-V of the shader, with push constants for the parameters the opengl renderer gives its compute programs as loose +// uniforms. the data of the gpu read back by the cpu goes through a readback ring, a buffer seen by the cpu for each frame in +// flight, taken once the gpu is done with the frame + +#pragma once + +#include "rendering/vulkan14/vulkan14common.h" +#include "rendering/vulkan14/vulkan14descriptors.h" +#include "rendering/vulkan14/vulkan14memory.h" +#include "rendering/vulkan14/vulkan14shaders.h" + +#include + +namespace vk14 +{ + +// what a template of a pushed set reads for each of its bindings: an image or a buffer, laid out alike +union descriptor +{ + VkDescriptorImageInfo image; + VkDescriptorBufferInfo buffer; +}; +static_assert(sizeof(VkDescriptorImageInfo) == sizeof(VkDescriptorBufferInfo), "the images and the buffers of the templates are laid out by one stride"); + +inline descriptor image_descriptor(VkSampler Sampler, VkImageView View, VkImageLayout Layout) +{ + descriptor result; + result.image = VkDescriptorImageInfo{Sampler, View, Layout}; + return result; +} +inline descriptor storage_image(VkImageView View) +{ + descriptor result; + result.image = VkDescriptorImageInfo{VK_NULL_HANDLE, View, VK_IMAGE_LAYOUT_GENERAL}; + return result; +} +inline descriptor buffer_descriptor(VkBuffer Buffer, VkDeviceSize Offset, VkDeviceSize Range) +{ + descriptor result; + result.buffer = VkDescriptorBufferInfo{Buffer, Offset, Range}; + return result; +} + +// the bindings of the descriptor set Set the shader uses, from the decorations of its variables, in the order of the bindings +std::vector reflect_bindings(std::vector const &Spirv, std::uint32_t Set, VkShaderStageFlags Stages); + +// the set of the compute kernels pushed with their dispatches +constexpr std::uint32_t COMPUTE_PUSH_SET = 1; +// the push constants of the kernels at most, vulkan 1.4 guarantees them +constexpr std::uint32_t COMPUTE_PUSH_CONSTANTS = 256; + +class compute_kernel +{ + public: + compute_kernel() = default; + compute_kernel(compute_kernel const &) = delete; + compute_kernel &operator=(compute_kernel const &) = delete; + + // the kernel of the shader: Passlayout is the layout of the pass set of the scene, set 0 of every kernel; the bindings of + // set 1 are those the shader declares. Specialization: the values of the constants 0, 1, 2 ... + bool create(VkDevice Device, pipeline_library &Pipelines, std::shared_ptr const &Shader, VkDescriptorSetLayout Passlayout, std::vector const &Specialization, std::string const &Name); + void destroy(VkDevice Device); + explicit operator bool() const + { + return m_pipeline != VK_NULL_HANDLE; + } + VkPipelineLayout layout() const + { + return m_layout; + } + std::size_t bindings() const + { + return m_count; + } + // the pipeline bound, the set pushed (Descriptors in the order of the bindings of the shader), the constants pushed (Size + // bytes) and the groups dispatched + void dispatch(command_stream *Commands, descriptor const *Descriptors, void const *Constants, std::uint32_t Size, std::uint32_t X, std::uint32_t Y = 1, std::uint32_t Z = 1) const; + template void dispatch(command_stream *Commands, std::vector const &Descriptors, Constants const &Data, std::uint32_t X, std::uint32_t Y = 1, std::uint32_t Z = 1) const + { + static_assert(sizeof(Constants) <= COMPUTE_PUSH_CONSTANTS, "too many push constants"); + dispatch(Commands, Descriptors.data(), &Data, static_cast(sizeof(Constants)), X, Y, Z); + } + // the same without push constants + void run(command_stream *Commands, std::vector const &Descriptors, std::uint32_t X, std::uint32_t Y = 1, std::uint32_t Z = 1) const + { + dispatch(Commands, Descriptors.data(), nullptr, 0, X, Y, Z); + } + // the same with the groups taken from a buffer + void dispatch_indirect(command_stream *Commands, descriptor const *Descriptors, void const *Constants, std::uint32_t Size, VkBuffer Buffer, VkDeviceSize Offset) const; + + private: + void bind(command_stream *Commands, descriptor const *Descriptors, void const *Constants, std::uint32_t Size) const; + + push_set m_set; + VkPipelineLayout m_layout{VK_NULL_HANDLE}; + VkPipeline m_pipeline{VK_NULL_HANDLE}; + std::size_t m_count{0}; + std::string m_name; +}; + +// a global memory barrier, between the passes which write and read buffers (the images have their own, vk14::barriers()) +void memory_barrier(command_stream *Commands, VkPipelineStageFlags2 SrcStage, VkAccessFlags2 SrcAccess, VkPipelineStageFlags2 DstStage, VkAccessFlags2 DstAccess); +// the writes of the compute passes made seen by what follows: the compute passes, the draws, the indirect commands, the copies +void compute_barrier(command_stream *Commands); +// Size bytes of Data (a multiple of 4, at most 64 KiB) written to the buffer from Offset with the commands of the frame, between +// its reads by the shaders before and after. outside a rendering +void update_buffer(command_stream *Commands, buffer const &Target, void const *Data, VkDeviceSize Size, VkDeviceSize Offset); + +// data copied from the gpu for the cpu: a buffer per frame in flight, the copy of a frame read once the gpu is done with it +class readback_ring +{ + public: + bool create(allocator const &Allocator, VkDeviceSize Size, std::uint32_t Frames, std::string const &Name); + void destroy(); + // the copy of Size bytes (at most the size of the ring) of Source from Offset, in the frame of the slot, done by the gpu once + // it reaches Value of the timeline + void push(command_stream *Commands, std::uint32_t Slot, std::uint64_t Value, VkBuffer Source, VkDeviceSize Offset, VkDeviceSize Size); + // the newest copy the gpu is done with (Completed: the value of the timeline it reached), copied to Data; false when there's + // none newer than the one taken last + bool pop(std::uint64_t Completed, void *Data, VkDeviceSize Size); + // the frame of the copy taken last, for the data which tell what they were made for + std::uint64_t popped_value() const + { + return m_popped; + } + std::uint32_t popped_slot() const + { + return m_poppedslot; + } + + private: + struct slot + { + buffer data; + std::uint64_t value{0}; + bool pending{false}; + }; + std::vector m_slots; + VkDeviceSize m_size{0}; + std::uint64_t m_popped{0}; + std::uint32_t m_poppedslot{0}; +}; + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14context.h b/rendering/vulkan14/vulkan14context.h index e1271234..0c354526 100644 --- a/rendering/vulkan14/vulkan14context.h +++ b/rendering/vulkan14/vulkan14context.h @@ -25,11 +25,23 @@ http://mozilla.org/MPL/2.0/. #include "rendering/vulkan14/vulkan14imgui.h" #include "rendering/vulkan14/vulkan14bindings.h" #include "rendering/vulkan14/vulkan14geometry.h" +#include "rendering/vulkan14/vulkan14compute.h" +#include "rendering/vulkan14/vulkan14cull.h" +#include "rendering/vulkan14/vulkan14shadows.h" +#include "rendering/vulkan14/vulkan14terrain.h" +#include "rendering/vulkan14/vulkan14grass.h" +#include "rendering/vulkan14/vulkan14particles.h" +#include "rendering/vulkan14/vulkan14commands.h" +#include "utilities/workerpool.h" #include "model/material.h" #include "gl/ubo.h" #include +#include +#include #include +#include +#include #include #include @@ -42,8 +54,10 @@ enum class vertex_kind : std::uint8_t none, // made by the vertex shader (the celestial bodies, the full screen quads) scene, // gfx::basic_vertex and the user data, the geometry banks colour, // position and colour, the sky dome + star, // direction with the magnitude and colour with the phase of the twinkling in one stream, the stars particle, // position, colour and texture coordinates in one stream, the smoke - position_uv // position and texture coordinates in two streams, the precipitation + position_uv, // position and texture coordinates in two streams, the precipitation + particle_instance // three vec4 of gl::particle_draw for each instance, the particles of the gpu }; // the push constants of the pipelines of the scene and of the post-processing (shaders/vk14/common.glsl, draw_constants_block) @@ -65,6 +79,12 @@ struct scene_program : public gfx::shader_program // the fragment shader of the passes without the velocity target (NO_MOTION_OUTPUT), made when it's first needed std::string fragment_name; std::shared_ptr fragment_single; + // the vertex shader, and the definitions both stages were made with + std::string vertex_name; + std::string defines; + // the variant of the multi draws of the instances drawn from the gpu culling (MODEL_DRAWS), made when it's first needed + std::shared_ptr multidraw; + bool multidraw_tried{false}; vertex_kind vertices{vertex_kind::scene}; std::unordered_map pipelines; }; @@ -76,6 +96,8 @@ struct target_formats std::uint32_t colours{0}; VkFormat depth{VK_FORMAT_UNDEFINED}; VkSampleCountFlagBits samples{VK_SAMPLE_COUNT_1_BIT}; + // the views drawn at once (multiview), 0: none + std::uint32_t view_mask{0}; // the number the pipelines of the targets are known by std::uint32_t id{0}; }; @@ -102,6 +124,19 @@ struct viewport_targets std::shared_ptr output; // the depth under the cursor of the editor: one sample of the depth of the ground resolved to it, and the buffer it's read back with std::shared_ptr cursor_depth; + // the depth of the opaque scene drawn before its colour, which the contact shadows read (the stage P3), and the formats of the + // pass drawing it (the depth alone) + std::shared_ptr scene_depth; + target_formats depth_formats; + // the ambient occlusion (gfx.postfx.ao, the stage P3), half the size: the raw one and the smoothed one; made when it's on + std::shared_ptr ssao; + std::shared_ptr ssao_blur; + // the formats of the passes drawn over the colour of the scene after it (the ambient occlusion): the colour alone + target_formats post_formats; + // the bloom chain (gfx.postfx.bloom, the stage P1): the levels down, half the size each, and up, one fewer + std::vector> bloom_down; + std::vector> bloom_up; + std::vector bloom_sizes; target_formats formats; target_formats output_formats; bool srgb{true}; // the output encodes the colour (gfx.shadergamma no) @@ -129,6 +164,16 @@ struct cluster_state bool stale{true}; // the boxes have to be made anew VkDeviceSize params_offset{0}; // in the ring of the frame, for the pass under way std::uint32_t lights_total{0}; + // the counters of the cullings read back, for the debug panel (gl::cluster_grid::cull_stats): entries of the index list in + // use, lights dropped from full clusters, the most of them seen + readback_ring counters_readback; + struct + { + std::uint32_t index_count{0}; + std::uint32_t overflow_count{0}; + std::uint32_t overflow_peak{0}; + bool valid{false}; + } stats; }; // a picking or a reading of a value of the depth, waiting for the gpu to be done with the frame it was made in @@ -140,6 +185,104 @@ struct readback glm::ivec2 position{0}; }; +// the light of the sky on the gpu (the stage P1, opengl46_sky): the buffers the shading reads (the directional ambient light, +// the parameters of the atmosphere), the look-up tables of the atmosphere with the kernels making them and the result read +// back, the environment map prefiltered with the GGX lobe, the stars +struct sky_state +{ + buffer sky; // gl::sky_data + buffer atmosphere; // gl::atmosphere_ubs + buffer result; // gl::atmosphere_result, written by atmosphere_ambient.comp + readback_ring result_readback; + std::shared_ptr transmittance; + std::shared_ptr multiscattering; + std::shared_ptr skyview; + bool tables_ready{false}; // the tables were made once (they are black before) + compute_kernel transmittance_kernel; + compute_kernel multiscattering_kernel; + compute_kernel skyview_kernel; + compute_kernel ambient_kernel; + // the prefiltered environment map, a cube view of each of its levels for the prefilter to write + std::shared_ptr ggx; + std::vector ggx_levels; + bool ggx_ready{false}; + compute_kernel prefilter_kernel; + // what the buffers were last given + gl::sky_data uploaded{}; + bool sky_uploaded{false}; + // the dome drawn with the atmosphere, the stars of the catalogue + std::shared_ptr atmosphere_dome; + std::shared_ptr stars; + std::shared_ptr star_vertices; + std::uint32_t star_count{0}; +}; + +// the automatic exposure (the stage P1, opengl46_exposure): the histogram and the exposure on the gpu (gl::exposure_data), the +// kernels, the values read back for the debug window +struct exposure_state +{ + buffer data; + compute_kernel histogram; + compute_kernel average; + readback_ring readback; + float values[4]{1.f, 1.f, 1.f, 0.f}; + bool cleared{false}; +}; + +// the weather on the gpu (the stage P2, opengl46_weather): the noises of the clouds, the map of their shadows, the clouds of the +// main view and their history, the panorama of the clouds for the other views, the fog volume of the main view and its +// history, with the kernels making them, and the program laying the clouds and the fog over the sky +struct weather_resources +{ + std::shared_ptr shape_noise; + std::shared_ptr detail_noise; + std::shared_ptr coverage_noise; + bool noise_made{false}; + std::shared_ptr shadow_map; + int shadow_size{0}; + // the clouds of the main view, at a part of its resolution + glm::ivec2 view_size{0}; + std::shared_ptr clouds_color; + std::shared_ptr clouds_depth; + std::array, 2> clouds_history; + int clouds_current{0}; // the history holding the latest result + std::shared_ptr panorama; + glm::ivec2 panorama_size{0}; + // what the panorama is marched with as its depth, which it doesn't write + std::shared_ptr depth_dummy; + // the fog volume: the scattering of the cells and its history, the fog integrated along the view + glm::ivec3 fog_grid{0}; + std::array, 2> fog_scattering; + int fog_current{0}; + std::shared_ptr fog_integrated; + compute_kernel noise; + compute_kernel march; + compute_kernel temporal; + compute_kernel shadow; + compute_kernel horizon; + compute_kernel inject; + compute_kernel integrate; + std::shared_ptr sky; +}; + +// the gpu culling of the instances (the stage P5, Global.gfx_gpucull): the cullings of the passes, which share the records and +// the kernels with the one of the main view, and the depth pyramid of its occlusion test +struct cull_resources +{ + instance_cull main; + std::array cascades; + instance_cull reflections; + // the maps of headlights, by the slot of the map, and the scenery parts of the maps of lamps, by their order among those drawn + // in a frame: the passes of a frame don't share the buffers of a culling. made when first needed + std::vector> headlights; + std::vector> lamps; + hiz_pyramid hiz; + bool ready{false}; + // the cascades of the sun shadows fitted to the depth of the main view (gfx.shadow.sdsm), made when they're first needed + shadow_fitter shadowfit; + bool shadowfit_tried{false}; +}; + // what the scene is drawn with struct scene_state { @@ -163,6 +306,8 @@ struct scene_state }; std::array pass_sets{}; bool pass_sets_stale{true}; + // the pass sets were made anew this many times (the ones of the cullings follow them) + std::uint64_t pass_generation{0}; // the rings of the draws: the blocks of the models (gl::model_ubs) and the modelview matrices of the instances window_ring draws; window_ring instances; @@ -178,10 +323,48 @@ struct scene_state std::array env_faces{}; std::shared_ptr pick_colour; std::shared_ptr pick_depth; + // the stand-ins of the images of the stages P the pass sets hold until there are real ones: a volume (the fog volume, the noises + // of the clouds), the cube shadow maps of the lamps, the shadow maps of the headlights, the depth of the scene + std::shared_ptr volume_empty; + std::shared_ptr cube_shadow_empty; + std::shared_ptr headlight_shadow_empty; + std::shared_ptr depth_empty; + // the colours of the fog towards the horizon (the stage P2), a block of the pass sets + buffer weather_horizon; VkImageView headlights_view{VK_NULL_HANDLE}; // the one the pass sets were made with + VkSampler headlights_sampler{VK_NULL_HANDLE}; + // the views of the images of the bindings of each pass set it was made with (vulkan14scene.cpp, pass_set_images()) + std::array, 3> pass_images{}; cluster_state clusters; + // the ambient occlusion: the noise of the older ssao, the kernels of the ground truth one, the program applying it + std::shared_ptr ssao_noise; + compute_kernel gtao_main; + compute_kernel gtao_denoise; + std::shared_ptr ao_apply; + sky_state sky; + exposure_state exposure; + weather_resources weather; + cull_resources cull; + // the shadow maps of the lamps and the headlights (the stage P4), and the programs drawing the cube maps + local_shadow_maps lamps; + std::shared_ptr cube_shadow, cube_alpha_shadow; + target_formats cube_formats; + // the materials of the heightmap terrain (the stage P6), made with the bank of the textures + std::unique_ptr terrain; + // the grass of the heightmap terrain, and its program, made when it's first drawn + grass_field grass; + std::shared_ptr grass_program; + bool grass_tried{false}; + // the smoke and the rain or snow of the gpu + gpu_particles particles; + // the heatmap of the light clusters (gfx.lights.clusterheatmap), made when it's first drawn + std::shared_ptr cluster_heatmap; + bool cluster_heatmap_tried{false}; // the targets of the viewports, [0] the main window std::vector targets; + // the format of the levels of the bloom chains: no alpha, no negative values, half the memory of rgba16f, where the device + // draws to it + VkFormat bloom_format{VK_FORMAT_B10G11R11_UFLOAT_PACK32}; // the formats of the passes which don't depend on the viewport target_formats shadow_formats; target_formats reflection_formats; @@ -190,6 +373,9 @@ struct scene_state // the programs: of the materials (by the name Fetch_Shader() is given), and the ones of the renderer std::unordered_map> materials; std::shared_ptr shadow, alpha_shadow, pick, pick_surface, line, billboard, freespot, celestial, skydome, smoke, precipitation; + // the depth prepass of the scene (the stage P3): without and with the alpha test, and which of them a program of a material takes + std::shared_ptr prepass, prepass_alpha; + std::unordered_map prepass_classes; // the post-processing, by the fragment shader std::unordered_map> post_fragments; std::shared_ptr quad; @@ -225,7 +411,20 @@ struct vulkan14_renderer::context VkCommandPool pool{VK_NULL_HANDLE}; // the uploads of the frame go to the queue first, then the commands of the frame VkCommandBuffer uploads{VK_NULL_HANDLE}; - VkCommandBuffer commands{VK_NULL_HANDLE}; + // the calls made while the frame is made, recorded into the command buffers once it's done (M1): the uploads into theirs, + // the commands chunk by chunk by the workers + vk14::command_stream upload_stream; + vk14::command_stream commands; + // a command pool for each worker, with the command buffers it made + struct worker_commands + { + VkCommandPool pool{VK_NULL_HANDLE}; + std::vector buffers; + std::size_t used{0}; + }; + std::vector workers; + // the command buffers of the chunks of the frame, in their order + std::vector chunks; // the value of the timeline semaphore the gpu reaches when it's done with the frame; 0: never submitted std::uint64_t done{0}; // the number of the frame, for the report of a lost device @@ -262,7 +461,35 @@ struct vulkan14_renderer::context bool devicelost{false}; // the frame being recorded: its index among the frames in flight, its commands std::uint32_t slot{0}; - VkCommandBuffer commands{VK_NULL_HANDLE}; + vk14::command_stream *commands{nullptr}; + // the threads recording the command buffers of the frames: a pool of its own, the one of the application is for the simulation + worker_pool workers; + // the cpu time the command buffers of the last frame took to record, in milliseconds; its calls and chunks + std::atomic record_time{0.0}; + std::atomic record_calls{0}; + std::atomic record_chunks{0}; + std::atomic record_threads{0}; + // what's left of a frame made by the main thread: recording its command buffers, submitting them, presenting its windows + struct render_job + { + frame *target{nullptr}; + bool uploaded{false}; + std::vector waits; + std::vector signals; + // the windows presented once the frame is submitted (the render thread) + std::vector windows; + }; + // the render thread and the job it's given + std::thread render_thread; + std::mutex render_mutex; + std::condition_variable render_wake; + std::condition_variable render_idle; + render_job render_pending; + bool render_queued{false}; // render_pending holds a job not done yet + bool render_stop{false}; + char const *render_failure{nullptr}; + // how long the main thread waited for the render thread at the start of the last frame, in milliseconds + double render_wait{0.0}; ~context() { @@ -270,6 +497,26 @@ struct vulkan14_renderer::context } bool create_frames(std::uint32_t const Count, std::string &Reason); + // the streams of Frame recorded into its command buffers; false: no command buffer for them + bool record(frame &Frame, bool Uploads); + + // the job done; false: Failure says where it failed + bool execute(render_job &Job, char const *&Failure); + // the windows of the frame submitted last presented; false: the device or a surface is lost + bool present(std::vector const &Windows); + // the render thread (gfx.vulkan.renderthread, M4): the frames are recorded, submitted and presented by it while the main + // thread goes on with the simulation of the next one. the main thread waits for it to be done before it begins a frame and + // before anything else touches the queue, the swapchains or the device as a whole + void start_render_thread(); + void stop_render_thread(); + bool render_threaded() const + { + return render_thread.joinable(); + } + // the job given to the render thread + void hand_over(render_job &&Job); + // waits until the render thread is done with the jobs given to it; where it failed, if it did + char const *drain(); // the parts of the renderer made once the device is there bool create_core(std::uint32_t const Frames, std::string &Reason); // the value of the timeline the gpu reached, 0 when it can't be read @@ -282,7 +529,7 @@ struct vulkan14_renderer::context // the state of the draws of the walk being recorded into a pass of the scene struct vulkan14_renderer::draw_state { - VkCommandBuffer commands{VK_NULL_HANDLE}; + vk14::command_stream *commands{nullptr}; vk14::target_formats const *targets{nullptr}; VkExtent2D extent{0, 0}; vk14::scene_state::pass_set_kind set{vk14::scene_state::set_plain}; @@ -292,6 +539,7 @@ struct vulkan14_renderer::draw_state std::uint32_t scene_offset{0}; std::uint32_t light_offset{0}; std::uint32_t cluster_offset{0}; + std::uint32_t weather_offset{0}; bool set_dirty{true}; // the program and the textures for the next draw (walk_material(), walk_program(), walk_texture()) vk14::scene_program *program{nullptr}; @@ -319,4 +567,20 @@ struct vulkan14_renderer::draw_state std::uint32_t query{~0u}; // the query under way // the draws made in the pass, for the debug panel std::uint32_t draws{0}; + // the depth of the scene before its colour (the stage P3): the walk draws with the programs of the depth prepass, and what they + // don't draw is left out (depthskip, set by walk_material()) + bool depthpass{false}; + bool depthskip{true}; + // the pass sets of a culling, the instance buffer its output, for the indirect draws of its instances; null: the pass sets + VkDescriptorSet const *cull_sets{nullptr}; + // the rendering under way, begun again where it was after it was ended for the compute passes of the culling + struct rendering_resume + { + std::array colours{}; + VkRenderingAttachmentInfo depth{}; + bool has_depth{false}; + VkRenderingInfo info{}; + std::vector uses; + bool valid{false}; + } resume; }; diff --git a/rendering/vulkan14/vulkan14cull.cpp b/rendering/vulkan14/vulkan14cull.cpp new file mode 100644 index 00000000..e0727ea1 --- /dev/null +++ b/rendering/vulkan14/vulkan14cull.cpp @@ -0,0 +1,604 @@ +/* +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/vulkan14/vulkan14cull.h" +#include "rendering/vulkan14/vulkan14commands.h" + +#include "rendering/frustum.h" +#include "utilities/Globals.h" + +namespace vk14 +{ + +namespace +{ + +constexpr VkDeviceSize RECORD_SIZE{sizeof(scene::basic_cell::instance_record)}; +// the counters read back: the header and the entries compared +constexpr VkDeviceSize READBACK_SIZE{(gl::INSTANCE_COUNTER_HEADER + 16384) * sizeof(std::uint32_t)}; + +// the parameters of instance_cull.comp, its kernel_block (std140) +struct cull_block +{ + glm::mat4 view; + glm::vec4 planes[6]; + glm::vec4 distance_params; + glm::vec4 size_params; + glm::mat4 hiz_projection; + glm::ivec4 hiz_params; + glm::vec4 hiz_screen; + glm::uvec4 phases; +}; +static_assert(sizeof(cull_block) == 304, "the block of the culling doesn't match the shader"); + +// the parameters of hiz_build.comp +struct hiz_constants +{ + glm::ivec4 sizes; + glm::ivec4 params; +}; + +// what read and wrote the buffers of the culling before (the draws of the passes before, the copies for the cpu, the kernels) is +// done before they're written again +void before_writes(command_stream *Commands) +{ + memory_barrier(Commands, + VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_CLEAR_BIT, + VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_CLEAR_BIT, + VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT); +} + +} // namespace + +instance_cull::~instance_cull() = default; + +bool instance_cull::init(cull_environment const &Environment, VkDevice Device, pipeline_library &Pipelines, shader_library &Shaders, VkDescriptorSetLayout Passlayout, instance_cull const *Shared) +{ + if (Shared != nullptr) + { + m_shared = Shared->m_shared; + share_records(*Shared); + } + else + { + auto shared{std::make_shared()}; + shared->environment = Environment; + try + { + if ((false == shared->cull.create(Device, Pipelines, Shaders.get("instance_cull.comp"), Passlayout, {}, "instance cull")) || + (false == shared->offsets.create(Device, Pipelines, Shaders.get("instance_cull_offsets.comp"), Passlayout, {}, "instance cull offsets")) || + (false == shared->commands.create(Device, Pipelines, Shaders.get("instance_cull_commands.comp"), Passlayout, {}, "instance cull commands"))) + { + return false; + } + } + catch (gfx::shader_exception const &Exception) + { + log_error(Exception.what()); + return false; + } + shared->records = std::make_shared(); + if (false == shared->records->create(*Environment.memory, 65536 * RECORD_SIZE, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, memory_kind::device, "instance records")) + { + return false; + } + m_shared = std::move(shared); + } + auto const &environment{m_shared->environment}; + return reserve(m_counters, READBACK_SIZE, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, "instance counters") && + reserve(m_output, 65536 * sizeof(glm::mat4), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, "instance output") && + reserve(m_commands, 3 * 4096 * sizeof(gl::instance_draw_command), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, "instance commands") && + m_readback.create(*environment.memory, READBACK_SIZE, environment.frames, "instance counters readback"); +} + +void instance_cull::destroy(VkDevice Device) +{ + if (m_shared && (m_shared.use_count() == 1)) + { + m_shared->cull.destroy(Device); + m_shared->offsets.destroy(Device); + m_shared->commands.destroy(Device); + } + m_shared.reset(); + m_counters.reset(); + m_output.reset(); + m_commands.reset(); + m_readback.destroy(); + if (set_pool != VK_NULL_HANDLE) + { + vkDestroyDescriptorPool(Device, set_pool, nullptr); + set_pool = VK_NULL_HANDLE; + } + sets = {}; +} + +void instance_cull::begin_frame(std::uint32_t const Slot, std::uint64_t const Value, std::uint64_t const Completed) +{ + if (!m_shared) + { + return; + } + auto &environment{m_shared->environment}; + environment.slot = Slot; + environment.value = Value; + environment.completed = Completed; +} + +bool instance_cull::reserve(std::shared_ptr &Buffer, VkDeviceSize const Size, VkBufferUsageFlags const Usage, char const *Name) +{ + if (Buffer && (Size <= Buffer->size())) + { + return true; + } + auto const &environment{m_shared->environment}; + auto replacement{std::make_shared()}; + if (false == replacement->create(*environment.memory, std::max(Size, Buffer ? 2 * Buffer->size() : 0), Usage, memory_kind::device, Name)) + { + log_error(std::string("no memory for the buffer of the culling of the instances: ") + Name); + return false; + } + if (Buffer) + { + // the frames in flight may still read it + environment.deletions->push(environment.uploads->current_value(), [old = std::move(Buffer)]() {}); + } + Buffer = std::move(replacement); + return true; +} + +std::int64_t instance_cull::record_buffer_size() const +{ + return (m_shared && m_shared->records) ? static_cast(m_shared->records->size()) : 0; +} + +void instance_cull::resize_record_buffer(std::int64_t const Size) +{ + auto &shared{*m_shared}; + auto const &environment{shared.environment}; + auto replacement{std::make_shared()}; + if (false == replacement->create(*environment.memory, static_cast(Size), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, memory_kind::device, "instance records")) + { + log_error("no memory for the records of the instances"); + return; + } + // the passes culled before in the frame keep the old buffer, with its records + environment.deletions->push(environment.uploads->current_value(), [old = std::move(shared.records)]() {}); + shared.records = std::move(replacement); + shared.pending.clear(); + shared.parts.clear(); +} + +void instance_cull::restart_records() +{ + // the records of the passes culled before in the frame go to the gpu before the frame (the uploads), where the records sent from + // the start of the buffer again would take their places + resize_record_buffer(record_buffer_size()); +} + +void instance_cull::send_records(std::uint32_t const First, scene::basic_cell::instance_record const *Records, std::uint32_t const Count) +{ + if ((Count == 0) || !m_shared) + { + return; + } + auto &shared{*m_shared}; + auto const *data{reinterpret_cast(Records)}; + shared.pending.insert(shared.pending.end(), data, data + Count * RECORD_SIZE); + shared.parts.emplace_back(First * RECORD_SIZE, Count * RECORD_SIZE); +} + +void instance_cull::flush_records() +{ + auto &shared{*m_shared}; + if (shared.pending.empty()) + { + return; + } + if (false == shared.environment.uploads->upload_buffer(shared.records, shared.pending.data(), shared.pending.size(), shared.parts)) + { + log_error("no staging memory for the records of the instances"); + } + shared.pending.clear(); + shared.parts.clear(); +} + +void instance_cull::push_counts() +{ + // copied with the last stage of the culling (read_counters()) +} + +bool instance_cull::pop_counts(std::uint32_t *Counts) +{ + if (!m_shared) + { + return false; + } + return m_readback.pop(m_shared->environment.completed, Counts, READBACK_SIZE); +} + +void instance_cull::read_counters(command_stream *Commands) +{ + auto const &environment{m_shared->environment}; + m_readback.push(Commands, environment.slot, environment.value, m_counters->handle(), 0, READBACK_SIZE); +} + +std::vector instance_cull::cull_descriptors(cull_occlusion const *Occlusion) const +{ + auto const &environment{m_shared->environment}; + auto const slice = [](ring_slice const &Slice) { return buffer_descriptor(Slice.buffer, Slice.offset, Slice.size); }; + return {buffer_descriptor(m_shared->records->handle(), 0, VK_WHOLE_SIZE), + slice(m_jobs), + slice(m_groups), + slice(m_lods), + buffer_descriptor(m_counters->handle(), 0, VK_WHOLE_SIZE), + buffer_descriptor(m_output->handle(), 0, VK_WHOLE_SIZE), + ((Occlusion != nullptr) && (Occlusion->pyramid != VK_NULL_HANDLE)) ? image_descriptor(Occlusion->sampler, Occlusion->pyramid, VK_IMAGE_LAYOUT_GENERAL) : environment.no_pyramid, + slice(m_block)}; +} + +void instance_cull::dispatch(command_stream *Commands, glm::mat4 const &View, cull_occlusion const *Occlusion, bool const Twophase) +{ + if (!m_shared || (false == dispatch_begin())) + { + return; + } + auto &shared{*m_shared}; + auto const &environment{shared.environment}; + flush_records(); + + m_countersize = (gl::INSTANCE_COUNTER_HEADER + 3 * m_entries) * sizeof(std::uint32_t); + if ((false == reserve(m_counters, std::max(m_countersize, READBACK_SIZE), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, "instance counters")) || + (false == reserve(m_output, static_cast(std::max(m_candidates, 1)) * sizeof(glm::mat4), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, "instance output"))) + { + return; + } + // the data of the pass in the ring of the frame + auto const place = [&](ring_slice &Slice, void const *Data, std::size_t const Size, VkDeviceSize const Alignment) { + Slice = environment.ring->allocate(std::max(Size, 16), Alignment); + if (Slice && (Size > 0)) + { + std::memcpy(Slice.data, Data, Size); + } + return bool(Slice); + }; + cull_block block{}; + block.view = View; + frustum_planes(block.planes); + block.distance_params = glm::vec4{m_setup.distancescale, m_setup.distanceoffset, m_setup.distancerange, m_setup.drawrange}; + block.size_params = glm::vec4{m_setup.zoom, 0.f, 0.f, 0.f}; + // the occlusion test only when the instances are drawn from the result, the cpu doesn't make it + m_occlusion = (m_setup.drawing && (Occlusion != nullptr) && (Occlusion->pyramid != VK_NULL_HANDLE)); + if (m_occlusion) + { + block.hiz_projection = Occlusion->projection; + block.hiz_params = glm::ivec4{1, Occlusion->levels, Occlusion->size.x, Occlusion->size.y}; + block.hiz_screen = glm::vec4{Occlusion->screen, 0.f, 0.f}; + } + m_twophase = (m_occlusion && Twophase); + block.phases = glm::uvec4{m_twophase ? 1u : 0u, 0u, 0u, 0u}; + if ((false == place(m_jobs, m_joblist.data(), m_joblist.size() * sizeof(gl::instance_cull_job), environment.storage_alignment)) || + (false == place(m_groups, m_grouplist.data(), m_grouplist.size() * sizeof(gl::instance_cull_group), environment.storage_alignment)) || + (false == place(m_lods, m_lodlist.data(), m_lodlist.size() * sizeof(float), environment.storage_alignment)) || (false == place(m_block, &block, sizeof(block), environment.uniform_alignment))) + { + log_error("the ring of the frame is full, the instances aren't culled"); + m_occlusion = false; + m_twophase = false; + return; + } + + // counts and cursors start at zero + before_writes(Commands); + std::uint32_t const header[gl::INSTANCE_COUNTER_HEADER]{m_setup.frame, 0, m_entries, 0, 0, 0, 0, 0}; + Commands->update_buffer(m_counters->handle(), 0, sizeof(header), header); + Commands->fill_buffer(m_counters->handle(), sizeof(header), m_countersize - sizeof(header), 0); + memory_barrier(Commands, VK_PIPELINE_STAGE_2_CLEAR_BIT | VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT); + + // one invocation per record; past the count limit of one dimension the groups go into rows + auto const groups{(m_candidates + gl::INSTANCE_CULL_GROUP_SIZE - 1) / gl::INSTANCE_CULL_GROUP_SIZE}; + auto const groups_x{std::min(groups, 65535)}; + auto const groups_y{(groups + groups_x - 1) / groups_x}; + auto const descriptors{cull_descriptors(Occlusion)}; + auto const jobs{static_cast(m_joblist.size())}; + shared.cull.dispatch(Commands, descriptors, glm::uvec4{jobs, m_candidates, m_entries, 0u}, groups_x, groups_y); + compute_barrier(Commands); + shared.offsets.dispatch(Commands, {buffer_descriptor(m_counters->handle(), 0, VK_WHOLE_SIZE)}, glm::uvec4{m_entries, 0u, 0u, 0u}, 1); + compute_barrier(Commands); + shared.cull.dispatch(Commands, descriptors, glm::uvec4{jobs, m_candidates, m_entries, 1u}, groups_x, groups_y); + // the output and the counts go to draws, indirect draws and the copy for the cpu + compute_barrier(Commands); + if (false == m_twophase) + { + read_counters(Commands); + } + dispatch_end(); +} + +void instance_cull::dispatch_second(command_stream *Commands, cull_occlusion const &Occlusion) +{ + if (!m_dispatched || !m_twophase) + { + return; + } + auto &shared{*m_shared}; + // the draws of the first phase read the output and the commands the second phase writes (other slots of them) + memory_barrier(Commands, VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT); + auto const groups{(m_candidates + gl::INSTANCE_CULL_GROUP_SIZE - 1) / gl::INSTANCE_CULL_GROUP_SIZE}; + auto const groups_x{std::min(groups, 65535)}; + auto const groups_y{(groups + groups_x - 1) / groups_x}; + shared.cull.dispatch(Commands, cull_descriptors(&Occlusion), glm::uvec4{static_cast(m_joblist.size()), m_candidates, m_entries, 2u}, groups_x, groups_y); + compute_barrier(Commands); + if (m_commandcount > 0) + { + shared.commands.dispatch(Commands, {buffer_descriptor(m_counters->handle(), 0, VK_WHOLE_SIZE), buffer_descriptor(m_commands->handle(), 0, VK_WHOLE_SIZE)}, glm::uvec4{m_commandcount, m_entries, 1u, 0u}, + (m_commandcount + gl::INSTANCE_COMMAND_GROUP_SIZE - 1) / gl::INSTANCE_COMMAND_GROUP_SIZE); + } + // the commands go to indirect draws, the matrices to the vertex shaders, the counts to the copy for the cpu + compute_barrier(Commands); + read_counters(Commands); +} + +void instance_cull::fill_commands(command_stream *Commands, std::vector const &Commands_) +{ + m_commandcount = static_cast(Commands_.size()); + if (!m_dispatched || Commands_.empty()) + { + return; + } + auto &shared{*m_shared}; + auto const &environment{shared.environment}; + // the first copy is sent, the others are made from it on the gpu + auto const size{static_cast(Commands_.size() * sizeof(gl::instance_draw_command))}; + if (false == reserve(m_commands, 3 * size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, "instance commands")) + { + m_commandcount = 0; + return; + } + before_writes(Commands); + auto const slice{environment.ring->allocate(size, 16)}; + if (slice) + { + std::memcpy(slice.data, Commands_.data(), static_cast(size)); + VkBufferCopy2 region{}; + region.sType = VK_STRUCTURE_TYPE_BUFFER_COPY_2; + region.srcOffset = slice.offset; + region.dstOffset = 0; + region.size = size; + VkCopyBufferInfo2 copy{}; + copy.sType = VK_STRUCTURE_TYPE_COPY_BUFFER_INFO_2; + copy.srcBuffer = slice.buffer; + copy.dstBuffer = m_commands->handle(); + copy.regionCount = 1; + copy.pRegions = ®ion; + Commands->copy_buffer2(©); + } + else + { + // the ring of the frame is full: the commands written in pieces with the commands of the frame + auto const *data{reinterpret_cast(Commands_.data())}; + for (VkDeviceSize offset = 0; offset < size; offset += 65536) + { + Commands->update_buffer(m_commands->handle(), offset, std::min(65536, size - offset), data + offset); + } + } + memory_barrier(Commands, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT); + shared.commands.dispatch(Commands, {buffer_descriptor(m_counters->handle(), 0, VK_WHOLE_SIZE), buffer_descriptor(m_commands->handle(), 0, VK_WHOLE_SIZE)}, glm::uvec4{m_commandcount, m_entries, 0u, 0u}, + (m_commandcount + gl::INSTANCE_COMMAND_GROUP_SIZE - 1) / gl::INSTANCE_COMMAND_GROUP_SIZE); + // the commands go to indirect draws, the matrices to the vertex shaders + compute_barrier(Commands); +} + +// the depth pyramid + +bool hiz_pyramid::init(VkDevice Device, pipeline_library &Pipelines, shader_library &Shaders, VkDescriptorSetLayout Passlayout) +{ + try + { + return m_kernel.create(Device, Pipelines, Shaders.get("hiz_build.comp"), Passlayout, {}, "hi-z"); + } + catch (gfx::shader_exception const &Exception) + { + log_error(Exception.what()); + return false; + } +} + +void hiz_pyramid::destroy(VkDevice Device, deletion_queue *Deletions, std::uint64_t const Value) +{ + if (Deletions != nullptr) + { + // the frames in flight may still read them + Deletions->push(Value, [Device, levels = std::move(m_levels), pyramid = std::move(m_pyramid)]() { + for (auto *view : levels) + { + vkDestroyImageView(Device, view, nullptr); + } + }); + } + else + { + for (auto *view : m_levels) + { + vkDestroyImageView(Device, view, nullptr); + } + m_kernel.destroy(Device); + m_pyramid.reset(); + } + m_levels.clear(); + m_pyramid.reset(); + m_sourcesize = glm::ivec2{0}; + m_size = glm::ivec2{0}; +} + +bool hiz_pyramid::build(command_stream *Commands, VkDevice Device, allocator &Memory, deletion_queue &Deletions, std::uint64_t const Value, VkImageView const Depth, VkSampler const Sampler, int const Width, int const Height) +{ + if ((Width <= 0) || (Height <= 0) || (false == bool(m_kernel))) + { + return false; + } + if ((m_sourcesize != glm::ivec2{Width, Height}) || (false == bool(m_pyramid))) + { + if (m_pyramid) + { + destroy(Device, &Deletions, Value); + } + m_sourcesize = glm::ivec2{Width, Height}; + m_size = glm::ivec2{(Width + 1) / 2, (Height + 1) / 2}; + std::uint32_t levels{1}; + while ((std::max(m_size.x, m_size.y) >> levels) > 0) + { + ++levels; + } + image_desc desc; + desc.format = VK_FORMAT_R32_SFLOAT; + desc.extent = {static_cast(m_size.x), static_cast(m_size.y), 1}; + desc.mips = levels; + desc.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + m_pyramid = std::make_shared(); + if (false == m_pyramid->create(Memory, desc, "hi-z pyramid")) + { + m_pyramid.reset(); + m_sourcesize = glm::ivec2{0}; + return false; + } + for (std::uint32_t level = 0; level < levels; ++level) + { + VkImageViewCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + info.image = m_pyramid->handle(); + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.format = desc.format; + info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, level, 1, 0, 1}; + VkImageView view{VK_NULL_HANDLE}; + VK14_VERIFY(vkCreateImageView(Device, &info, nullptr, &view)); + m_levels.push_back(view); + } + // the pyramid stays in the general layout: the kernel reads the levels it wrote, the culling all of them + transition(Commands, *m_pyramid, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); + } + else + { + // the culling done with the pyramid made before + memory_barrier(Commands, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT); + } + auto source{Depth}; + auto sourcelayout{VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + auto sourcesize{m_sourcesize}; + for (std::size_t level = 0; level < m_levels.size(); ++level) + { + auto const targetsize{glm::max(m_size >> static_cast(level), glm::ivec2{1})}; + hiz_constants constants{glm::ivec4{sourcesize, targetsize}, glm::ivec4{0}}; + m_kernel.dispatch(Commands, {image_descriptor(Sampler, source, sourcelayout), storage_image(m_levels[level])}, constants, (targetsize.x + gl::HIZ_GROUP_SIZE - 1) / gl::HIZ_GROUP_SIZE, + (targetsize.y + gl::HIZ_GROUP_SIZE - 1) / gl::HIZ_GROUP_SIZE); + memory_barrier(Commands, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT); + source = m_levels[level]; + sourcelayout = VK_IMAGE_LAYOUT_GENERAL; + sourcesize = targetsize; + } + return true; +} + +cull_occlusion hiz_pyramid::occlusion(VkSampler const Sampler, glm::mat4 const &Projection) const +{ + cull_occlusion result; + if (false == bool(m_pyramid)) + { + return result; + } + result.pyramid = m_pyramid->view(); + result.sampler = Sampler; + result.size = m_size; + result.levels = static_cast(m_levels.size()); + result.screen = glm::vec2{m_sourcesize}; + result.projection = Projection; + return result; +} + +// the cascades fitted to the depth of the view + +namespace +{ + +struct fit_depth_constants +{ + glm::mat4 inv_projection; +}; + +struct fit_bounds_constants +{ + glm::mat4 inv_projection; + glm::mat4 view_to_light; + glm::vec4 split_params; + glm::vec4 margin_params; +}; + +} // namespace + +bool shadow_fitter::init(VkDevice Device, pipeline_library &Pipelines, shader_library &Shaders, VkDescriptorSetLayout Passlayout, allocator const &Memory, std::uint32_t const Frames) +{ + try + { + if ((false == m_depthkernel.create(Device, Pipelines, Shaders.get("shadow_fit_depth.comp"), Passlayout, {}, "shadow fit depth")) || + (false == m_boundskernel.create(Device, Pipelines, Shaders.get("shadow_fit_bounds.comp"), Passlayout, {}, "shadow fit bounds"))) + { + return false; + } + } + catch (gfx::shader_exception const &Exception) + { + log_error(Exception.what()); + return false; + } + return m_buffer.create(Memory, sizeof(gl::shadow_fit_data), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, memory_kind::device, "shadow fit") && + m_readback.create(Memory, sizeof(gl::shadow_fit_data), Frames, "shadow fit readback"); +} + +void shadow_fitter::destroy(VkDevice Device) +{ + m_depthkernel.destroy(Device); + m_boundskernel.destroy(Device); + m_buffer.destroy(); + m_readback.destroy(); +} + +void shadow_fitter::measure(command_stream *Commands, VkImageView const Depth, VkSampler const Sampler, int const Width, int const Height, view const &View, unsigned int const Frame, std::uint32_t const Slot, + std::uint64_t const Value) +{ + if ((Width <= 0) || (Height <= 0) || (View.range <= 0.f) || (false == bool(*this))) + { + return; + } + auto const data{begin_measurement(View, Frame)}; + update_buffer(Commands, m_buffer, &data, sizeof(data), 0); + auto const inverseprojection{glm::inverse(View.projection)}; + auto const viewtolight{glm::mat4{View.light * glm::inverse(View.rotation)}}; + auto const groupsx{(static_cast(Width) + gl::SHADOW_FIT_GROUP_SIZE - 1) / gl::SHADOW_FIT_GROUP_SIZE}; + auto const groupsy{(static_cast(Height) + gl::SHADOW_FIT_GROUP_SIZE - 1) / gl::SHADOW_FIT_GROUP_SIZE}; + std::vector const descriptors{image_descriptor(Sampler, Depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL), buffer_descriptor(m_buffer.handle(), 0, VK_WHOLE_SIZE)}; + m_depthkernel.dispatch(Commands, descriptors, fit_depth_constants{inverseprojection}, groupsx, groupsy); + compute_barrier(Commands); + m_boundskernel.dispatch(Commands, descriptors, fit_bounds_constants{inverseprojection, viewtolight, bounds_params(View), margin_params()}, groupsx, groupsy); + compute_barrier(Commands); + m_readback.push(Commands, Slot, Value, m_buffer.handle(), 0, sizeof(gl::shadow_fit_data)); +} + +shadow_fitter::result const *shadow_fitter::latest(std::uint64_t const Completed) +{ + gl::shadow_fit_data data; + if (m_readback.pop(Completed, &data, sizeof(data))) + { + arrived(data); + } + return gfx::shadow_fit::latest(); +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14cull.h b/rendering/vulkan14/vulkan14cull.h new file mode 100644 index 00000000..a1d7024f --- /dev/null +++ b/rendering/vulkan14/vulkan14cull.h @@ -0,0 +1,196 @@ +/* +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/. +*/ + +// the gpu culling of the instances in the vulkan renderer (the stage P5 of the plan): the buffers and the compute passes of +// gfx::instance_cull, the opengl46_instance_cull of this renderer, with the same shaders (instance_cull.comp, +// instance_cull_offsets.comp, instance_cull_commands.comp), and the depth pyramid of its occlusion test (hiz_build.comp, the +// opengl46_hiz of this renderer). what differs from the opengl renderer: +// - the records go to the gpu with the uploads of the frame, which come before its commands; a record buffer started over gets +// a new buffer, so the passes of the frame culled before keep the places they had +// - the jobs, the groups, the lod bounds and the parameters of a pass are read by the kernels from the ring of the frame +// - the counters are copied for the cpu at the end of the last stage of the culling, in the frame being recorded (the rendering +// of the pass is suspended for the dispatches) +// - the output is the instance buffer of the indirect draws in pass sets of its own (vulkan14_renderer::cull_pass_sets()) + +#pragma once + +#include "rendering/common/instancecull.h" +#include "rendering/common/shadowfit.h" +#include "rendering/vulkan14/vulkan14compute.h" +#include "rendering/vulkan14/vulkan14memory.h" +#include "rendering/vulkan14/vulkan14texture.h" + +#include +#include +#include + +namespace vk14 +{ + +// what the cullings need of the renderer +struct cull_environment +{ + allocator *memory{nullptr}; + uploader *uploads{nullptr}; + deletion_queue *deletions{nullptr}; + frame_ring *ring{nullptr}; // the ring of the frame, the data of the passes go through it + VkDeviceSize uniform_alignment{256}; + VkDeviceSize storage_alignment{256}; + std::uint32_t frames{2}; // in flight + // the frame being recorded: its slot among the frames in flight, the value of the timeline the gpu reaches when it's done with it, + // and the value the gpu reached + std::uint32_t slot{0}; + std::uint64_t value{0}; + std::uint64_t completed{0}; + // a sampled image to give the occlusion test of the kernel when there's no pyramid + descriptor no_pyramid{}; +}; + +// the occlusion test of a pass: the pyramid (the view of all its levels, in the general layout) and what it was made from +struct cull_occlusion +{ + VkImageView pyramid{VK_NULL_HANDLE}; + VkSampler sampler{VK_NULL_HANDLE}; + glm::ivec2 size{0}; // of its first level + int levels{0}; + glm::vec2 screen{0.f}; // size of the depth it was made from, pixels + glm::mat4 projection{1.f}; // of the pass +}; + +class instance_cull : public gfx::instance_cull +{ + public: + instance_cull() = default; + ~instance_cull() override; + + // the kernels and the record buffer made, or taken from Shared. Shaders: the shader library + bool init(cull_environment const &Environment, VkDevice Device, pipeline_library &Pipelines, shader_library &Shaders, VkDescriptorSetLayout Passlayout, instance_cull const *Shared = nullptr); + void destroy(VkDevice Device); + explicit operator bool() const + { + return bool(m_shared); + } + // the frame being recorded (cull_environment::slot, value, completed) + void begin_frame(std::uint32_t const Slot, std::uint64_t const Value, std::uint64_t const Completed); + // as opengl46_instance_cull::dispatch(), dispatch_second(), fill_commands(), outside a rendering + void dispatch(command_stream *Commands, glm::mat4 const &View, cull_occlusion const *Occlusion = nullptr, bool const Twophase = false); + void dispatch_second(command_stream *Commands, cull_occlusion const &Occlusion); + void fill_commands(command_stream *Commands, std::vector const &Commands_); + // the indirect commands (draw_command_offset()) and the matrices of the instances drawn + VkBuffer command_buffer() const + { + return m_commands ? m_commands->handle() : VK_NULL_HANDLE; + } + VkBuffer output_buffer() const + { + return m_output ? m_output->handle() : VK_NULL_HANDLE; + } + VkDeviceSize output_size() const + { + return m_output ? m_output->size() : 0; + } + // the pass sets of the renderer with the output as the instance buffer, made for the generation of the pass sets and the output + std::array sets{}; + VkDescriptorPool set_pool{VK_NULL_HANDLE}; + std::uint64_t sets_generation{0}; + VkBuffer sets_output{VK_NULL_HANDLE}; + // the gpu time of the passes, for the panel (the parts of the pass list) + std::string info_times(std::string const &Label = "instance cull") const + { + return {}; + } + + protected: + std::int64_t record_buffer_size() const override; + void resize_record_buffer(std::int64_t const Size) override; + void send_records(std::uint32_t const First, scene::basic_cell::instance_record const *Records, std::uint32_t const Count) override; + void restart_records() override; + void push_counts() override; + bool pop_counts(std::uint32_t *Counts) override; + + private: + // what the cullings of all passes share: the kernels, the record buffer and the records sent since the last dispatch + struct shared_data + { + compute_kernel cull; + compute_kernel offsets; + compute_kernel commands; + std::shared_ptr records; + std::vector pending; + std::vector> parts; + cull_environment environment; + }; + // a buffer of the culling grown to hold at least Size bytes, at least doubled; the old one goes once the gpu is done with it + bool reserve(std::shared_ptr &Buffer, VkDeviceSize Size, VkBufferUsageFlags Usage, char const *Name); + // the records sent since the last dispatch go with the uploads of the frame + void flush_records(); + // the descriptors of the culling kernel; Occlusion: the pyramid, or none + std::vector cull_descriptors(cull_occlusion const *Occlusion) const; + // the counters copied for the cpu + void read_counters(command_stream *Commands); + + std::shared_ptr m_shared; + std::shared_ptr m_counters; + std::shared_ptr m_output; + std::shared_ptr m_commands; + readback_ring m_readback; + // the slices of the ring the jobs, the groups, the lod bounds and the parameters of the pass were placed in + ring_slice m_jobs; + ring_slice m_groups; + ring_slice m_lods; + ring_slice m_block; + VkDeviceSize m_countersize{0}; +}; + +// the depth pyramid of the occlusion test of the main view (opengl46_hiz): made from the resolved depth of the pass, the first +// level half its size, each level the farthest depth of the 2x2 texels below it +class hiz_pyramid +{ + public: + bool init(VkDevice Device, pipeline_library &Pipelines, shader_library &Shaders, VkDescriptorSetLayout Passlayout); + void destroy(VkDevice Device, deletion_queue *Deletions = nullptr, std::uint64_t Value = 0); + // the pyramid made from Depth (a sampled image of Width x Height, in the read only layout), the images made anew for another size + bool build(command_stream *Commands, VkDevice Device, allocator &Memory, deletion_queue &Deletions, std::uint64_t Value, VkImageView Depth, VkSampler Sampler, int Width, int Height); + // the occlusion test of the pass + cull_occlusion occlusion(VkSampler Sampler, glm::mat4 const &Projection) const; + + private: + compute_kernel m_kernel; + std::shared_ptr m_pyramid; + std::vector m_levels; + glm::ivec2 m_sourcesize{0}; + glm::ivec2 m_size{0}; +}; + +// the cascades of the sun shadows fitted to the depth of the main view (gfx.shadow.sdsm, opengl46_shadow_fit): the passes and the +// buffers of gfx::shadow_fit, the result read back through a ring +class shadow_fitter : public gfx::shadow_fit +{ + public: + bool init(VkDevice Device, pipeline_library &Pipelines, shader_library &Shaders, VkDescriptorSetLayout Passlayout, allocator const &Memory, std::uint32_t Frames); + void destroy(VkDevice Device); + explicit operator bool() const + { + return bool(m_depthkernel) && bool(m_boundskernel) && bool(m_buffer); + } + // measures the depth of the view: Depth, the resolved depth of Width x Height pixels in the read only layout, outside a rendering. + // Frame: the frame of the renderer; Slot, Value: the frame being recorded, as the readback rings take it + void measure(command_stream *Commands, VkImageView Depth, VkSampler Sampler, int Width, int Height, view const &View, unsigned int Frame, std::uint32_t Slot, std::uint64_t Value); + // takes in the newest measurement the gpu has finished (Completed: the value of the timeline it reached), and returns the + // newest one taken in so far + result const *latest(std::uint64_t Completed); + + private: + compute_kernel m_depthkernel; + compute_kernel m_boundskernel; + buffer m_buffer; + readback_ring m_readback; +}; + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14debug.cpp b/rendering/vulkan14/vulkan14debug.cpp new file mode 100644 index 00000000..33dbec80 --- /dev/null +++ b/rendering/vulkan14/vulkan14debug.cpp @@ -0,0 +1,440 @@ +/* +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/. +*/ + +// the tools of the vulkan renderer (the stage P7 of the plan): the debug windows of opengl46_renderer::draw_debug_ui() over the +// settings and the state shared with the opengl renderer, the lines of the statistics of the scene and of the cullings for +// the debug panel (Shift+F12), and the heatmap of the light clusters (gfx.lights.clusterheatmap) + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14renderer.h" +#include "rendering/vulkan14/vulkan14commands.h" +#include "rendering/vulkan14/vulkan14context.h" + +#include "rendering/common/exposure.h" +#include "simulation/simulationenvironment.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/utilities.h" +#include "imgui/imgui.h" + +namespace +{ + +VkPipelineStageFlags2 constexpr heatmap_colour_stage{VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT}; +VkAccessFlags2 constexpr heatmap_colour_access{VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT}; + +} // namespace + +void vulkan14_renderer::collect_cluster_stats() +{ + if (!m_context) + { + return; + } + auto &clusters{m_context->scene.clusters}; + gl::cluster_counters counters{}; + if (false == clusters.counters_readback.pop(m_context->completed(), &counters, sizeof(counters))) + { + return; + } + auto &stats{clusters.stats}; + stats.index_count = counters.index_count; + stats.overflow_count = counters.overflow_count; + stats.overflow_peak = std::max(counters.overflow_count, stats.valid ? stats.overflow_peak : 0u); + stats.valid = true; +} + +// the lines of the cullings of the passes (the part of opengl46_renderer::Update_Debug_Info() after the scene statistics) +std::string vulkan14_renderer::cull_info() +{ + std::string text; + if (!m_context || (false == m_context->scene.cull.ready)) + { + return text; + } + auto &cull{m_context->scene.cull}; + text += cull.main.info(); + if (Global.gfx_gpucull_shadows) + { + for (std::size_t cascade = 0; cascade < cull.cascades.size(); ++cascade) + { + if (false == cull.cascades[cascade].ran()) + { + continue; + } + cull.cascades[cascade].collect(); + text += cull.cascades[cascade].info("gpu cull shadows " + std::to_string(cascade + 1), false); + } + } + if (Global.gfx_gpucull_reflections && cull.reflections.ran()) + { + cull.reflections.collect(); + text += cull.reflections.info("gpu cull reflections, last face", false); + } + if (Global.gfx_gpucull != 0) + { + // the maps are drawn now and then, the numbers are of the last drawing of each + auto const add = [&](std::vector> &Culls, bool const Enabled, std::string const &Label) { + if (false == Enabled) + { + return; + } + for (std::size_t idx = 0; idx < Culls.size(); ++idx) + { + if (!Culls[idx] || (false == Culls[idx]->ran())) + { + continue; + } + Culls[idx]->collect(); + text += Culls[idx]->info("gpu cull " + Label + " " + std::to_string(idx + 1), false); + } + }; + add(cull.headlights, Global.gfx_gpucull_shadows_headlights, "headlight shadows"); + add(cull.lamps, Global.gfx_gpucull_shadows_lamps, "lamp shadows, scenery"); + } + if (Global.gfx_gpucull == 1) + { + // all passes of the last frame + auto const &walks{m_lastwalkcounts}; + text += "\nwalks of the submodel trees in the frame: " + std::to_string(walks.walked) + " made (" + std::to_string(walks.changing) + " not kept), " + std::to_string(walks.reused) + + " kept from earlier frames (" + std::to_string(m_walkrecords.size()) + " groups), cpu " + to_string(walks.walktime, 3) + " ms; the draws put in order and sent, cpu " + + to_string(walks.ordertime, 3) + " ms"; + } + return text; +} + +// the statistics of the scene (the part "Scene statistics" of opengl46_renderer::Update_Debug_Info()) +std::string vulkan14_renderer::scene_info() +{ + auto &scene{m_context->scene}; + debug_stats shadowstats; + for (auto const &shadowpass : m_shadowpass) + { + shadowstats += shadowpass.draw_stats; + } + auto const &colour{m_laststats}; + auto const line = [](char const *Name, int const Colour, int const Shadows) { + return std::string(Name) + to_string(Colour, 7) + " +" + to_string(Shadows, 7) + " =" + to_string(Colour + Shadows, 7) + "\n"; + }; + // the cascades of the sun shadows: where they end and the size of a texel of their maps, fixed or fitted to the depth + std::string cascades; + if (Global.gfx_shadowmap_enabled && (scene.shadow_size > 0)) + { + auto const &fit{m_shadowfitstate}; + auto const size{static_cast(scene.shadow_size)}; + cascades = " sun shadow cascades to"; + for (std::size_t cascade = 0; cascade < gl::MAX_CASCADES; ++cascade) + { + cascades += " " + to_string(fit.splits[cascade + 1], 1); + } + cascades += " m, texels"; + for (std::size_t cascade = 0; cascade < gl::MAX_CASCADES; ++cascade) + { + cascades += " " + to_string(100.f * fit.drawn[cascade] / size, 1); + } + cascades += " cm"; + if (Global.gfx_shadow_sdsm) + { + if (fit.fitted) + { + cascades += " (whole slices"; + for (std::size_t cascade = 0; cascade < gl::MAX_CASCADES; ++cascade) + { + cascades += " " + to_string(100.f * fit.slice[cascade] / size, 1); + } + cascades += "), fitted to pixels " + to_string(fit.depthmin, 2) + "-" + to_string(fit.depthmax, 1) + " m " + std::to_string(fit.age) + " frames ago, view turned " + to_string(fit.turn, 1) + + " deg since"; + } + else + { + cascades += ", fixed: " + std::string{fit.reason != nullptr ? fit.reason : ""}; + } + } + cascades += "\n"; + } + std::string text{"triangles: " + to_string(static_cast(m_geometry.primitives_count()), 7) + "\n" + line("vehicles: ", colour.dynamics, shadowstats.dynamics) + + line("models: ", colour.models, shadowstats.models) + line("drawcalls: ", colour.drawcalls, shadowstats.drawcalls) + " instanced:" + to_string(colour.instances, 7) + " +" + + to_string(shadowstats.instances, 7) + " =" + to_string(colour.instances + shadowstats.instances, 7) + " (" + + std::to_string(colour.instanced_drawcalls + shadowstats.instanced_drawcalls) + " batches)\n"}; + if (Global.gfx_contactshadows > 0) + { + text += " contact shadows: " + std::string{Global.gfx_contactshadows == 3 ? "high" : Global.gfx_contactshadows == 2 ? "medium" : "low"} + ", rays " + + to_string(Global.gfx_contactshadows_length, 2) + " m within " + to_string(Global.gfx_contactshadows_range, 0) + " m\n"; + } + text += cascades + line(" submodels:", colour.submodels, shadowstats.submodels) + line(" paths: ", colour.paths, shadowstats.paths) + + line(" shapes: ", colour.shapes, shadowstats.shapes) + " traction: " + to_string(colour.traction, 7) + "\n" + " lines: " + to_string(colour.lines, 7) + "\n" + + "particles: " + to_string(colour.particles, 7) + "\nlights: " + to_string(static_cast(m_lights_lit), 7) + " (" + std::to_string(m_lights_dropped) + " over capacity), " + + (Global.gfx_lights_clustered ? "clustered" : "first " + std::to_string(std::max(Global.DynamicLightCount, 0))) + "\n" + m_locallights.info() + scene.lamps.info() + + scene.particles.info(); + auto const &stats{scene.clusters.stats}; + if (stats.valid && (Global.gfx_lights_clustered || Global.gfx_lights_clusterheatmap)) + { + text += "cluster lists: " + std::to_string(stats.index_count) + ", overflow " + std::to_string(stats.overflow_count) + " (peak " + std::to_string(stats.overflow_peak) + ")\n"; + } + return text; +} + +void vulkan14_renderer::draw_debug_ui() +{ + if ((false == m_debug_ui_active) || !m_context) + { + return; + } + auto &scene{m_context->scene}; + collect_cluster_stats(); + + if (ImGui::Begin("Headlight config", &m_debug_ui_active)) + { + ImGui::SetWindowSize(ImVec2(0, 0)); + auto &conf{m_headlightconfig}; + ImGui::SliderFloat("in_cutoff", &conf.in_cutoff, 0.9f, 1.1f); + ImGui::SliderFloat("out_cutoff", &conf.out_cutoff, 0.9f, 1.1f); + ImGui::SliderFloat("falloff_linear", &conf.falloff_linear, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("falloff_quadratic", &conf.falloff_quadratic, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("ambient", &conf.ambient, 0.0f, 3.0f); + ImGui::SliderFloat("intensity", &conf.intensity, 0.0f, 10.0f); + } + ImGui::End(); + + if (ImGui::Begin("Light clusters")) + { + ImGui::Checkbox("clustered lights (off: the first dynamiclights headlights)", &Global.gfx_lights_clustered); + ImGui::Checkbox("heatmap", &Global.gfx_lights_clusterheatmap); + ImGui::Checkbox("beams of vehicle headlights (gfx.lights.headlights)", &Global.gfx_lights_headlights); + auto const ¶ms{scene.clusters.params}; + ImGui::Text("grid %u x %u x %u, %s", gl::CLUSTER_GRID_X, gl::CLUSTER_GRID_Y, gl::CLUSTER_GRID_Z, params.valid ? "built" : "none (orthographic view)"); + ImGui::Text("near %.3f m, far %.1f m", params.znear, params.zfar); + ImGui::Text("headlights %u, %u over capacity of %u", m_lights_lit, m_lights_dropped, static_cast(gl::MAX_LIGHTS - 1)); + auto const &stats{scene.clusters.stats}; + if (stats.valid) + { + ImGui::Text("index list %u of %u", stats.index_count, gl::CLUSTER_COUNT * gl::MAX_LIGHTS_PER_CLUSTER); + ImGui::Text("overflow: %u lights dropped from full clusters (limit %u each), peak %u", stats.overflow_count, gl::MAX_LIGHTS_PER_CLUSTER, stats.overflow_peak); + } + ImGui::Separator(); + m_locallights.draw_debug_ui(); + ImGui::Separator(); + scene.lamps.draw_debug_ui(); + ImGui::Separator(); + char const *contactlevels[]{"no", "low (the sun)", "medium (the sun and 2 lamps)", "high (the sun and 4 lamps)"}; + ImGui::Combo("contact shadows (gfx.contactshadows)", &Global.gfx_contactshadows, contactlevels, IM_ARRAYSIZE(contactlevels)); + ImGui::SliderFloat("length of their rays, m (gfx.contactshadows.length)", &Global.gfx_contactshadows_length, 0.05f, 5.f, "%.2f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("distance they reach, m (gfx.contactshadows.range)", &Global.gfx_contactshadows_range, 1.f, 1000.f, "%.0f", ImGuiSliderFlags_Logarithmic); + } + ImGui::End(); + + if (ImGui::Begin("Sun shadows")) + { + ImGui::Checkbox("cascades fitted to the depth of the view (gfx.shadow.sdsm)", &Global.gfx_shadow_sdsm); + ImGui::SliderFloat("splits, 0 equal lengths .. 1 equal ratios (gfx.shadow.sdsm.split)", &Global.gfx_shadow_sdsm_split, 0.f, 1.f, "%.2f"); + auto const &fit{m_shadowfitstate}; + if (Global.gfx_shadow_sdsm) + { + if (fit.fitted) + { + ImGui::Text("fitted to pixels %.2f - %.1f m, measured %u frames ago, view turned %.1f deg since", fit.depthmin, fit.depthmax, fit.age, fit.turn); + } + else + { + ImGui::Text("fixed cascades: %s", fit.reason != nullptr ? fit.reason : ""); + } + } + if (scene.shadow_size > 0) + { + for (std::size_t cascade = 0; cascade < gl::MAX_CASCADES; ++cascade) + { + ImGui::Text("cascade %d: to %.1f m, map %.1f m across (whole slice %.1f m), texel %.1f cm", static_cast(cascade + 1), fit.splits[cascade + 1], fit.drawn[cascade], fit.slice[cascade], + 100.f * fit.drawn[cascade] / static_cast(scene.shadow_size)); + } + } + } + ImGui::End(); + + if (ImGui::Begin("Weather")) + { + m_weatherstate.draw_debug_ui(); + } + ImGui::End(); + + if (ImGui::Begin("Particles")) + { + ImGui::Checkbox("smoke simulated and drawn on the gpu (gfx.smoke.gpu)", &Global.gfx_smoke_gpu); + if (Global.gfx_smoke_gpu) + { + ImGui::Checkbox("smoke lit by the sun, the sky and the lamps (gfx.smoke.lighting)", &Global.gfx_smoke_lighting); + } + ImGui::Checkbox("rain and snow as particles of the gpu (gfx.precipitation.gpu)", &Global.gfx_precipitation_gpu); + if (Global.gfx_precipitation_gpu) + { + ImGui::SliderInt("particles of the heaviest rain or snow (gfx.precipitation.particles)", &Global.gfx_precipitation_particles, 0, 262144); + } + ImGui::SliderFloat("soft particles, share of their size, 0: off (gfx.particles.soft)", &Global.gfx_particles_soft, 0.f, 4.f, "%.2f"); + auto const info{scene.particles.info()}; + if (false == info.empty()) + { + ImGui::TextUnformatted(info.c_str() + 1); + } + } + ImGui::End(); + + if (ImGui::Begin("Sky and exposure")) + { + m_skylight.draw_debug_ui(); + ImGui::SliderFloat("sun disc, 1 = physical (gfx.sun.disc)", &Global.gfx_sun_disc, 0.f, 2.f); + ImGui::SliderFloat("moon disc (gfx.moon.disc)", &Global.gfx_moon_disc, 0.f, 8.f); + ImGui::SliderFloat("moonlight on the scenery (gfx.moonlight)", &Global.gfx_moonlight, 0.f, 10.f); + { + // where the sun and the moon are: azimuth from the north to the east (the sky has x to the west, z to the north) + auto const azimuth = [](glm::vec3 const &Direction) { return glm::degrees(std::atan2(-Direction.x, Direction.z)) + (Direction.x > 0.f ? 360.f : 0.f); }; + auto const &sun{simulation::Environment.sun()}; + auto const &moon{simulation::Environment.moon()}; + ImGui::Text("sun: elevation %.1f, azimuth %.1f", sun.getAngle(), azimuth(sun.getDirection())); + ImGui::Text("moon: elevation %.1f, azimuth %.1f, lit %.0f%%, age %.1f d", moon.getAngle(), azimuth(moon.getDirection()), 100.f * moon.getIllumination(), moon.getPhase()); + } + ImGui::Separator(); + gfx::exposure_settings::draw_debug_ui(scene.exposure.values); + ImGui::Separator(); + // the ambient occlusion (opengl46_ao::draw_debug_ui()): its images are made with the targets of the viewports + if (scene.targets.empty() || (scene.targets.front().ssao == nullptr)) + { + ImGui::TextUnformatted("ambient occlusion: off at the start, set gfx.postfx.ao in eu07.ini"); + } + else + { + char const *modes[]{"off", "older ssao", "ground truth (gtao)"}; + ImGui::Combo("ambient occlusion (gfx.postfx.ao)", &Global.gfx_postfx_ao, modes, IM_ARRAYSIZE(modes)); + if (Global.gfx_postfx_ao == 2) + { + ImGui::SliderFloat("ao radius (m)", &Global.gfx_postfx_ao_radius, 0.1f, 10.f, "%.2f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("ao strength", &Global.gfx_postfx_ao_strength, 0.f, 4.f); + ImGui::SliderFloat("ao bounce albedo", &Global.gfx_postfx_ao_bounce, 0.f, 1.f); + ImGui::SliderInt("ao slices", &Global.gfx_postfx_ao_slices, 1, 8); + ImGui::SliderInt("ao steps per side", &Global.gfx_postfx_ao_steps, 1, 16); + } + } + } + ImGui::End(); + + if (ImGui::Begin("Instance culling")) + { + char const *modes[]{"cpu", "gpu, drawn from its result", "cpu, compared with the gpu"}; + ImGui::Combo("instance culling (gfx.gpucull)", &Global.gfx_gpucull, modes, IM_ARRAYSIZE(modes)); + if (Global.gfx_gpucull == 1) + { + ImGui::Checkbox("leave out instances hidden behind the depth drawn before them (gfx.gpucull.occlusion)", &Global.gfx_gpucull_occlusion); + if (Global.gfx_gpucull_occlusion) + { + ImGui::Checkbox("draw the instances seen in the last frame first, to hide the others (gfx.gpucull.occlusion.instances)", &Global.gfx_gpucull_occlusion_instances); + } + ImGui::Checkbox("draw the depth of the instances before their colour (gfx.depthprepass)", &Global.gfx_depthprepass); + ImGui::Checkbox("draw the instances of the same material and model with one multi draw (gfx.gpucull.multidraw)", &Global.gfx_gpucull_multidraw); + } + if (Global.gfx_gpucull != 0) + { + ImGui::Checkbox("the same for the cascades of the sun shadows (gfx.gpucull.shadows)", &Global.gfx_gpucull_shadows); + ImGui::Checkbox("the same for the environment map, when it's drawn next (gfx.gpucull.reflections)", &Global.gfx_gpucull_reflections); + ImGui::Checkbox("the same for the shadow maps of headlights (gfx.gpucull.shadows.headlights)", &Global.gfx_gpucull_shadows_headlights); + ImGui::Checkbox("the same for the scenery of the shadow maps of lamps, when it's drawn next (gfx.gpucull.shadows.lamps)", &Global.gfx_gpucull_shadows_lamps); + ImGui::Checkbox("the same for the sleepers of the tracks of the view (gfx.gpucull.sleepers)", &Global.gfx_gpucull_sleepers); + ImGui::Checkbox("the same for the models drawn on their own, with lights, animations or skins of their own (gfx.gpucull.models)", &Global.gfx_gpucull_models); + ImGui::Checkbox("leave out the models of a cell lying wholly out of view, beyond their range or too small before the culling (gfx.gpucull.bucketrange)", &Global.gfx_gpucull_bucketrange); + ImGui::Checkbox("keep the walks of the submodel trees of the instances from frame to frame (gfx.gpucull.walkcache)", &Global.gfx_gpucull_walkcache); + } + ImGui::Checkbox("draw the depth of the whole scene before its colour (gfx.depthprepass.scene)", &Global.gfx_depthprepass_scene); + if (Global.gfx_contactshadows > 0) + { + ImGui::TextUnformatted("(drawn anyway for the contact shadows, gfx.contactshadows)"); + } + ImGui::Checkbox("grass of the heightmap terrain (gfx.grass)", &Global.gfx_grass); + if (Global.gfx_grass) + { + ImGui::SliderFloat("the grass drawn whole up to, m (gfx.grass.distance)", &Global.gfx_grass_distance, 10.f, 300.f, "%.0f"); + ImGui::SliderFloat("thinning out up to, m (gfx.grass.fadedistance)", &Global.gfx_grass_fadedistance, 10.f, 350.f, "%.0f"); + ImGui::SliderFloat("density of the grass (gfx.grass.density)", &Global.gfx_grass_density, 0.1f, 2.f, "%.2f"); + } + auto const info{cull_info()}; + if (false == info.empty()) + { + ImGui::TextUnformatted(info.c_str() + 1); + } + } + ImGui::End(); +} + +// the number of the lights of the cluster of each pixel tinted over the output of a viewport, from the depth of its scene +// (opengl46_renderer::Render_cluster_heatmap()), after its post-processing. outside a rendering +void vulkan14_renderer::render_cluster_heatmap(std::size_t const Index) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &targets{scene.targets[Index]}; + auto &draw{*m_draw}; + auto const commands{ctx.commands}; + if ((scene.clusters.params.valid == 0) || (false == bool(targets.output)) || (false == bool(targets.depth))) + { + return; + } + if ((scene.cluster_heatmap == nullptr) && (false == scene.cluster_heatmap_tried)) + { + scene.cluster_heatmap_tried = true; + try + { + auto program{std::make_shared()}; + program->vertex = ctx.shaders.get("quad.vert", "#define QUAD_SCENE 1\n"); + program->fragment = ctx.shaders.get("cluster_heatmap.frag"); + program->fragment_name = "cluster_heatmap.frag"; + program->vertices = vk14::vertex_kind::none; + program->name = "quad.vert + cluster_heatmap.frag"; + scene.cluster_heatmap = program; + } + catch (gfx::shader_exception const &e) + { + ErrorLog("light clusters: can't use the heatmap: " + std::string(e.what())); + } + } + if (scene.cluster_heatmap == nullptr) + { + return; + } + // the depth of the scene as the colour pass resolved it + vk14::barriers(commands, {vk14::use_of(*targets.output, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, heatmap_colour_stage, heatmap_colour_access), + vk14::use_of(*targets.depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT)}); + VkRenderingAttachmentInfo attachment{}; + attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; + attachment.imageView = targets.output->view(); + attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + VkRenderingInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + rendering.renderArea = {{0, 0}, targets.extent}; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &attachment; + commands->begin_rendering(&rendering); + // the clusters of the pass set of the colour pass, with the offsets it was drawn with + auto const offsets{std::array{draw.scene_offset, draw.light_offset, draw.cluster_offset, draw.weather_offset}}; + start_draws(targets.output_formats, static_cast(targets.extent.width), static_cast(targets.extent.height), m_viewports[Index].main ? vk14::scene_state::set_colour : vk14::scene_state::set_plain, false); + draw.scene_offset = offsets[0]; + draw.light_offset = offsets[1]; + draw.cluster_offset = static_cast(scene.clusters.params_offset); + draw.weather_offset = offsets[3]; + if (bind_scene_pipeline(*scene.cluster_heatmap, targets.output_formats, vk14::blend_mode::scene_alpha, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP)) + { + bind_pass_set(); + std::array images; + images.fill(VkDescriptorImageInfo{ctx.sampling.clamp_nearest, targets.depth->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}); + scene.material_set.push(commands, scene.scene_layout, images.data()); + vk14::draw_constants constants; + push_constants(&constants); + commands->draw(4, 1, 0, 0); + } + commands->end_rendering(); + end_draws(); +} diff --git a/rendering/vulkan14/vulkan14descriptors.cpp b/rendering/vulkan14/vulkan14descriptors.cpp index f0238e01..f64c4f13 100644 --- a/rendering/vulkan14/vulkan14descriptors.cpp +++ b/rendering/vulkan14/vulkan14descriptors.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/vulkan14/vulkan14descriptors.h" +#include "rendering/vulkan14/vulkan14commands.h" #include "rendering/vulkan14/vulkan14device.h" namespace vk14 @@ -20,14 +21,17 @@ bool samplers::create(device const &Device, float Anisotropy, std::string &Reaso { auto const handle{Device.handle()}; auto const maxanisotropy{Device.caps().limits.maxSamplerAnisotropy}; - auto const make = [&](VkSampler &Sampler, VkFilter Filter, VkSamplerAddressMode Address, bool Mipmaps, float Aniso, bool Compare, char const *Name) { + VkFormatProperties depthformat{}; + vkGetPhysicalDeviceFormatProperties(Device.physical(), VK_FORMAT_D32_SFLOAT, &depthformat); + auto const depthlinear{(depthformat.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0}; + auto const make = [&](VkSampler &Sampler, VkFilter Filter, VkSamplerAddressMode Address, bool Mipmaps, float Aniso, bool Compare, char const *Name, VkSamplerAddressMode AddressV = VK_SAMPLER_ADDRESS_MODE_MAX_ENUM) { VkSamplerCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; info.magFilter = Filter; info.minFilter = Filter; info.mipmapMode = (Filter == VK_FILTER_LINEAR ? VK_SAMPLER_MIPMAP_MODE_LINEAR : VK_SAMPLER_MIPMAP_MODE_NEAREST); info.addressModeU = Address; - info.addressModeV = Address; + info.addressModeV = (AddressV != VK_SAMPLER_ADDRESS_MODE_MAX_ENUM ? AddressV : Address); info.addressModeW = Address; info.maxLod = (Mipmaps ? VK_LOD_CLAMP_NONE : 0.f); if (Aniso > 1.f) @@ -35,6 +39,8 @@ bool samplers::create(device const &Device, float Anisotropy, std::string &Reaso info.anisotropyEnable = VK_TRUE; info.maxAnisotropy = std::min(Aniso, maxanisotropy); } + // the border of the clamped samplers: the farthest depth of the reverse z, black + info.borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK; if (Compare) { // as GL_LEQUAL of the opengl renderer: 1 (in the shadow) where the reference isn't nearer the light than the map (reverse @@ -54,12 +60,16 @@ bool samplers::create(device const &Device, float Anisotropy, std::string &Reaso }; return make(repeat_aniso, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_REPEAT, true, Anisotropy, false, "repeat aniso") && make(clamp_aniso, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, true, Anisotropy, false, "clamp aniso") && make(clamp_linear, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, true, 1.f, false, "clamp linear") && make(clamp_nearest, VK_FILTER_NEAREST, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, false, 1.f, false, "clamp nearest") && - make(shadow, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, false, 1.f, true, "shadow"); + make(shadow, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, false, 1.f, true, "shadow") && + make(repeat_linear, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_REPEAT, true, 1.f, false, "repeat linear") && + make(panorama, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_REPEAT, false, 1.f, false, "panorama", VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE) && + make(repeat_nearest, VK_FILTER_NEAREST, VK_SAMPLER_ADDRESS_MODE_REPEAT, false, 1.f, false, "repeat nearest") && + make(depth, (depthlinear ? VK_FILTER_LINEAR : VK_FILTER_NEAREST), VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, false, 1.f, false, "depth"); } void samplers::destroy(VkDevice Device) { - for (auto *sampler : {&repeat_aniso, &clamp_aniso, &clamp_linear, &clamp_nearest, &shadow}) + for (auto *sampler : {&repeat_aniso, &clamp_aniso, &clamp_linear, &clamp_nearest, &shadow, &repeat_linear, &panorama, &repeat_nearest, &depth}) { if (*sampler != VK_NULL_HANDLE) { @@ -138,7 +148,7 @@ void push_set::destroy(VkDevice Device) } } -void push_set::push(VkCommandBuffer Commands, VkPipelineLayout Layout, void const *Data) const +void push_set::push(command_stream *Commands, VkPipelineLayout Layout, void const *Data) const { VkPushDescriptorSetWithTemplateInfo info{}; info.sType = VK_STRUCTURE_TYPE_PUSH_DESCRIPTOR_SET_WITH_TEMPLATE_INFO; @@ -146,7 +156,7 @@ void push_set::push(VkCommandBuffer Commands, VkPipelineLayout Layout, void cons info.layout = Layout; info.set = set; info.pData = Data; - vkCmdPushDescriptorSetWithTemplate2(Commands, &info); + Commands->push_descriptor_set_with_template2(&info, m_bindings.size() * m_stride, bind_point); } VkPipelineLayout make_pipeline_layout(VkDevice Device, std::vector const &Sets, std::vector const &PushConstants, std::string const &Name) diff --git a/rendering/vulkan14/vulkan14descriptors.h b/rendering/vulkan14/vulkan14descriptors.h index 8e3b52fd..e28cf69e 100644 --- a/rendering/vulkan14/vulkan14descriptors.h +++ b/rendering/vulkan14/vulkan14descriptors.h @@ -28,6 +28,12 @@ struct samplers VkSampler clamp_linear{VK_NULL_HANDLE}; // images of the user interface, post-processing VkSampler clamp_nearest{VK_NULL_HANDLE}; VkSampler shadow{VK_NULL_HANDLE}; // depth compared (shadow maps, reverse z) + VkSampler repeat_linear{VK_NULL_HANDLE}; // repeated, mipmapped, without anisotropy (the noises of the clouds) + VkSampler panorama{VK_NULL_HANDLE}; // repeated across, clamped up and down (the panorama of the clouds around the camera) + VkSampler repeat_nearest{VK_NULL_HANDLE}; // repeated, texel by texel (the noise of the ssao) + // the depth of the scene read by the post-processing, as the render targets of the depth of the opengl renderer: filtered where + // the device filters the depth format, the border the farthest depth (0) + VkSampler depth{VK_NULL_HANDLE}; bool create(device const &Device, float Anisotropy, std::string &Reason); void destroy(VkDevice Device); @@ -48,7 +54,7 @@ struct push_set bool create_template(VkDevice Device, VkPipelineLayout Layout, std::string const &Name); void destroy(VkDevice Device); // pushes Data (laid out as create() was told) - void push(VkCommandBuffer Commands, VkPipelineLayout Layout, void const *Data) const; + void push(command_stream *Commands, VkPipelineLayout Layout, void const *Data) const; private: std::vector m_bindings; diff --git a/rendering/vulkan14/vulkan14frame.cpp b/rendering/vulkan14/vulkan14frame.cpp index 068aa49f..cb872760 100644 --- a/rendering/vulkan14/vulkan14frame.cpp +++ b/rendering/vulkan14/vulkan14frame.cpp @@ -15,8 +15,10 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/vulkan14/vulkan14context.h" +#include "rendering/vulkan14/vulkan14commands.h" #include "rendering/screenshot.h" #include "rendering/editoroverlay.h" +#include "rendering/common/exposure.h" #include "application/application.h" #include "simulation/simulation.h" #include "simulation/simulationenvironment.h" @@ -187,7 +189,7 @@ VkPipelineStageFlags2 const sampled_stage{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BI VkAccessFlags2 const sampled_access{VK_ACCESS_2_SHADER_SAMPLED_READ_BIT}; // the copies to the buffers the cpu reads back, made seen by the host once the gpu is done with the frame -void host_read_barrier(VkCommandBuffer Commands) +void host_read_barrier(vk14::command_stream *Commands) { VkMemoryBarrier2 barrier{}; barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; @@ -199,7 +201,7 @@ void host_read_barrier(VkCommandBuffer Commands) dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.memoryBarrierCount = 1; dependency.pMemoryBarriers = &barrier; - vkCmdPipelineBarrier2(Commands, &dependency); + Commands->pipeline_barrier2(&dependency); } // the images the colour of a viewport is drawn to, as its rendering uses them @@ -305,7 +307,8 @@ void vulkan14_renderer::init_frame_data() namespace { -bool make_targets(vk14::allocator &Memory, vk14::deletion_queue &Deletions, std::uint64_t const Value, std::uint32_t &Formatscount, VkSampleCountFlags const Samplecounts, vk14::viewport_targets &Targets, int const Width, int const Height, bool const Srgb) +bool make_targets(vk14::allocator &Memory, vk14::deletion_queue &Deletions, std::uint64_t const Value, std::uint32_t &Formatscount, VkSampleCountFlags const Samplecounts, VkFormat const Bloomformat, vk14::viewport_targets &Targets, + int const Width, int const Height, bool const Srgb) { // as many samples as asked for, as far as the device has them for the colour and the depth auto samples{static_cast(std::clamp(1 << Global.iMultisampling, 1, 8))}; @@ -324,6 +327,8 @@ bool make_targets(vk14::allocator &Memory, vk14::deletion_queue &Deletions, std: Deletions.push(Value, [old = std::move(Targets)]() {}); auto const oldformats{Targets.formats}; auto const oldoutput{Targets.output_formats}; + auto const oldpost{Targets.post_formats}; + auto const olddepth{Targets.depth_formats}; Targets = vk14::viewport_targets{}; Targets.extent = extent; Targets.samples = samples; @@ -370,6 +375,66 @@ bool make_targets(vk14::allocator &Memory, vk14::deletion_queue &Deletions, std: ok = ok && make(Targets.tonemapped, output, sampledcolour, VK_SAMPLE_COUNT_1_BIT, "tonemapped"); } ok = ok && make(Targets.output, output, sampledcolour | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, VK_SAMPLE_COUNT_1_BIT, "output"); + // the ambient occlusion, half the size, when it's on + if (ok && (Global.gfx_postfx_ao != 0)) + { + auto makehalf = [&](std::shared_ptr &Image, char const *Name) { + vk14::image_desc desc; + desc.format = VK_FORMAT_R8_UNORM; + desc.extent = {std::max(extent.width / 2, 1u), std::max(extent.height / 2, 1u), 1}; + desc.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + Image = std::make_shared(); + if (false == Image->create(Memory, desc, Name)) + { + Image.reset(); + return false; + } + return true; + }; + if ((false == makehalf(Targets.ssao, "ambient occlusion")) || (false == makehalf(Targets.ssao_blur, "ambient occlusion smoothed"))) + { + ErrorLog("ambient occlusion framebuffer setup failed"); + Targets.ssao.reset(); + Targets.ssao_blur.reset(); + } + } + // the bloom chain, made whether the bloom is on or not: it can be switched on in the debug window + if (ok) + { + auto const sizes{gfx::exposure_settings::bloom_sizes(static_cast(extent.width), static_cast(extent.height))}; + auto makelevel = [&](std::vector> &Levels, glm::ivec2 const &Size, char const *Name) { + vk14::image_desc desc; + desc.format = Bloomformat; + desc.extent = {static_cast(Size.x), static_cast(Size.y), 1}; + desc.usage = sampledcolour; + auto level{std::make_shared()}; + if (false == level->create(Memory, desc, Name)) + { + return false; + } + Levels.push_back(level); + return true; + }; + auto bloomok{true}; + for (std::size_t level = 0; bloomok && (level < sizes.size()); ++level) + { + bloomok = makelevel(Targets.bloom_down, sizes[level], "bloom down"); + } + for (std::size_t level = 0; bloomok && (level + 1 < sizes.size()); ++level) + { + bloomok = makelevel(Targets.bloom_up, sizes[level], "bloom up"); + } + if (bloomok) + { + Targets.bloom_sizes = sizes; + } + else + { + Targets.bloom_down.clear(); + Targets.bloom_up.clear(); + ErrorLog("bloom framebuffer setup failed, no bloom"); + } + } if (false == ok) { vk14::log_error("the images of a viewport of " + std::to_string(extent.width) + "x" + std::to_string(extent.height) + " can't be made"); @@ -382,12 +447,18 @@ bool make_targets(vk14::allocator &Memory, vk14::deletion_queue &Deletions, std: Targets.formats.samples = samples; Targets.output_formats.colour = {output, VK_FORMAT_UNDEFINED}; Targets.output_formats.colours = 1; + Targets.post_formats.colour = {hdr, VK_FORMAT_UNDEFINED}; + Targets.post_formats.colours = 1; + Targets.depth_formats.depth = depth; + Targets.depth_formats.samples = samples; // the pipelines are made for the number of the formats: the same formats keep theirs auto const same = [](vk14::target_formats const &Left, vk14::target_formats const &Right) { return (Left.colour == Right.colour) && (Left.colours == Right.colours) && (Left.depth == Right.depth) && (Left.samples == Right.samples); }; Targets.formats.id = ((oldformats.id != 0) && same(oldformats, Targets.formats) ? oldformats.id : ++Formatscount); Targets.output_formats.id = ((oldoutput.id != 0) && same(oldoutput, Targets.output_formats) ? oldoutput.id : ++Formatscount); + Targets.post_formats.id = ((oldpost.id != 0) && same(oldpost, Targets.post_formats) ? oldpost.id : ++Formatscount); + Targets.depth_formats.id = ((olddepth.id != 0) && same(olddepth, Targets.depth_formats) ? olddepth.id : ++Formatscount); return true; } @@ -409,6 +480,13 @@ bool vulkan14_renderer::Render() Timer::subsystem.gfx_total.start(); + // the render thread is done with the frame before: the queue, the swapchains and the frames are the main thread's again + if (auto const *failure{ctx.drain()}) + { + report_device_lost(failure); + return false; + } + // the gpu has to be done with the frame which used the slot before if (frame.done > 0) { @@ -422,6 +500,11 @@ bool vulkan14_renderer::Render() // what the frames done used last goes ctx.deletions.collect(std::max(ctx.completed(), frame.done)); VK14_VERIFY(vkResetCommandPool(device, frame.pool, 0)); + for (auto &worker : frame.workers) + { + VK14_VERIFY(vkResetCommandPool(device, worker.pool, 0)); + worker.used = 0; + } ctx.ring.begin_frame(slot); scene.draws.ring.begin_frame(slot); scene.instances.ring.begin_frame(slot); @@ -447,15 +530,25 @@ bool vulkan14_renderer::Render() } } - VkCommandBufferBeginInfo begin{}; - begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - VK14_VERIFY(vkBeginCommandBuffer(frame.uploads, &begin)); - VK14_VERIFY(vkBeginCommandBuffer(frame.commands, &begin)); + // the calls of the frame are kept, recorded into the command buffers once the frame is done + frame.upload_stream.clear(); + frame.commands.clear(); auto const value{ctx.timelinevalue + 1}; - ctx.uploads.begin_frame(frame.uploads, value); + ctx.uploads.begin_frame(&frame.upload_stream, value); ctx.slot = slot; - ctx.commands = frame.commands; + ctx.commands = &frame.commands; + if (scene.cull.ready) + { + // the cullings share what they need of the frame + scene.cull.main.begin_frame(slot, value, ctx.completed()); + } + scene.lamps.begin_frame(ctx.commands, value); + if (scene.terrain) + { + // textures of new heightmap terrain layers go to the arrays the terrain shader reads them from + scene.terrain->begin_frame(ctx.commands, value); + scene.terrain->update(); + } // the copies of the light buffer were in the part of the ring of another frame m_lightheaders.clear(); // the content other threads gave the textures (python) @@ -494,22 +587,36 @@ bool vulkan14_renderer::Render() } frame.number = ctx.framecount; - ctx.passes.begin_frame(frame.commands, slot, ctx.framecount); + ctx.passes.begin_frame(ctx.commands, slot, ctx.framecount); // the stand-ins put in place the first time: the empty maps cleared if (false == scene.images_ready) { scene.images_ready = true; - ctx.passes.begin_pass("frame setup", {vk14::use_of(*scene.shadow_empty, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_CLEAR_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT), - vk14::use_of(*scene.env_empty, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_CLEAR_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT)}); + std::vector const standins{scene.shadow_empty.get(), scene.env_empty.get(), scene.volume_empty.get(), scene.cube_shadow_empty.get(), scene.headlight_shadow_empty.get(), scene.depth_empty.get()}; + std::vector clears; + for (auto *image : standins) + { + clears.push_back(vk14::use_of(*image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_CLEAR_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT)); + } + ctx.passes.begin_pass("frame setup", clears); + // the empty shadow maps hold the farthest depth, the empty images black VkClearDepthStencilValue const farthest{0.f, 0}; - VkImageSubresourceRange const depthrange{VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, gl::MAX_CASCADES}; - vkCmdClearDepthStencilImage(frame.commands, scene.shadow_empty->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &farthest, 1, &depthrange); VkClearColorValue const black{{0.f, 0.f, 0.f, 0.f}}; - VkImageSubresourceRange const colourrange{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 6}; - vkCmdClearColorImage(frame.commands, scene.env_empty->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &colourrange); - std::vector uses{vk14::use_of(*scene.shadow_empty, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access), - vk14::use_of(*scene.env_empty, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}; + std::vector uses; + for (auto *image : standins) + { + VkImageSubresourceRange const range{image->aspect(), 0, image->desc().mips, 0, image->desc().layers}; + if (image->aspect() == VK_IMAGE_ASPECT_DEPTH_BIT) + { + ctx.commands->clear_depth_stencil_image(image->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &farthest, 1, &range); + } + else + { + ctx.commands->clear_color_image(image->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); + } + uses.push_back(vk14::use_of(*image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)); + } // the maps which aren't drawn yet are read before they are (the pass sets hold them) if (scene.shadow_map) { @@ -519,9 +626,9 @@ bool vulkan14_renderer::Render() { uses.push_back(vk14::use_of(*scene.env_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)); } - vk14::barriers(frame.commands, uses); + vk14::barriers(ctx.commands, uses); // no light is assigned to any cluster until the culling pass runs - vkCmdFillBuffer(frame.commands, scene.clusters.ranges.handle(), 0, VK_WHOLE_SIZE, 0); + ctx.commands->fill_buffer(scene.clusters.ranges.handle(), 0, VK_WHOLE_SIZE, 0); VkMemoryBarrier2 barrier{}; barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; barrier.srcStageMask = VK_PIPELINE_STAGE_2_CLEAR_BIT; @@ -532,9 +639,17 @@ bool vulkan14_renderer::Render() dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.memoryBarrierCount = 1; dependency.pMemoryBarriers = &barrier; - vkCmdPipelineBarrier2(frame.commands, &dependency); + ctx.commands->pipeline_barrier2(&dependency); + // the light of the sky and the exposure from their first state + clear_sky_images(); ctx.passes.end_pass(); } + // the block of the weather and the scene for the compute passes before the first pass of the scene + m_draw->scene_offset = 0; + m_draw->light_offset = 0; + m_draw->cluster_offset = 0; + upload_scene_block(); + upload_weather_block(); // fetch simulation data if (simulation::is_ready) @@ -544,6 +659,16 @@ bool vulkan14_renderer::Render() auto const quantizationstep{0.004f}; m_sunlight.direction = glm::normalize(quantizationstep * glm::roundEven(m_sunlight.direction * (1.f / quantizationstep))); m_fogcolor = Global.FogColor; + // 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. the volumetric clouds of the previous frame tell the sky whether to look overcast + // and dim the sun itself + m_skylight.set_clouds(m_weatherstate.clouds_drawn(), m_weatherstate.cloud_shadows()); + update_sky(); + // fog, clouds and stars of the weather, in the light of the sky worked out above + if (false == Global.gfx_skiprendering) + { + update_weather(); + } // lamps of models and vehicles around the camera and the cab lamp, for the views of the frame to take into their light buffers if ((false == Global.gfx_skiprendering) && (simulation::Region != nullptr)) { @@ -554,6 +679,10 @@ bool vulkan14_renderer::Render() { // a scenery is being loaded: the cells, instances and models the lights were taken from may be gone m_locallights.reset(); + scene.lamps.reset_lamps(); + scene.lamps.reset_headlights(); + // and so may the tracks the sleeper records were made for, and the models drawn on their own + Clear_Indirect_Records(); scene.glare_visible.clear(); } if ((EditorModeFlag) || (m_nodestateversion != scene::node_state_version)) @@ -562,6 +691,13 @@ bool vulkan14_renderer::Render() ++m_instancerecordsversion; m_nodestateversion = scene::node_state_version; } + // the sleeper records of tracks out of sight for a while, and the records of the models drawn on their own + Expire_Indirect_Records(); + // scenery lamps which get shadows in this frame. their maps hold the scenery as of the version of the instance records + if (simulation::is_ready && (false == Global.gfx_skiprendering)) + { + scene.lamps.update_lamps(m_locallights.lights(), Global.pCamera.Pos, m_instancerecordsversion, m_framestamp); + } m_renderpass.draw_mode = rendermode::none; // the vehicles which moved in the last frame (motion blur) m_vehiclesmoved = m_vehiclesmoving; @@ -588,6 +724,7 @@ bool vulkan14_renderer::Render() // the user interface after the scene, as in the opengl renderer: imgui makes its frame, the images it asks for are uploaded in // this frame Timer::subsystem.gfx_gui.start(); + draw_debug_ui(); Application.render_ui(); Timer::subsystem.gfx_gui.stop(); @@ -615,19 +752,19 @@ bool vulkan14_renderer::Render() rendering.layerCount = 1; rendering.colorAttachmentCount = 1; rendering.pColorAttachments = &attachment; - vkCmdBeginRendering(frame.commands, &rendering); + ctx.commands->begin_rendering(&rendering); if (drawscene && (viewport < scene.targets.size()) && scene.targets[viewport].output) { present_viewport(viewport); } if (ismain) { - ctx.imgui.record(frame.commands, window->extent(), window->format(), window->srgb()); + ctx.imgui.record(ctx.commands, window->extent(), window->format(), window->srgb()); uidrawn = true; } - vkCmdEndRendering(frame.commands); + ctx.commands->end_rendering(); ctx.passes.end_pass(); - vk14::barriers(frame.commands, {window->use(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_PIPELINE_STAGE_2_NONE, VK_ACCESS_2_NONE)}); + vk14::barriers(ctx.commands, {window->use(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_PIPELINE_STAGE_2_NONE, VK_ACCESS_2_NONE)}); } if (false == uidrawn) { @@ -643,7 +780,7 @@ bool vulkan14_renderer::Render() scene.screenshot = std::make_shared(); if (scene.screenshot->create(ctx.memory, static_cast(extent.width) * extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::readback, "screenshot")) { - vk14::barriers(frame.commands, {vk14::use_of(output, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT)}); + vk14::barriers(ctx.commands, {vk14::use_of(output, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT)}); VkBufferImageCopy2 region{}; region.sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2; region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; @@ -655,76 +792,69 @@ bool vulkan14_renderer::Render() copy.dstBuffer = scene.screenshot->handle(); copy.regionCount = 1; copy.pRegions = ®ion; - vkCmdCopyImageToBuffer2(frame.commands, ©); - host_read_barrier(frame.commands); + ctx.commands->copy_image_to_buffer2(©); + host_read_barrier(ctx.commands); scene.screenshot_pending = true; scene.screenshot_value = value; scene.screenshot_extent = extent; } } ctx.passes.end_frame(); - auto const uploaded{ctx.uploads.end_frame()}; - VK14_VERIFY(vkEndCommandBuffer(frame.uploads)); - VK14_VERIFY(vkEndCommandBuffer(frame.commands)); + vulkan14_renderer::context::render_job job; + job.target = &frame; + job.uploaded = ctx.uploads.end_frame(); + // the stream of the frame is the render thread's now: nothing records into it any more + ctx.commands = nullptr; + m_draw->commands = nullptr; + scene.lamps.end_frame(); + if (scene.terrain) + { + scene.terrain->end_frame(); + } ctx.ring.flush(); scene.draws.ring.flush(); scene.instances.ring.flush(); - ctx.commands = VK_NULL_HANDLE; - std::vector waits; - std::vector signals; for (auto *window : ctx.acquired) { VkSemaphoreSubmitInfo wait{}; wait.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; wait.semaphore = window->acquire_semaphore(slot); wait.stageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; - waits.push_back(wait); + job.waits.push_back(wait); VkSemaphoreSubmitInfo signal{}; signal.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; signal.semaphore = window->present_semaphore(); signal.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; - signals.push_back(signal); + job.signals.push_back(signal); } VkSemaphoreSubmitInfo timelinesignal{}; timelinesignal.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO; timelinesignal.semaphore = ctx.timeline; timelinesignal.value = value; timelinesignal.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; - signals.push_back(timelinesignal); + job.signals.push_back(timelinesignal); - // the uploads first, in the same submit: their barriers make them done before the frame uses what they copied - std::vector commands; - if (uploaded) + if (ctx.render_threaded()) { - VkCommandBufferSubmitInfo info{}; - info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; - info.commandBuffer = frame.uploads; - commands.push_back(info); + // the render thread records, submits and presents the frame while the simulation goes on; the value of the frame is + // known now, the gpu reaches it once the thread submitted it + job.windows.swap(ctx.acquired); + frame.done = ctx.timelinevalue = value; + ctx.hand_over(std::move(job)); } + else { - VkCommandBufferSubmitInfo info{}; - info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; - info.commandBuffer = frame.commands; - commands.push_back(info); + char const *failure{nullptr}; + if (false == ctx.execute(job, failure)) + { + report_device_lost(failure); + return false; + } + frame.done = ctx.timelinevalue = value; } - VkSubmitInfo2 submit{}; - submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; - submit.waitSemaphoreInfoCount = static_cast(waits.size()); - submit.pWaitSemaphoreInfos = waits.data(); - submit.commandBufferInfoCount = static_cast(commands.size()); - submit.pCommandBufferInfos = commands.data(); - submit.signalSemaphoreInfoCount = static_cast(signals.size()); - submit.pSignalSemaphoreInfos = signals.data(); - if (!VK14_VERIFY(vkQueueSubmit2(ctx.device.graphics_queue(), 1, &submit, VK_NULL_HANDLE))) - { - report_device_lost("submitting a frame"); - return false; - } - frame.done = ctx.timelinevalue = value; - m_laststats = m_colorpass.draw_stats; ++m_framestamp; Timer::subsystem.gfx_total.stop(); @@ -743,7 +873,8 @@ void vulkan14_renderer::render_scene() { auto const &viewport{m_viewports[i]}; auto const srgb{false == Global.gfx_shadergamma}; - if (false == make_targets(ctx.memory, ctx.deletions, ctx.uploads.current_value(), scene.formats_count, ctx.device.caps().limits.framebufferColorSampleCounts & ctx.device.caps().limits.framebufferDepthSampleCounts, scene.targets[i], viewport.width, viewport.height, srgb)) + if (false == make_targets(ctx.memory, ctx.deletions, ctx.uploads.current_value(), scene.formats_count, ctx.device.caps().limits.framebufferColorSampleCounts & ctx.device.caps().limits.framebufferDepthSampleCounts, scene.bloom_format, + scene.targets[i], viewport.width, viewport.height, srgb)) { continue; } @@ -781,7 +912,7 @@ void vulkan14_renderer::start_draws(vk14::target_formats const &Targets, int con vk14::set_draw_state(draw.commands, draw.extent); // the faces counter-clockwise in the window of opengl are clockwise in the framebuffer of vulkan, which counts its rows the // other way (with the same memory of the images, decision K8) - vkCmdSetFrontFace(draw.commands, VK_FRONT_FACE_CLOCKWISE); + draw.commands->set_front_face(VK_FRONT_FACE_CLOCKWISE); } void vulkan14_renderer::end_draws() @@ -789,6 +920,8 @@ void vulkan14_renderer::end_draws() auto &draw{*m_draw}; draw.rendering = false; draw.program = nullptr; + draw.resume.valid = false; + draw.cull_sets = nullptr; } void vulkan14_renderer::upload_scene_block() @@ -984,12 +1117,15 @@ void vulkan14_renderer::setup_pass(viewport_config &Viewport, renderpass_config bounding_viewports_count++; } pass_camera /= std::max(bounding_viewports_count, 1); + // ...a cascade fitted to the depth of the view (gfx.shadow.sdsm) covers the area of the pixels it takes, within the slice of the + // view... + auto quantizationstep{gfx::shadow_fit::fit_slice(frustumchunkshapepoints, m_shadowfitarea, gfx::shadow_fit::light_rotation(m_sunlight.direction), m_shadowslicesize)}; // ...modelview matrix: determine the centre of frustum chunk in world space... glm::vec3 frustumchunkmin, frustumchunkmax; bounding_box(frustumchunkmin, frustumchunkmax, std::begin(frustumchunkshapepoints), std::end(frustumchunkshapepoints)); auto const frustumchunkcentre = (frustumchunkmin + frustumchunkmax) * 0.5f; // ...cap the vertical angle to keep shadows from getting too long... - auto const lightvector{glm::normalize(glm::vec3{m_sunlight.direction.x, std::min(m_sunlight.direction.y, Global.gfx_shadow_angle_min), m_sunlight.direction.z})}; + auto const lightvector{gfx::shadow_fit::light_vector(m_sunlight.direction)}; // ...place the light source at the calculated centre and setup world space light view matrix... camera.position() = pass_camera + glm::dvec3{frustumchunkcentre}; viewmatrix *= glm::lookAt(camera.position(), camera.position() + glm::dvec3{lightvector}, glm::dvec3{0.f, 1.f, 0.f}); @@ -1002,9 +1138,13 @@ void vulkan14_renderer::setup_pass(viewport_config &Viewport, renderpass_config bounding_box(frustumchunkmin, frustumchunkmax, std::begin(frustumchunkshapepoints), std::end(frustumchunkshapepoints)); // quantize the frustum points and add some padding, to reduce shadow shimmer on scale changes auto const frustumchunkdepth{zfar - (Znear > 1.f ? Znear : 0.f)}; - auto const quantizationstep{std::min(frustumchunkdepth, 50.f)}; + if (quantizationstep <= 0.f) + { + quantizationstep = std::min(frustumchunkdepth, 50.f); + } frustumchunkmin = quantizationstep * glm::floor(frustumchunkmin * (1.f / quantizationstep)); frustumchunkmax = quantizationstep * glm::ceil(frustumchunkmax * (1.f / quantizationstep)); + m_shadowdrawnsize = std::max(frustumchunkmax.x - frustumchunkmin.x, frustumchunkmax.y - frustumchunkmin.y); // ...use the dimensions to set up light projection boundaries... camera.projection() = ortho_projection(frustumchunkmin.x, frustumchunkmax.x, frustumchunkmin.y, frustumchunkmax.y, frustumchunkmin.z - 500.f, frustumchunkmax.z + 500.f); frustumtest_proj = ortho_frustumtest_projection(frustumchunkmin.x, frustumchunkmax.x, frustumchunkmin.y, frustumchunkmax.y, frustumchunkmin.z - 500.f, frustumchunkmax.z + 500.f); @@ -1114,6 +1254,11 @@ void vulkan14_renderer::render_viewport(std::size_t const Index) scene_ubs.contact_shadow_lamps = glm::vec4{0.f}; scene_ubs.shadow_fit = glm::vec4{0.f}; + if (vp.main) + { + // the shadow maps of the lamps made since the last frame, before the passes read them + ready_lamp_maps(); + } if (Global.gfx_shadowmap_enabled && vp.main && scene.shadow_map) { Timer::subsystem.gfx_shadows.start(); @@ -1130,8 +1275,23 @@ void vulkan14_renderer::render_viewport(std::size_t const Index) { m_renderpass = m_colorpass; } + // 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 && (false == scene.sky.ggx_ready)) + { + prefilter_environment(); + } Timer::subsystem.gfx_reflections.stop(); } + // rough reflections read the prefiltered map with the environment map of the main view; the other views keep to the mipmaps + m_envmap_bound = (vp.main && Global.gfx_envmap_enabled && scene.env_map && scene.sky.ggx_ready); + if (vp.main) + { + // the shadow maps of the cab lamp, the scenery lamps and the headlights + render_local_shadows(); + render_lamp_shadows(); + render_headlight_shadows(); + ready_lamp_maps(); + } // the motion blur only when something in the view moves (see opengl46_renderer::Render_pass()) auto const motionblur{targets.motion && (m_vehiclesmoved || ((Global.pCamera.m_owner != nullptr) && (Global.pCamera.m_owner->get_future_movement() != glm::dvec3{0.0})))}; @@ -1156,6 +1316,26 @@ void vulkan14_renderer::render_viewport(std::size_t const Index) } auto const clustered{clusters && Global.gfx_lights_clustered && (scene.clusters.params.valid != 0)}; update_lights(simulation::Lights, true, clustered); + ctx.passes.end_pass(); + + // the clouds of the view and the fog volume, which take the lights of the clusters and the shadow map of the pass + { + auto const fogtarget{glm::ivec2{vp.width, vp.height}}; + scene_ubs.projection = OpenGLMatrices.data(GL_PROJECTION); + scene_ubs.inv_view = glm::inverse(glm::mat4{glm::mat3{m_colorpass.pass_camera.modelview()}}); + setup_shadow_matrices(); + upload_scene_block(); + if (clustered) + { + m_draw->cluster_offset = static_cast(scene.clusters.params_offset); + } + if (vp.main) + { + render_weather_view(OpenGLMatrices.data(GL_PROJECTION), glm::mat4{m_colorpass.pass_camera.modelview()}, m_colorpass.pass_camera.position(), fogtarget, (m_cablamplit && clustered) ? 1 : -1); + } + m_weatherstate.setup_pass(vp.main, fogtarget); + upload_weather_block(); + } // the colour of the scene: drawn to the images with the samples, resolved to the single ones auto const uses{colour_target_uses(targets)}; @@ -1179,6 +1359,18 @@ void vulkan14_renderer::render_viewport(std::size_t const Index) setup_shadow_matrices(); upload_scene_block(); + // the depth of the whole opaque scene before its colour, which the contact shadows need + m_indirectcolourpending = false; + auto const contactshadows{contact_shadows_wanted(vp)}; + if ((Global.gfx_depthprepass_scene || contactshadows) && vp.main && (false == Global.bWireFrame)) + { + render_scene_depth(Index, future); + if (contactshadows) + { + setup_contact_shadows(Index); + } + } + // opaque parts... ctx.passes.begin_pass("colour, opaque"); walk_blending(false); @@ -1197,12 +1389,23 @@ void vulkan14_renderer::render_viewport(std::size_t const Index) setup_shadow_color(m_shadowcolor); model_ubs.future = future; Render(simulation::Region); + m_indirectcolourpending = false; + + // the grass of the terrain, opaque, over the ground drawn + if (vp.main && (false == Global.bWireFrame)) + { + ctx.passes.begin_pass("grass"); + render_grass(future); + } // ...translucent parts ctx.passes.begin_pass("colour, translucent and cab"); walk_blending(true); Render_Alpha(simulation::Region); render_editor_overlay(); + // the depth drawn so far, which the particles of the gpu fade in front of + m_particlesoft = (Global.gfx_particles_soft > 0.f) && ((Global.Smoke && Global.gfx_smoke_gpu && (false == simulation::Particles.sequence().empty())) || (Global.gfx_precipitation_gpu && (Global.Overcast > 1.f))) && + setup_particles_depth(Index); // particles render_particles(); // precipitation; done at end, only before cab render @@ -1233,14 +1436,177 @@ void vulkan14_renderer::render_viewport(std::size_t const Index) Render_Alpha(vehicle); } } - vkCmdEndRendering(commands); + commands->end_rendering(); end_draws(); + if (m_contactshadows) + { + // the depth they read is of this view only + m_contactshadows = false; + scene_ubs.contact_shadow_params = glm::vec4{0.f}; + scene_ubs.contact_shadow_lamps = glm::vec4{0.f}; + } // the images resolved and drawn are read by the post-processing m_colorpass.draw_stats = m_renderpass.draw_stats; + // the depth of the finished view, which the cascades of the sun shadows of the next frames are fitted to (gfx.shadow.sdsm) + measure_shadow_fit(Index); render_post(Index, motionblur); } +// the depth of the whole opaque scene of the colour pass, before its colour (opengl46_renderer::Render_Scene_Depth() without the +// record of the walk, gfx.depthprepass.replay no): the same walk as the colour pass makes (the cab drawn early, then the region) +// with the programs of the depth prepass, the depth a little farther than the surface, so the colour pass which tests and writes +// its depth as usual passes where it's the same surface. the rendering of the colour is ended for it and begun again after it; +// the depth is resolved or copied for the contact shadows +void vulkan14_renderer::render_scene_depth(std::size_t const Index, glm::mat4 const &Future) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &targets{scene.targets[Index]}; + auto &draw{*m_draw}; + auto const commands{ctx.commands}; + auto const msaa{targets.samples != VK_SAMPLE_COUNT_1_BIT}; + auto const contact{contact_shadows_wanted(m_viewports[Index])}; + commands->end_rendering(); + // what the draws go on with + auto const set{draw.set}; + auto const offsets{std::array{draw.scene_offset, draw.light_offset, draw.cluster_offset, draw.weather_offset}}; + end_draws(); + + if (contact && ((false == bool(targets.scene_depth)) || (targets.scene_depth->desc().extent.width != targets.extent.width) || (targets.scene_depth->desc().extent.height != targets.extent.height))) + { + // the depth is resolved apart from the one the colour pass draws to, which it tests and writes + vk14::image_desc desc; + desc.format = VK_FORMAT_D32_SFLOAT; + desc.extent = {targets.extent.width, targets.extent.height, 1}; + desc.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (targets.scene_depth) + { + ctx.deletions.push(ctx.uploads.current_value(), [old = std::move(targets.scene_depth)]() {}); + } + targets.scene_depth = std::make_shared(); + if (false == targets.scene_depth->create(ctx.memory, desc, "scene depth")) + { + targets.scene_depth.reset(); + } + } + auto &depthimage{msaa ? *targets.msaa_depth : *targets.depth}; + std::vector uses{vk14::use_of(depthimage, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)}; + // the first sample resolved to the depth of one sample, as the colour pass resolves it: the contact shadows copy it, the depth + // pyramid of the gpu culling is made from it + if (msaa) + { + uses.push_back(vk14::use_of(*targets.depth, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, resolve_depth_stage, resolve_depth_access)); + } + ctx.passes.begin_pass("scene depth", uses); + VkClearValue clear{}; + auto depth{attachment_info(depthimage.view(), VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, false, clear)}; + if (msaa) + { + depth.resolveMode = VK_RESOLVE_MODE_SAMPLE_ZERO_BIT; + depth.resolveImageView = targets.depth->view(); + depth.resolveImageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; + } + VkRenderingInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + rendering.renderArea = {{0, 0}, targets.extent}; + rendering.layerCount = 1; + rendering.pDepthAttachment = &depth; + commands->begin_rendering(&rendering); + remember_rendering(rendering, uses); + start_draws(targets.depth_formats, static_cast(targets.extent.width), static_cast(targets.extent.height), set, false); + draw.scene_offset = offsets[0]; + draw.light_offset = offsets[1]; + draw.cluster_offset = offsets[2]; + draw.weather_offset = offsets[3]; + draw.depthpass = true; + draw.depthskip = true; + commands->set_depth_bias_enable(VK_TRUE); + commands->set_depth_bias(-1.f, 0.f, -1.f); + auto const colourstats{m_renderpass.draw_stats}; + m_renderpass.draw_stats = {}; + walk_blending(false); + // the conditions of the cab drawn early by the colour pass + if ((false == FreeFlyModeFlag) && (Global.Overcast <= 1.0f) && Global.render_cab && simulation::Train) + { + model_ubs.future = glm::mat4(); + auto const *vehicle{simulation::Train->Dynamic()}; + Render_cab(vehicle, vehicle->InteriorLightLevel, false); + } + model_ubs.future = Future; + Render(simulation::Region); + m_renderpass.draw_stats = colourstats; + commands->set_depth_bias_enable(VK_FALSE); + draw.depthpass = false; + draw.depthskip = true; + commands->end_rendering(); + end_draws(); + if (contact && targets.scene_depth) + { + // the depth of one sample (resolved with more of them) copied apart from the one the colour pass goes on with + vk14::barriers(commands, {vk14::use_of(*targets.depth, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT), + vk14::use_of(*targets.scene_depth, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT)}); + VkImageCopy2 region{}; + region.sType = VK_STRUCTURE_TYPE_IMAGE_COPY_2; + region.srcSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}; + region.dstSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}; + region.extent = {targets.extent.width, targets.extent.height, 1}; + VkCopyImageInfo2 copy{}; + copy.sType = VK_STRUCTURE_TYPE_COPY_IMAGE_INFO_2; + copy.srcImage = targets.depth->handle(); + copy.srcImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + copy.dstImage = targets.scene_depth->handle(); + copy.dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + copy.regionCount = 1; + copy.pRegions = ®ion; + commands->copy_image2(©); + vk14::barriers(commands, {vk14::use_of(*targets.scene_depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); + } + // the colour pass goes on where the sky left it + vk14::barriers(commands, colour_target_uses(targets)); + begin_colour_rendering(Index, false); + start_draws(targets.formats, static_cast(targets.extent.width), static_cast(targets.extent.height), set, false); + draw.scene_offset = offsets[0]; + draw.light_offset = offsets[1]; + draw.cluster_offset = offsets[2]; + draw.weather_offset = offsets[3]; +} + +// the contact shadows read the depth of the scene drawn before the colour, which the main view has, and work out the distance +// of a surface from its depth the way a perspective projection with the far plane at infinity gives it (depth = near / w): not +// in the orthographic view of the editor (opengl46_renderer::Contact_Shadows_Wanted()) +bool vulkan14_renderer::contact_shadows_wanted(viewport_config const &Viewport) const +{ + if ((Global.gfx_contactshadows <= 0) || (false == Viewport.main) || Global.bWireFrame || Global.gfx_skippipeline) + { + return false; + } + auto const &projection{scene_ubs.projection}; + return (projection[2][3] != 0.f) && (projection[2][2] == 0.f) && (projection[3][2] > 0.f); +} + +void vulkan14_renderer::setup_contact_shadows(std::size_t const Index) +{ + auto &targets{m_context->scene.targets[Index]}; + if (false == bool(targets.scene_depth)) + { + return; + } + // steps of the ray towards the sun, of the rays towards lamps and headlights, and how many of those a pixel traces + struct contact_level + { + float sunsteps; + float lampsteps; + float lamps; + }; + static contact_level const levels[]{{8.f, 0.f, 0.f}, {12.f, 8.f, 2.f}, {16.f, 12.f, 4.f}}; + auto const &level{levels[std::clamp(Global.gfx_contactshadows, 1, 3) - 1]}; + scene_ubs.contact_shadow_params = glm::vec4{Global.gfx_contactshadows_length, Global.gfx_contactshadows_range, level.sunsteps, level.lampsteps}; + scene_ubs.contact_shadow_lamps = glm::vec4{level.lamps, 0.f, 0.f, 0.f}; + upload_scene_block(); + m_contactshadows = true; +} + // the rendering of the colour of a viewport: begun with the images cleared, or again after the depth under the cursor was read void vulkan14_renderer::begin_colour_rendering(std::size_t const Index, bool const Clear) { @@ -1284,7 +1650,8 @@ void vulkan14_renderer::begin_colour_rendering(std::size_t const Index, bool con rendering.colorAttachmentCount = targets.formats.colours; rendering.pColorAttachments = colours.data(); rendering.pDepthAttachment = &depth; - vkCmdBeginRendering(ctx.commands, &rendering); + ctx.commands->begin_rendering(&rendering); + remember_rendering(rendering, colour_target_uses(targets)); } // the depth of the ground under the cursor of the editor: the rendering is ended (which resolves the depth), the depth under the @@ -1306,7 +1673,7 @@ void vulkan14_renderer::walk_editor_cursor() auto bufferpos{glm::ivec2(mousepos * glm::vec2(targets.extent.width, targets.extent.height) / glm::max(glm::vec2(Global.window_size), glm::vec2(1.f)))}; bufferpos = glm::clamp(bufferpos, glm::ivec2(0, 0), glm::ivec2(targets.extent.width - 1, targets.extent.height - 1)); - vkCmdEndRendering(commands); + commands->end_rendering(); auto readback{std::make_shared()}; if (readback->create(ctx.memory, sizeof(float), VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::readback, "cursor depth")) { @@ -1323,7 +1690,7 @@ void vulkan14_renderer::walk_editor_cursor() copy.dstBuffer = readback->handle(); copy.regionCount = 1; copy.pRegions = ®ion; - vkCmdCopyImageToBuffer2(commands, ©); + commands->copy_image_to_buffer2(©); host_read_barrier(commands); scene.cursor.data = readback; scene.cursor.value = ctx.timelinevalue + 1; @@ -1358,22 +1725,23 @@ void vulkan14_renderer::render_shadows(viewport_config &Viewport) auto &scene{ctx.scene}; auto const commands{ctx.commands}; setup_pass(Viewport, m_renderpass, rendermode::shadows); - static_assert(gl::MAX_CASCADES == 3, "the fixed cascades are made for 3 of them"); - std::array csmstageboundaries{0.0f, Global.shadowtune.range / 32, Global.shadowtune.range / 8, Global.shadowtune.range}; - if (Global.shadowtune.map_size > 2048) - { - // increase coverage if our shadow map is large enough to produce decent results - for (std::size_t idx = 1; idx < csmstageboundaries.size(); ++idx) - { - csmstageboundaries[idx] *= Global.shadowtune.map_size / 2048; - } - } + auto csmstageboundaries{gfx::shadow_fit::fixed_splits()}; + // the cascades fitted to the depth of the view (gfx.shadow.sdsm) go between the distances of its pixels, each map covering the + // area of the pixels its cascade takes + gfx::shadow_fit::areas fitareas; + auto const fitted{shadow_fit(Viewport, csmstageboundaries, fitareas)}; + scene_ubs.shadow_fit = glm::vec4{fitted ? 1.f : 0.f, 0.f, 0.f, 0.f}; + m_shadowfitstate.splits = csmstageboundaries; static char const *const parts[] = {"sun shadows 1", "sun shadows 2", "sun shadows 3"}; for (std::size_t idx = 0; idx < m_shadowpass.size(); ++idx) { ctx.passes.begin_pass(parts[idx], {vk14::use_of(*scene.shadow_map, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)}); + m_shadowfitarea = (fitareas[idx] ? &(*fitareas[idx]) : nullptr); setup_pass(Viewport, m_renderpass, rendermode::shadows, csmstageboundaries[idx], csmstageboundaries[idx + 1]); + m_shadowfitarea = nullptr; m_shadowpass[idx] = m_renderpass; + m_shadowfitstate.slice[idx] = m_shadowslicesize; + m_shadowfitstate.drawn[idx] = m_shadowdrawnsize; setup_matrices(); scene_ubs.projection = OpenGLMatrices.data(GL_PROJECTION); auto const csmstagezfar{(csmstageboundaries[idx + 1]) + (m_shadowpass.size() - idx)}; @@ -1387,8 +1755,10 @@ void vulkan14_renderer::render_shadows(viewport_config &Viewport) rendering.renderArea = {{0, 0}, {scene.shadow_size, scene.shadow_size}}; rendering.layerCount = 1; rendering.pDepthAttachment = &depth; - vkCmdBeginRendering(commands, &rendering); + commands->begin_rendering(&rendering); + remember_rendering(rendering, {vk14::use_of(*scene.shadow_map, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)}); start_draws(scene.shadow_formats, static_cast(scene.shadow_size), static_cast(scene.shadow_size), vk14::scene_state::set_plain, true); + m_shadowcascade = static_cast(idx); walk_blending(false); upload_scene_block(); if (m_shadowcolor != colors::white) @@ -1401,7 +1771,8 @@ void vulkan14_renderer::render_shadows(viewport_config &Viewport) Render_cab(simulation::Train->Dynamic(), 0.0f, true); } } - vkCmdEndRendering(commands); + m_shadowcascade = -1; + commands->end_rendering(); end_draws(); ctx.passes.end_pass(); } @@ -1464,7 +1835,7 @@ bool vulkan14_renderer::render_reflections(viewport_config &Viewport) dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.imageMemoryBarrierCount = 1; dependency.pImageMemoryBarriers = &entry; - vkCmdPipelineBarrier2(commands, &dependency); + commands->pipeline_barrier2(&dependency); }; // the first level was drawn, the others are undefined barrier(0, 1, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, colour_stage, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); @@ -1489,12 +1860,18 @@ bool vulkan14_renderer::render_reflections(viewport_config &Viewport) info.regionCount = 1; info.pRegions = &blit; info.filter = VK_FILTER_LINEAR; - vkCmdBlitImage2(commands, &info); + commands->blit_image2(&info); barrier(level, 1, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); } barrier(0, mips, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT, sampled_stage, sampled_access); image.state = vk14::image_state{VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access}; m_environmentready = true; + ctx.passes.end_pass(); + if (Global.gfx_envmap_ggx) + { + prefilter_environment(); + } + return true; } else { @@ -1524,12 +1901,20 @@ void vulkan14_renderer::render_reflection_face(viewport_config &Viewport) rendering.colorAttachmentCount = 1; rendering.pColorAttachments = &colour; rendering.pDepthAttachment = &depth; - vkCmdBeginRendering(commands, &rendering); + commands->begin_rendering(&rendering); + remember_rendering(rendering, {vk14::use_of(*scene.env_map, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, colour_stage, colour_access), + vk14::use_of(*scene.env_depth, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)}); start_draws(scene.reflection_formats, static_cast(size), static_cast(size), vk14::scene_state::set_reflections, false); setup_matrices(); + // the fog volume is the one of the main view + m_weatherstate.setup_pass(false, glm::ivec2{static_cast(gl::ENVMAP_SIZE)}); + upload_weather_block(); // the light buffer holds what the last color pass left in it; the lights go in anew, without the headlights + auto const envmapbound{m_envmap_bound}; + m_envmap_bound = false; update_lights(simulation::Lights, false); + m_envmap_bound = envmapbound; upload_light_header(); // environment... walk_blending(true); @@ -1543,12 +1928,12 @@ void vulkan14_renderer::render_reflection_face(viewport_config &Viewport) upload_scene_block(); Render(simulation::Region); - vkCmdEndRendering(commands); + commands->end_rendering(); end_draws(); } -// the sky dome, the sun and the moon (opengl46_renderer::Render(world_environment *) without the atmosphere, the stars and the -// weather of the later stages of the plan) +// the sky dome (with the atmosphere), the stars, the sun and the moon, the clouds and the fog over the sky +// (opengl46_renderer::Render(world_environment *)) void vulkan14_renderer::render_environment(world_environment *Environment) { auto &ctx{*m_context}; @@ -1573,13 +1958,19 @@ void vulkan14_renderer::render_environment(world_environment *Environment) ::glPushMatrix(); ::glScalef(500.0f, 500.0f, 500.0f); model_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW)); + auto const atmosphere{m_skylight.atmosphere_weight() > 0.f}; + if (atmosphere) + { + m_skylight.sky_params(model_ubs.param[0], model_ubs.param[1]); + } + auto &domeprogram{atmosphere ? *scene.sky.atmosphere_dome : *scene.skydome}; auto const &vertices{skydome.vertices()}; auto const &colours{skydome.colors()}; auto const &indices{skydome.indices()}; auto const vertexslice{ctx.ring.allocate(vertices.size() * sizeof(glm::vec3), 16)}; auto const colourslice{ctx.ring.allocate(colours.size() * sizeof(glm::vec3), 16)}; auto const indexslice{ctx.ring.allocate(indices.size() * sizeof(std::uint16_t), 16)}; - if (vertexslice && colourslice && indexslice && bind_scene_pipeline(*scene.skydome, *draw.targets, draw.blending ? vk14::blend_mode::scene_alpha : vk14::blend_mode::none, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)) + if (vertexslice && colourslice && indexslice && bind_scene_pipeline(domeprogram, *draw.targets, draw.blending ? vk14::blend_mode::scene_alpha : vk14::blend_mode::none, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)) { std::memcpy(vertexslice.data, vertices.data(), vertexslice.size); std::memcpy(colourslice.data, colours.data(), colourslice.size); @@ -1601,9 +1992,9 @@ void vulkan14_renderer::render_environment(world_environment *Environment) push_constants(&constants); VkBuffer const buffers[2]{vertexslice.buffer, colourslice.buffer}; VkDeviceSize const offsets[2]{vertexslice.offset, colourslice.offset}; - vkCmdBindVertexBuffers(draw.commands, 0, 2, buffers, offsets); - vkCmdBindIndexBuffer(draw.commands, indexslice.buffer, indexslice.offset, VK_INDEX_TYPE_UINT16); - vkCmdDrawIndexed(draw.commands, static_cast(indices.size()), 1, 0, 0, 0); + draw.commands->bind_vertex_buffers(0, 2, buffers, offsets); + draw.commands->bind_index_buffer(indexslice.buffer, indexslice.offset, VK_INDEX_TYPE_UINT16); + draw.commands->draw_indexed(static_cast(indices.size()), 1, 0, 0, 0); draw.vertices = VK_NULL_HANDLE; draw.userdata = VK_NULL_HANDLE; draw.indices = VK_NULL_HANDLE; @@ -1613,13 +2004,16 @@ void vulkan14_renderer::render_environment(world_environment *Environment) } auto const &modelview = OpenGLMatrices.data(GL_MODELVIEW); + // stars of the catalogue, turned by the sidereal time; the fog and the clouds go over them + render_stars(modelview); // the bodies give their colour premultiplied, and as alpha how much of the sky behind them they hide (celestial.frag) auto const &sun{Environment->sun()}; auto const &moon{Environment->moon()}; - // without the clouds the overcast fades the sun and the moon - auto const overcastfactor{std::clamp(1.5f - Global.Overcast, 0.f, 1.f)}; + // the fog and the clouds laid over the sky hide the sun and the moon; without the clouds the overcast fades them + auto const overcastfactor{m_weatherstate.clouds_drawn() ? 1.f : std::clamp(1.5f - Global.Overcast, 0.f, 1.f)}; float const duskfactor = 1.0f - std::clamp(std::abs(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_skylight.sun_disc_color(suncolor); glm::vec2 zenith{glm::mat3{modelview} * glm::vec3{0.f, 1.f, 0.f}}; zenith = (glm::length(zenith) > 1e-3f ? glm::normalize(zenith) : glm::vec2{0.f, 1.f}); auto const flattening = [](float const Elevation, float const Radius) { @@ -1657,7 +2051,7 @@ void vulkan14_renderer::render_environment(world_environment *Environment) constants.data[i] = Body[i]; } push_constants(&constants); - vkCmdDraw(draw.commands, 4, 1, 0, 0); + draw.commands->draw(4, 1, 0, 0); }; std::array body{}; // sun @@ -1672,7 +2066,7 @@ void vulkan14_renderer::render_environment(world_environment *Environment) if (mainview) { auto const solidangle{2.f * glm::pi() * (1.f - std::cos(sunradius))}; - auto const hidden{glm::smoothstep(0.5f, 1.f, Global.Overcast)}; + auto const hidden{m_weatherstate.clouds_drawn() ? 0.f : glm::smoothstep(0.5f, 1.f, Global.Overcast)}; auto const physical{glm::vec3{m_sunlight.diffuse} * (1.5f * glm::pi() / solidangle * Global.gfx_sun_disc)}; radiance = glm::mix(physical, radiance, hidden); auto const peak{std::max({radiance.r, radiance.g, radiance.b}) * limbpeak}; @@ -1729,8 +2123,12 @@ void vulkan14_renderer::render_environment(world_environment *Environment) } draw_body(body); } + // clouds of the weather and the fog over the sky: of the main view in its colour pass, of the panorama around the camera in the + // other views and in the faces of the environment map + render_sky_weather(OpenGLMatrices.data(GL_PROJECTION), mainview); ::glPopMatrix(); model_ubs.fog_density = fogdensity; + m_sunlight.factor = 1.f; // calculate shadow tone, based on positions of celestial bodies m_shadowcolor = glm::mix(glm::vec4{colors::shadow}, glm::vec4{colors::white}, std::clamp(-sun.getAngle(), 0.f, 6.f) / 6.f); @@ -1740,8 +2138,11 @@ void vulkan14_renderer::render_environment(world_environment *Environment) auto const moonshadow{std::clamp(4.f * moon.getBrightness() * Global.gfx_moonlight, 0.f, 1.f) * std::clamp((-18.f - sun.getAngle()) / 2.f, 0.f, 1.f) * std::clamp(moon.getAngle() / 5.f, 0.f, 1.f)}; m_shadowcolor = glm::mix(glm::vec4{colors::white}, glm::vec4{colors::shadow}, moonshadow); } - // soften shadows depending on sky overcast factor (the shadows of the clouds come with the stage P2) - m_shadowcolor = glm::min(colors::white, m_shadowcolor + ((colors::white - colors::shadow) * Global.Overcast)); + // soften shadows depending on sky overcast factor, unless the shadows of the clouds hold back the sun where they fall + if (false == m_weatherstate.cloud_shadows()) + { + m_shadowcolor = glm::min(colors::white, m_shadowcolor + ((colors::white - colors::shadow) * Global.Overcast)); + } setup_shadow_color(m_shadowcolor); } @@ -1755,10 +2156,186 @@ void vulkan14_renderer::push_constants(void const *Constants) info.offset = 0; info.size = sizeof(vk14::draw_constants); info.pValues = Constants; - vkCmdPushConstants2(draw.commands, &info); + draw.commands->push_constants2(&info); draw.pushed_draw = ~0u; } +// the grass of the heightmap terrain (opengl46_renderer::Render_grass(), opengl46_grass::render()): a draw of the instances of a +// blade for each tile seen, the textures of its chunk pushed in place of those of a material +void vulkan14_renderer::render_grass(glm::mat4 const &Future) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &draw{*m_draw}; + auto &grass{scene.grass}; + if ((false == Global.gfx_grass) || grass.empty()) + { + return; + } + auto const &camera{m_renderpass.pass_camera}; + if (false == grass.prepare(camera.position(), [&](scene::bounding_area const &Area) { return camera.visible(Area); })) + { + return; + } + if ((scene.grass_program == nullptr) && (false == scene.grass_tried)) + { + scene.grass_tried = true; + try + { + auto program{std::make_shared()}; + program->vertex = ctx.shaders.get("grass.vert"); + program->fragment = ctx.shaders.get("grass.frag"); + program->fragment_name = "grass.frag"; + program->vertex_name = "grass.vert"; + program->vertices = vk14::vertex_kind::none; + program->name = "grass.vert + grass.frag"; + scene.grass_program = program; + } + catch (gfx::shader_exception const &e) + { + ErrorLog("heightmap terrain: can't use the grass shader: " + std::string(e.what())); + } + } + if ((scene.grass_program == nullptr) || grass.chunk_draws().empty()) + { + return; + } + // the blades bring their colour, the lights do the rest + model_ubs.future = Future; + model_ubs.param[0] = glm::vec4(1.f); + model_ubs.param[1] = glm::vec4(1.f, 1.f, 0.f, 1.f); // diffuse, specular, reflection, glossiness + model_ubs.param[2] = glm::vec4(0.f); + model_ubs.emission = 0.f; + model_ubs.alpha_mult = 1.f; + model_ubs.opacity = 0.5f; + if (false == bind_scene_pipeline(*scene.grass_program, *draw.targets, vk14::blend_mode::none, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP)) + { + return; + } + auto const cullface{draw.cullface}; + draw.cullface = false; + apply_draw_state(); + if (draw.set_dirty) + { + bind_pass_set(); + } + glm::mat4 const view{OpenGLMatrices.data(GL_MODELVIEW)}; + auto const &frame{grass.frame()}; + auto const &tiles{grass.tile_draws()}; + for (auto const &chunk : grass.chunk_draws()) + { + auto const *images{grass.images(chunk)}; + if (images == nullptr) + { + continue; + } + // the chunk is drawn relative to its corner, the heights as they are + model_ubs.set_modelview(glm::translate(view, glm::vec3{chunk.offset})); + auto const block{push_draw_block()}; + if (block == ~0u) + { + break; + } + std::array descriptors; + for (std::size_t unit = 0; unit < gl::MAX_TEXTURES; ++unit) + { + descriptors[unit] = (unit < images->images.size()) ? VkDescriptorImageInfo{images->samplers[unit], images->images[unit]->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL} + : VkDescriptorImageInfo{ctx.sampling.repeat_aniso, scene.white->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + } + scene.material_set.push(draw.commands, scene.scene_layout, descriptors.data()); + vk14::draw_constants constants; + constants.draw = block; + constants.data[2] = chunk.camera; + constants.data[3] = frame.lod; + constants.data[4] = frame.wind; + constants.data[5] = frame.season; + for (auto i = chunk.first; i < chunk.first + chunk.count; ++i) + { + auto const &tile{tiles[i]}; + constants.data[0] = tile.tile; + constants.data[1] = tile.part; + push_constants(&constants); + draw.commands->draw(static_cast(tile.segments * 2 + 1), static_cast(tile.count), 0, 0); + ++draw.draws; + } + } + m_renderpass.draw_stats.triangles += static_cast(grass.triangles()); + // the textures of the next material go in place of the ones of the grass + draw.textures_dirty = true; + draw.cullface = cullface; + apply_draw_state(); +} + +// what the particles of the gpu are drawn with in the colour pass under way (opengl46_renderer::Particles_View()) +gfx::gpu_particles::view vulkan14_renderer::particles_view() const +{ + gfx::gpu_particles::view view; + view.modelview = OpenGLMatrices.data(GL_MODELVIEW); + view.projection = OpenGLMatrices.data(GL_PROJECTION); + view.camera = m_renderpass.pass_camera.position(); + view.height = static_cast(m_draw->extent.height); + view.soft = m_particlesoft; + return view; +} + +// the depth of the scene drawn so far copied for the particles of the gpu, which fade in front of it (opengl46_renderer:: +// Setup_Particles_Depth()): the rendering is ended (which resolves the depth of many samples), the depth copied to the depth of +// the scene the pass sets read (the one of the contact shadows, as in the opengl renderer), and the rendering begun again +bool vulkan14_renderer::setup_particles_depth(std::size_t const Index) +{ + // the fading works out the distance from the depth as the contact shadows do (contact_shadows_wanted()) + auto &ctx{*m_context}; + auto &targets{ctx.scene.targets[Index]}; + auto &draw{*m_draw}; + if (Global.gfx_skippipeline || Global.bWireFrame || (false == draw.rendering) || (false == draw.resume.valid)) + { + return false; + } + auto const &projection{scene_ubs.projection}; + if ((projection[2][3] == 0.f) || (projection[2][2] != 0.f) || (projection[3][2] <= 0.f)) + { + return false; + } + if ((false == bool(targets.scene_depth)) || (targets.scene_depth->desc().extent.width != targets.extent.width) || (targets.scene_depth->desc().extent.height != targets.extent.height)) + { + vk14::image_desc desc; + desc.format = VK_FORMAT_D32_SFLOAT; + desc.extent = {targets.extent.width, targets.extent.height, 1}; + desc.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (targets.scene_depth) + { + ctx.deletions.push(ctx.uploads.current_value(), [old = std::move(targets.scene_depth)]() {}); + } + targets.scene_depth = std::make_shared(); + if (false == targets.scene_depth->create(ctx.memory, desc, "scene depth")) + { + targets.scene_depth.reset(); + return false; + } + } + auto const commands{ctx.commands}; + suspend_rendering(); + vk14::barriers(commands, {vk14::use_of(*targets.depth, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT), + vk14::use_of(*targets.scene_depth, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT)}); + VkImageCopy2 region{}; + region.sType = VK_STRUCTURE_TYPE_IMAGE_COPY_2; + region.srcSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}; + region.dstSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}; + region.extent = {targets.extent.width, targets.extent.height, 1}; + VkCopyImageInfo2 copy{}; + copy.sType = VK_STRUCTURE_TYPE_COPY_IMAGE_INFO_2; + copy.srcImage = targets.depth->handle(); + copy.srcImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + copy.dstImage = targets.scene_depth->handle(); + copy.dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + copy.regionCount = 1; + copy.pRegions = ®ion; + commands->copy_image2(©); + vk14::barriers(commands, {vk14::use_of(*targets.scene_depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); + resume_rendering(); + return true; +} + // the smoke of the cpu (opengl46_particles) void vulkan14_renderer::render_particles() { @@ -1767,8 +2344,21 @@ void vulkan14_renderer::render_particles() auto &draw{*m_draw}; model_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW)); walk_texture(0, m_smoketexture); - // the particles of the gpu come with the stage P6 - simulation::Particles.gpu(false); + // the smoke of the gpu, when its programs could be made; otherwise the particle manager simulates it on the cpu. a switch + // takes effect from the next update of the manager + auto const gpu{Global.gfx_smoke_gpu && scene.particles.smoke_available()}; + simulation::Particles.gpu(gpu); + if (gpu) + { + if (Global.Smoke) + { + scene.particles.simulate_smoke(simulation::Particles, m_renderpass.pass_camera.position()); + m_renderpass.draw_stats.particles = static_cast(scene.particles.render_smoke(particles_view())); + // the compute passes ended the rendering, which a draw may not have begun again + resume_rendering(); + } + return; + } if (false == Global.Smoke) { return; @@ -1834,8 +2424,8 @@ void vulkan14_renderer::render_particles() constants.draw = draw.block; push_constants(&constants); VkDeviceSize const offset{slice.offset}; - vkCmdBindVertexBuffers(draw.commands, 0, 1, &slice.buffer, &offset); - vkCmdDraw(draw.commands, static_cast(vertices.size()), 1, 0, 0); + draw.commands->bind_vertex_buffers(0, 1, &slice.buffer, &offset); + draw.commands->draw(static_cast(vertices.size()), 1, 0, 0); draw.vertices = VK_NULL_HANDLE; draw.userdata = VK_NULL_HANDLE; } @@ -1850,6 +2440,14 @@ void vulkan14_renderer::render_precipitation() auto &ctx{*m_context}; auto &scene{ctx.scene}; auto &draw{*m_draw}; + if (Global.gfx_precipitation_gpu && scene.particles.precipitation_available()) + { + // the particles of the gpu: the overcast above 1 is how heavy the rain or snow is + model_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW)); + scene.particles.render_precipitation(particles_view(), Global.Weather == "snow:", std::clamp(Global.Overcast - 1.f, 0.f, 1.f), m_framestamp); + resume_rendering(); + return; + } if (m_precipitationovercast != Global.Overcast) { m_precipitationovercast = Global.Overcast; @@ -1920,9 +2518,9 @@ void vulkan14_renderer::render_precipitation() push_constants(&constants); VkBuffer const buffers[2]{vertexslice.buffer, uvslice.buffer}; VkDeviceSize const offsets[2]{vertexslice.offset, uvslice.offset}; - vkCmdBindVertexBuffers(draw.commands, 0, 2, buffers, offsets); - vkCmdBindIndexBuffer(draw.commands, indexslice.buffer, indexslice.offset, VK_INDEX_TYPE_UINT16); - vkCmdDrawIndexed(draw.commands, static_cast(mesh.indices.size()), 1, 0, 0, 0); + draw.commands->bind_vertex_buffers(0, 2, buffers, offsets); + draw.commands->bind_index_buffer(indexslice.buffer, indexslice.offset, VK_INDEX_TYPE_UINT16); + draw.commands->draw_indexed(static_cast(mesh.indices.size()), 1, 0, 0, 0); draw.vertices = VK_NULL_HANDLE; draw.userdata = VK_NULL_HANDLE; draw.indices = VK_NULL_HANDLE; @@ -1993,87 +2591,120 @@ void vulkan14_renderer::render_editor_overlay() ::glPopMatrix(); } -// the post-processing of a viewport: from the colour of the scene to its output, which the window shows +// a pass of the post-processing: the fragment shader over the whole of the target, reading the images and the exposure +void vulkan14_renderer::post_pass(char const *Fragment, vk14::image &Target, std::vector const &Inputs, vk14::draw_constants const &Constants, char const *Pass, unsigned const Repeated) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto const commands{ctx.commands}; + VkExtent2D const extent{Target.desc().extent.width, Target.desc().extent.height}; + std::vector uses{vk14::use_of(Target, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, colour_stage, colour_access)}; + for (auto *input : Inputs) + { + uses.push_back(vk14::use_of(*input, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, sampled_access)); + } + if (Pass != nullptr) + { + ctx.passes.begin_pass(Pass, uses); + } + else + { + vk14::barriers(commands, uses); + } + auto const pipeline{post_pipeline(Fragment, static_cast(Target.desc().format))}; + if (pipeline == nullptr) + { + return; + } + VkClearValue clear{}; + auto attachment{attachment_info(Target.view(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, false, clear)}; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + VkRenderingInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + rendering.renderArea = {{0, 0}, extent}; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &attachment; + commands->begin_rendering(&rendering); + vk14::set_draw_state(commands, extent); + commands->bind_pipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, reinterpret_cast(pipeline)); + commands->set_primitive_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP); + std::array descriptors; + for (std::size_t i = 0; i < vk14::bindings::POST_TEXTURES; ++i) + { + auto *input{i < Inputs.size() ? Inputs[i] : Inputs.front()}; + // a depth as the opengl renderer reads its depth targets (filtered where the device can, the farthest depth past the edges) + auto const depth{input->desc().format == VK_FORMAT_D32_SFLOAT}; + auto const repeated{(i < Inputs.size()) && ((Repeated & (1u << i)) != 0)}; + descriptors[i] = vk14::image_descriptor(repeated ? ctx.sampling.repeat_nearest : depth ? ctx.sampling.depth : ctx.sampling.clamp_linear, input->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + } + descriptors[vk14::bindings::POST_EXPOSURE_SSBO] = vk14::buffer_descriptor(scene.exposure.data.handle(), 0, sizeof(gl::exposure_data)); + scene.post_set.push(commands, scene.post_layout, descriptors.data()); + VkPushConstantsInfo info{}; + info.sType = VK_STRUCTURE_TYPE_PUSH_CONSTANTS_INFO; + info.layout = scene.post_layout; + info.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + info.size = sizeof(Constants); + info.pValues = &Constants; + commands->push_constants2(&info); + commands->draw(4, 1, 0, 0); + commands->end_rendering(); +} + +// the post-processing of a viewport: from the colour of the scene to its output, which the window shows (the part of +// opengl46_renderer::Render_pass() after the colour pass) void vulkan14_renderer::render_post(std::size_t const Index, bool const Motionblur) { auto &ctx{*m_context}; auto &scene{ctx.scene}; auto &targets{scene.targets[Index]}; auto const commands{ctx.commands}; - auto const extent{targets.extent}; - - auto const post = [&](char const *Fragment, vk14::image &Target, std::vector const &Inputs, vk14::draw_constants const &Constants, char const *Pass) { - std::vector uses{vk14::use_of(Target, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, colour_stage, colour_access)}; - for (auto *input : Inputs) - { - uses.push_back(vk14::use_of(*input, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, sampled_access)); - } - if (Pass != nullptr) - { - ctx.passes.begin_pass(Pass, uses); - } - else - { - vk14::barriers(commands, uses); - } - auto const pipeline{post_pipeline(Fragment, static_cast(Target.desc().format))}; - if (pipeline == nullptr) - { - return; - } - VkClearValue clear{}; - auto attachment{attachment_info(Target.view(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, false, clear)}; - attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - VkRenderingInfo rendering{}; - rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; - rendering.renderArea = {{0, 0}, extent}; - rendering.layerCount = 1; - rendering.colorAttachmentCount = 1; - rendering.pColorAttachments = &attachment; - vkCmdBeginRendering(commands, &rendering); - vk14::set_draw_state(commands, extent); - vkCmdBindPipeline(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, reinterpret_cast(pipeline)); - vkCmdSetPrimitiveTopology(commands, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP); - std::array images; - for (std::size_t i = 0; i < images.size(); ++i) - { - auto *input{i < Inputs.size() ? Inputs[i] : Inputs.front()}; - // a depth isn't filtered (linear filtering of the depth formats isn't required of the devices) - auto const depth{input->desc().format == VK_FORMAT_D32_SFLOAT}; - images[i] = VkDescriptorImageInfo{depth ? ctx.sampling.clamp_nearest : ctx.sampling.clamp_linear, input->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; - } - scene.post_set.push(commands, scene.post_layout, images.data()); - VkPushConstantsInfo info{}; - info.sType = VK_STRUCTURE_TYPE_PUSH_CONSTANTS_INFO; - info.layout = scene.post_layout; - info.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; - info.size = sizeof(Constants); - info.pValues = &Constants; - vkCmdPushConstants2(commands, &info); - vkCmdDraw(commands, 4, 1, 0, 0); - vkCmdEndRendering(commands); - }; + // the occlusion goes into the image before the blur, which smears both alike. the targets are there only when it was on as they + // were made + auto const aomode{(targets.ssao != nullptr) ? Global.gfx_postfx_ao : 0}; + if (aomode != 0) + { + ctx.passes.begin_pass("ambient occlusion"); + render_ao(Index, aomode); + } auto *hdr{targets.colour.get()}; if (Motionblur && targets.blurred) { vk14::draw_constants constants; constants.data[0].x = m_framerate / (1.0 / Global.gfx_postfx_motionblur_shutter); - post("postfx_motionblur.frag", *targets.blurred, {targets.colour.get(), targets.motion_image.get(), targets.depth.get()}, constants, "resolve, ambient occlusion, motion blur"); + post_pass("postfx_motionblur.frag", *targets.blurred, {targets.colour.get(), targets.motion_image.get(), targets.depth.get()}, constants, "resolve, ambient occlusion, motion blur"); hdr = targets.blurred.get(); } - // the exposure and the bloom come with the stage P1: the fixed exposure + ctx.passes.begin_pass("exposure, bloom"); + // the exposure follows the main view; the other viewports use it too + if (Global.gfx_postfx_autoexposure && m_viewports[Index].main) + { + measure_exposure(*hdr); + } + vk14::image *bloom{nullptr}; + if (Global.gfx_postfx_bloom) + { + bloom = render_bloom(Index, *hdr); + } vk14::draw_constants tonemap; - tonemap.data[0] = glm::vec4{0.f, 0.f, std::exp2(Global.gfx_postfx_autoexposure_compensation), 0.f}; + tonemap.data[0] = gfx::exposure_settings::tonemap_params(bloom != nullptr ? targets.bloom_sizes.size() : 0); + // the dither goes to the last pass before the window: the chromatic aberration, when it follows tonemap.data[1] = glm::vec4{targets.aberration ? 0.f : 1.f, 0.f, 0.f, 0.f}; + std::vector const tonemapinput{hdr, (bloom != nullptr ? bloom : hdr)}; if (targets.aberration && targets.tonemapped) { - post("postfx_tonemapping.frag", *targets.tonemapped, {hdr, hdr}, tonemap, "tonemapping, output"); - post("postfx_chromaticaberration.frag", *targets.output, {targets.tonemapped.get()}, vk14::draw_constants{}, nullptr); + post_pass("postfx_tonemapping.frag", *targets.tonemapped, tonemapinput, tonemap, "tonemapping, output"); + post_pass("postfx_chromaticaberration.frag", *targets.output, {targets.tonemapped.get()}, vk14::draw_constants{}); } else { - post("postfx_tonemapping.frag", *targets.output, {hdr, hdr}, tonemap, "tonemapping, output"); + post_pass("postfx_tonemapping.frag", *targets.output, tonemapinput, tonemap, "tonemapping, output"); + } + // the lights of the clusters of the pixels over the image, for the debugging of the lights + if (Global.gfx_lights_clusterheatmap) + { + render_cluster_heatmap(Index); } vk14::barriers(commands, {vk14::use_of(*targets.output, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, sampled_access)}); } @@ -2093,11 +2724,12 @@ void vulkan14_renderer::present_viewport(std::size_t const Index) return; } vk14::set_draw_state(commands, window.extent()); - vkCmdBindPipeline(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, reinterpret_cast(pipeline)); - vkCmdSetPrimitiveTopology(commands, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP); - std::array images; - images.fill(VkDescriptorImageInfo{ctx.sampling.clamp_linear, targets.output->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}); - scene.post_set.push(commands, scene.post_layout, images.data()); + commands->bind_pipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, reinterpret_cast(pipeline)); + commands->set_primitive_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP); + std::array descriptors; + descriptors.fill(vk14::image_descriptor(ctx.sampling.clamp_linear, targets.output->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)); + descriptors[vk14::bindings::POST_EXPOSURE_SSBO] = vk14::buffer_descriptor(scene.exposure.data.handle(), 0, sizeof(gl::exposure_data)); + scene.post_set.push(commands, scene.post_layout, descriptors.data()); vk14::draw_constants constants; VkPushConstantsInfo info{}; info.sType = VK_STRUCTURE_TYPE_PUSH_CONSTANTS_INFO; @@ -2105,8 +2737,8 @@ void vulkan14_renderer::present_viewport(std::size_t const Index) info.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; info.size = sizeof(constants); info.pValues = &constants; - vkCmdPushConstants2(commands, &info); - vkCmdDraw(commands, 4, 1, 0, 0); + commands->push_constants2(&info); + commands->draw(4, 1, 0, 0); } // a pipeline of the post-processing: quad.vert with the fragment shader, to an image of the format. the copy to a window has the @@ -2151,13 +2783,13 @@ void vulkan14_renderer::update_lights(light_array &Lights, bool const Headlights }); } glm::mat4 mv = OpenGLMatrices.data(GL_MODELVIEW); - // the headlights of opengl46_renderer::headlight_config - float const in_cutoff{1.005f}; - float const out_cutoff{0.993f}; - float const falloff_linear{0.15f}; - float const falloff_quadratic{0.15f}; - float const intensity{1.0f}; - float const ambient{0.0f}; + // the headlights as the debug window sets them (opengl46_renderer::headlight_config) + auto const in_cutoff{m_headlightconfig.in_cutoff}; + auto const out_cutoff{m_headlightconfig.out_cutoff}; + auto const falloff_linear{m_headlightconfig.falloff_linear}; + auto const falloff_quadratic{m_headlightconfig.falloff_quadratic}; + auto const intensity{m_headlightconfig.intensity}; + auto const ambient{m_headlightconfig.ambient}; sun_light renderlight; renderlight.is_directional = false; @@ -2174,6 +2806,17 @@ void vulkan14_renderer::update_lights(light_array &Lights, bool const Headlights auto &light{light_data.lights[light_i++]}; write_local_light(light, *lamp, camera, mv); m_cablamplit = true; + auto const *vehicle{m_locallights.cab_lamp_vehicle()}; + auto const &lamps{m_context->scene.lamps}; + if (lamps.ready(vehicle)) + { + // view space to the space of the map: the space of the cab model, with the lamp at the origin. the vehicle is where it + // is now, the map stays right as it moves + auto const toworld{glm::dmat4{glm::transpose(glm::mat3{mv})}}; + auto const tomap{glm::translate(glm::dmat4{1.0}, -glm::dvec3{lamps.lamp()}) * glm::inverse(vehicle->mMatrix) * glm::translate(glm::dmat4{1.0}, camera - vehicle->GetPosition()) * toworld}; + light.headlight_projection = glm::mat4{tomap}; + light.headlight_weights = lamps.weights(); + } } } auto const headlightsstart{light_i}; @@ -2225,14 +2868,25 @@ void vulkan14_renderer::update_lights(light_array &Lights, bool const Headlights headlight_projector(scenelight, projector, lightcam, depthproj, nearplane, fieldofview); light->headlight_projection = coordmove * depthproj * glm::translate(glm::mat4{lightcam}, glm::vec3{m_renderpass.pass_camera.position() - projector}) * glm::mat4{glm::inverse(worldcam)}; } - // headlights weights; no shadow maps of the headlights before the stage P4 + // headlights weights light->headlight_weights = {scenelight.state, 0.f}; + // the shadow map of the headlights, if they have one: the same projection, from where the projector was when it was drawn + auto const &lamps{m_context->scene.lamps}; + if (auto const shadowmap{lamps.headlight_map(scenelight.owner, scenelight.index)}; shadowmap >= 0) + { + auto const &slot{lamps.headlight(static_cast(shadowmap))}; + scene_ubs.headlight_shadow[shadowmap] = coordmove * slot.projection * glm::translate(glm::mat4{slot.rotation}, glm::vec3{m_renderpass.pass_camera.position() - slot.projector}) * glm::mat4{glm::inverse(worldcam)}; + scene_ubs.headlight_shadow_params[shadowmap] = lamps.headlight_params(static_cast(shadowmap)); + light->headlight_weights.w = static_cast(shadowmap + 1); + } ++light_i; } m_lights_lit = static_cast(light_i - headlightsstart); // lamps of models and vehicles, the most important first, those which may be seen lighting something in this view if (locallights) { + auto const toworld{glm::dmat4{glm::transpose(glm::mat3{mv})}}; + auto const &lamps{m_context->scene.lamps}; uint32_t placed{0}; uint32_t dropped{0}; for (auto const &local : m_locallights.lights()) @@ -2248,6 +2902,15 @@ void vulkan14_renderer::update_lights(light_array &Lights, bool const Headlights } auto &light{light_data.lights[light_i++]}; write_local_light(light, local, camera, mv); + std::size_t slot; + if (auto const *lamp{lamps.lamp(local, slot)}) + { + // view space to the space of the map: world axes with the lamp at the origin + light.headlight_projection = glm::mat4{glm::translate(glm::dmat4{1.0}, camera - lamp->position) * toworld}; + light.headlight_weights = lamps.lamp_weights(slot); + // the map is in the array of the scenery lamps + light.linear = 1.0f; + } ++placed; } m_locallights.placed(placed, dropped); @@ -2255,21 +2918,22 @@ void vulkan14_renderer::update_lights(light_array &Lights, bool const Headlights light_data.lights_total = static_cast(light_i); light_data.lights_count = 1 + std::min(m_lights_lit, static_cast(std::max(Global.DynamicLightCount, 0))); light_data.lights_clustered = Clustered ? 1 : 0; - light_data.env_ggx = 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; - light_data.sky_ambient_factor = 0.f; + 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; the fog of the weather comes with the stage P2 + // fill fog data light_data.fog_color = m_fogcolor; if (Global.fFogEnd > 0) { - m_fogrange = std::max(Global.fFogEnd / 1.5f, 5.f); - model_ubs.fog_density = 1.f / m_fogrange; + // the fog of the weather; the regular density is a reference the shading scales the fog of a draw with + m_fogrange = m_weatherstate.fog_range(); + model_ubs.fog_density = m_weatherstate.fog_density(); } else { @@ -2297,7 +2961,7 @@ void vulkan14_renderer::update_lights(light_array &Lights, bool const Headlights } std::memcpy(lights.data, &light_data, lightsize); m_draw->light_offset = static_cast(lights.offset); - vkCmdFillBuffer(commands, clusters.counters.handle(), 0, VK_WHOLE_SIZE, 0); + commands->fill_buffer(clusters.counters.handle(), 0, VK_WHOLE_SIZE, 0); VkMemoryBarrier2 barrier{}; barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; barrier.srcStageMask = VK_PIPELINE_STAGE_2_CLEAR_BIT | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; @@ -2308,22 +2972,24 @@ void vulkan14_renderer::update_lights(light_array &Lights, bool const Headlights dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.memoryBarrierCount = 1; dependency.pMemoryBarriers = &barrier; - vkCmdPipelineBarrier2(commands, &dependency); + commands->pipeline_barrier2(&dependency); std::array buffers{{{ctx.ring.handle(), clusters.params_offset, sizeof(gl::cluster_ubs)}, {clusters.aabbs.handle(), 0, VK_WHOLE_SIZE}, {clusters.ranges.handle(), 0, VK_WHOLE_SIZE}, {clusters.indices.handle(), 0, VK_WHOLE_SIZE}, {clusters.counters.handle(), 0, VK_WHOLE_SIZE}, {lights.buffer, lights.offset, sizeof(gl::light_data)}}}; - vkCmdBindPipeline(commands, VK_PIPELINE_BIND_POINT_COMPUTE, scene.cluster_cull); + commands->bind_pipeline(VK_PIPELINE_BIND_POINT_COMPUTE, scene.cluster_cull); scene.cluster_set.push(commands, scene.cluster_layout, buffers.data()); - vkCmdDispatch(commands, (gl::CLUSTER_COUNT + gl::CLUSTER_CULL_GROUP_SIZE - 1) / gl::CLUSTER_CULL_GROUP_SIZE, 1, 1); + commands->dispatch((gl::CLUSTER_COUNT + gl::CLUSTER_CULL_GROUP_SIZE - 1) / gl::CLUSTER_CULL_GROUP_SIZE, 1, 1); barrier.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; barrier.srcAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT; barrier.dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_CLEAR_BIT; barrier.dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT; - vkCmdPipelineBarrier2(commands, &dependency); + commands->pipeline_barrier2(&dependency); clusters.lights_total = light_data.lights_total; + // the counters of the culling for the debug panel, read once the gpu is done with the frame + clusters.counters_readback.push(commands, ctx.slot, ctx.timelinevalue + 1, clusters.counters.handle(), 0, sizeof(gl::cluster_counters)); } } @@ -2421,13 +3087,13 @@ void vulkan14_renderer::update_clusters(glm::ivec2 const &Targetsize) dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.memoryBarrierCount = 1; dependency.pMemoryBarriers = &barrier; - vkCmdPipelineBarrier2(commands, &dependency); - vkCmdBindPipeline(commands, VK_PIPELINE_BIND_POINT_COMPUTE, scene.cluster_build); + commands->pipeline_barrier2(&dependency); + commands->bind_pipeline(VK_PIPELINE_BIND_POINT_COMPUTE, scene.cluster_build); scene.cluster_set.push(commands, scene.cluster_layout, buffers.data()); - vkCmdDispatch(commands, (gl::CLUSTER_COUNT + gl::CLUSTER_BUILD_GROUP_SIZE - 1) / gl::CLUSTER_BUILD_GROUP_SIZE, 1, 1); + commands->dispatch((gl::CLUSTER_COUNT + gl::CLUSTER_BUILD_GROUP_SIZE - 1) / gl::CLUSTER_BUILD_GROUP_SIZE, 1, 1); barrier.srcAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT; barrier.dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_READ_BIT; - vkCmdPipelineBarrier2(commands, &dependency); + commands->pipeline_barrier2(&dependency); clusters.stale = false; } } @@ -2461,7 +3127,7 @@ void vulkan14_renderer::render_picking(rendermode const Mode) rendering.colorAttachmentCount = 1; rendering.pColorAttachments = &colour; rendering.pDepthAttachment = &depth; - vkCmdBeginRendering(commands, &rendering); + commands->begin_rendering(&rendering); start_draws(scene.pick_formats, pick_buffer_size, pick_buffer_size, vk14::scene_state::set_plain, false); setup_matrices(); walk_blending(false); @@ -2480,7 +3146,7 @@ void vulkan14_renderer::render_picking(rendermode const Mode) m_picksceneryitems.clear(); Render(simulation::Region); } - vkCmdEndRendering(commands); + commands->end_rendering(); end_draws(); auto buffer{std::make_shared()}; @@ -2499,7 +3165,7 @@ void vulkan14_renderer::render_picking(rendermode const Mode) copy.dstBuffer = buffer->handle(); copy.regionCount = 1; copy.pRegions = ®ion; - vkCmdCopyImageToBuffer2(commands, ©); + commands->copy_image_to_buffer2(©); host_read_barrier(commands); readback.data = buffer; readback.value = ctx.timelinevalue + 1; diff --git a/rendering/vulkan14/vulkan14grass.cpp b/rendering/vulkan14/vulkan14grass.cpp new file mode 100644 index 00000000..3c0e99ca --- /dev/null +++ b/rendering/vulkan14/vulkan14grass.cpp @@ -0,0 +1,157 @@ +/* +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/vulkan14/vulkan14grass.h" +#include "utilities/Logs.h" + +#include + +namespace vk14 +{ + +grass_field::~grass_field() +{ + destroy(); +} + +void grass_field::init(resources const &Resources) +{ + m_resources = &Resources; +} + +void grass_field::destroy() +{ + if (m_resources == nullptr) + { + return; + } + // the images go with the chunks; the device is idle + clear(); + m_chunks.clear(); + m_resources = nullptr; +} + +std::shared_ptr grass_field::make_image(VkFormat const Format, std::uint32_t const Width, std::uint32_t const Height, void const *Data, VkDeviceSize const Size, char const *Name) +{ + image_desc desc; + desc.format = Format; + desc.extent = {Width, Height, 1}; + desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + auto result{std::make_shared()}; + if (false == result->create(*m_resources->memory, desc, Name)) + { + ErrorLog(std::string("heightmap terrain: no gpu image for the ") + Name); + return nullptr; + } + if (false == m_resources->uploads->upload(result, Data, Size, {{0, {Width, Height, 1}}}, false)) + { + return nullptr; + } + return result; +} + +void grass_field::gpu_upload(std::uint64_t const Id, gfx::grass_chunk const &Source) +{ + if (m_resources == nullptr) + { + return; + } + auto &chunk{m_chunks[Id]}; + auto const side{static_cast(Source.cells + 1)}; + chunk.images[0] = make_image(VK_FORMAT_R32_SFLOAT, side, side, Source.heights, static_cast(side) * side * sizeof(float), "grass heights"); + chunk.samplers[0] = m_resources->sampling->clamp_nearest; + + // the densities of the kinds side by side, as the channels of a texel + auto const samples{static_cast(Source.samples + 1)}; + auto const plane{static_cast(samples) * samples}; + std::size_t kinds{0}; + while (kinds < Source.kinds.size() && Source.kinds[kinds] >= 0) + ++kinds; + std::vector texels(plane * 4, 0); + for (std::size_t kind = 0; kind < kinds; ++kind) + for (std::size_t i = 0; i < plane; ++i) + texels[i * 4 + kind] = Source.density[kind * plane + i]; + chunk.images[1] = make_image(VK_FORMAT_R8G8B8A8_UNORM, samples, samples, texels.data(), texels.size(), "grass densities"); + chunk.samplers[1] = m_resources->sampling->clamp_linear; + + std::uint8_t const none{0}; + auto const holeside{static_cast((Source.holes > 0 && Source.hole_mask != nullptr) ? Source.holes + 1 : 1)}; + chunk.images[2] = make_image(VK_FORMAT_R8_UNORM, holeside, holeside, holeside > 1 ? Source.hole_mask : &none, static_cast(holeside) * holeside, "grass holes"); + chunk.samplers[2] = m_resources->sampling->clamp_linear; + // the table is made when the chunk is drawn + chunk.images[3].reset(); + chunk.samplers[3] = m_resources->sampling->clamp_nearest; + chunk.table_made = false; +} + +void grass_field::gpu_release(std::uint64_t const Id) +{ + auto const lookup{m_chunks.find(Id)}; + if (lookup == m_chunks.end()) + { + return; + } + if (m_resources != nullptr) + { + // the frames in flight may still read them + m_resources->deletions->push(m_resources->uploads->current_value(), [images = std::move(lookup->second.images)]() {}); + } + m_chunks.erase(lookup); +} + +grass_field::chunk_images const *grass_field::images(chunk_draw const &Draw) +{ + auto const lookup{m_chunks.find(Draw.id)}; + if ((lookup == m_chunks.end()) || (m_resources == nullptr)) + { + return nullptr; + } + auto &chunk{lookup->second}; + for (std::size_t i = 0; i < 3; ++i) + { + if (chunk.images[i] == nullptr) + { + return nullptr; + } + } + // the kinds of the chunk, by columns: base, tip, shape, dry; the share and the chunk in the last row + std::array table{}; + for (std::size_t i = 0; i < 4; ++i) + { + table[0 * 4 + i] = Draw.base[i]; + table[1 * 4 + i] = Draw.tip[i]; + table[2 * 4 + i] = Draw.shape[i]; + table[3 * 4 + i] = Draw.dry[i]; + } + table[4 * 4 + 0] = Draw.share; + table[4 * 4 + 1] = Draw.chunk; + if ((false == chunk.table_made) || (std::memcmp(table.data(), chunk.table.data(), sizeof(table)) != 0) || (chunk.images[3] == nullptr)) + { + auto map{chunk.images[3]}; + if (map == nullptr) + { + map = make_image(VK_FORMAT_R32G32B32A32_SFLOAT, 4, 5, table.data(), sizeof(table), "grass kinds"); + } + else if (false == m_resources->uploads->upload(map, table.data(), sizeof(table), {{0, {4, 5, 1}}}, false)) + { + map.reset(); + } + if (map == nullptr) + { + return nullptr; + } + chunk.images[3] = map; + chunk.table = table; + chunk.table_made = true; + } + return &chunk; +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14grass.h b/rendering/vulkan14/vulkan14grass.h new file mode 100644 index 00000000..3a943082 --- /dev/null +++ b/rendering/vulkan14/vulkan14grass.h @@ -0,0 +1,60 @@ +/* +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/. +*/ + +// the grass of the heightmap terrain in the vulkan renderer (the stage P6 of the plan, opengl46_grass): the images of the chunks +// of gfx::grass_field, uploaded when a chunk comes near. what the opengl renderer gives grass.vert in uniforms goes to the push +// constants (the tile and the frame) and to a table of the chunk (its kinds, an image the vertex shader reads), which is made +// again when what it holds changes + +#pragma once + +#include "rendering/common/grassfield.h" +#include "rendering/vulkan14/vulkan14texture.h" + +#include +#include +#include + +namespace vk14 +{ + +class grass_field : public gfx::grass_field +{ + public: + // the images a chunk is drawn with, in the order of the bindings of grass.vert: heights, densities, holes, the table + struct chunk_images + { + std::array, 4> images; + std::array samplers{}; + std::array table{}; // what the table holds + bool table_made{false}; + }; + + grass_field() = default; + ~grass_field() override; + + // called once, before anything is made + void init(resources const &Resources); + // the images go (the device is idle) + void destroy(); + // the images of a chunk drawn, its table made for the draw first; null: the chunk has none (yet) + chunk_images const *images(chunk_draw const &Draw); + + protected: + void gpu_upload(std::uint64_t const Id, gfx::grass_chunk const &Source) override; + void gpu_release(std::uint64_t const Id) override; + + private: + std::shared_ptr make_image(VkFormat const Format, std::uint32_t const Width, std::uint32_t const Height, void const *Data, VkDeviceSize const Size, char const *Name); + + resources const *m_resources{nullptr}; + std::unordered_map m_chunks; +}; + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14imgui.cpp b/rendering/vulkan14/vulkan14imgui.cpp index 57476cfc..4018ffb3 100644 --- a/rendering/vulkan14/vulkan14imgui.cpp +++ b/rendering/vulkan14/vulkan14imgui.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/vulkan14/vulkan14imgui.h" +#include "rendering/vulkan14/vulkan14commands.h" #include "rendering/vulkan14/vulkan14bindings.h" #include "rendering/vulkan14/vulkan14device.h" #include "imgui/imgui.h" @@ -216,7 +217,7 @@ void imgui_backend::Release_Image(std::uint64_t const Image) m_resources->images->remove(Image, *m_resources->deletions, m_resources->uploads->current_value()); } -void imgui_backend::record(VkCommandBuffer Commands, VkExtent2D Extent, VkFormat Format, bool Srgb) +void imgui_backend::record(command_stream *Commands, VkExtent2D Extent, VkFormat Format, bool Srgb) { auto const *drawdata{m_drawdata}; m_drawdata = nullptr; @@ -270,10 +271,10 @@ void imgui_backend::record(VkCommandBuffer Commands, VkExtent2D Extent, VkFormat indexdata += commands->IdxBuffer.Size * sizeof(ImDrawIdx); } - vkCmdBindPipeline(Commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + Commands->bind_pipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); set_draw_state(Commands, Extent); - vkCmdBindVertexBuffers(Commands, 0, 1, &vertices.buffer, &vertices.offset); - vkCmdBindIndexBuffer(Commands, indices.buffer, indices.offset, sizeof(ImDrawIdx) == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32); + Commands->bind_vertex_buffers(0, 1, &vertices.buffer, &vertices.offset); + Commands->bind_index_buffer(indices.buffer, indices.offset, sizeof(ImDrawIdx) == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32); // the pixels of the display (top left origin, y down) to the clip space of vulkan (y down too) float transform[4]; @@ -288,7 +289,7 @@ void imgui_backend::record(VkCommandBuffer Commands, VkExtent2D Extent, VkFormat push.offset = 0; push.size = sizeof(transform); push.pValues = transform; - vkCmdPushConstants2(Commands, &push); + Commands->push_constants2(&push); auto const clipoffset{drawdata->DisplayPos}; std::uint32_t vertexoffset{0}, indexoffset{0}; @@ -324,14 +325,14 @@ void imgui_backend::record(VkCommandBuffer Commands, VkExtent2D Extent, VkFormat continue; } VkRect2D scissor{{static_cast(minx), static_cast(miny)}, {static_cast(maxx - minx), static_cast(maxy - miny)}}; - vkCmdSetScissor(Commands, 0, 1, &scissor); + Commands->set_scissor(0, 1, &scissor); if (pushed != command.GetTexID()) { VkDescriptorImageInfo info{entry->sampler, entry->target->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; m_set.push(Commands, m_layout, &info); pushed = command.GetTexID(); } - vkCmdDrawIndexed(Commands, command.ElemCount, 1, command.IdxOffset + indexoffset, static_cast(command.VtxOffset + vertexoffset), 0); + Commands->draw_indexed(command.ElemCount, 1, command.IdxOffset + indexoffset, static_cast(command.VtxOffset + vertexoffset), 0); } vertexoffset += static_cast(commands->VtxBuffer.Size); indexoffset += static_cast(commands->IdxBuffer.Size); diff --git a/rendering/vulkan14/vulkan14imgui.h b/rendering/vulkan14/vulkan14imgui.h index 7a4238dd..38d27852 100644 --- a/rendering/vulkan14/vulkan14imgui.h +++ b/rendering/vulkan14/vulkan14imgui.h @@ -50,7 +50,7 @@ class imgui_backend : public imgui_renderer } // records the frame of imgui into Commands, in a rendering to an image of Extent and Format begun by the renderer. Srgb: the // image encodes the colour itself - void record(VkCommandBuffer Commands, VkExtent2D Extent, VkFormat Format, bool Srgb); + void record(command_stream *Commands, VkExtent2D Extent, VkFormat Format, bool Srgb); // the frame of imgui is dropped (nothing to draw it to) void skip() { diff --git a/rendering/vulkan14/vulkan14indirect.cpp b/rendering/vulkan14/vulkan14indirect.cpp new file mode 100644 index 00000000..e494dcf7 --- /dev/null +++ b/rendering/vulkan14/vulkan14indirect.cpp @@ -0,0 +1,822 @@ +/* +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/. +*/ + +// the instances drawn from the gpu culling in the vulkan renderer (the stage P5 of the plan; opengl46_renderer::Render_Instances_Indirect() +// and the rest): the cullings of the passes picked as the opengl renderer picks them, the rendering of the pass ended for the compute +// passes and begun again where it was, the walks of the culled groups shared with the opengl renderer (gfx::scene_walk), and the indirect +// draws of the recorded draws, with multi draws where they follow one another with the same state + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14context.h" +#include "rendering/vulkan14/vulkan14commands.h" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" + +#include + +namespace +{ + +VkPipelineStageFlags2 const compute_stage{VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT}; + +} // namespace + +// the cullings + +bool vulkan14_renderer::init_cull() +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &cull{scene.cull}; + auto const device{ctx.device.handle()}; + vk14::cull_environment environment; + environment.memory = &ctx.memory; + environment.uploads = &ctx.uploads; + environment.deletions = &ctx.deletions; + environment.ring = &ctx.ring; + environment.uniform_alignment = ctx.device.caps().limits.minUniformBufferOffsetAlignment; + environment.storage_alignment = ctx.device.caps().limits.minStorageBufferOffsetAlignment; + environment.frames = static_cast(ctx.frames.size()); + environment.no_pyramid = vk14::image_descriptor(ctx.sampling.clamp_nearest, scene.depth_empty->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + if (false == cull.main.init(environment, device, ctx.pipelines, ctx.shaders, scene.pass_layout)) + { + return false; + } + for (auto &cascade : cull.cascades) + { + if (false == cascade.init(environment, device, ctx.pipelines, ctx.shaders, scene.pass_layout, &cull.main)) + { + return false; + } + } + if ((false == cull.reflections.init(environment, device, ctx.pipelines, ctx.shaders, scene.pass_layout, &cull.main)) || (false == cull.hiz.init(device, ctx.pipelines, ctx.shaders, scene.pass_layout))) + { + return false; + } + cull.main.begin_frame(ctx.slot, ctx.timelinevalue + 1, ctx.completed()); + cull.ready = true; + return true; +} + +void vulkan14_renderer::destroy_cull() +{ + if (!m_context) + { + return; + } + auto &cull{m_context->scene.cull}; + auto const device{m_context->device.handle()}; + // the ones which share the kernels of the main view first + for (auto &cascade : cull.cascades) + { + cascade.destroy(device); + } + for (auto *culls : {&cull.headlights, &cull.lamps}) + { + for (auto &culling : *culls) + { + if (culling) + { + culling->destroy(device); + } + } + culls->clear(); + } + cull.reflections.destroy(device); + cull.main.destroy(device); + cull.hiz.destroy(device); + cull.shadowfit.destroy(device); + cull.ready = false; +} + +vk14::instance_cull &vulkan14_renderer::pass_cull() +{ + return static_cast(*m_passcull); +} + +// the gpu culling runs along the first color pass of the main viewport of a frame, and the passes of the cascades of the sun +// shadows and of the faces of the environment map drawn for it, each with a culling of its own (opengl46_renderer::walk_region_begin()) +void vulkan14_renderer::walk_region_begin() +{ + vk14::instance_cull *passcull{nullptr}; + auto &cull{m_context->scene.cull}; + if ((Global.gfx_gpucull != 0) && (m_current_viewport != nullptr) && (m_current_viewport->main) && m_draw->rendering) + { + static bool tried{false}; + if ((false == cull.ready) && (false == tried)) + { + tried = true; + if (false == init_cull()) + { + ErrorLog("vulkan: the gpu culling of the instances can't be used, the cpu culls them (gfx.gpucull)"); + destroy_cull(); + } + } + if (cull.ready) + { + if ((m_renderpass.draw_mode == rendermode::color) && (m_instancecullframe != m_framestamp)) + { + m_instancecullframe = m_framestamp; + passcull = &cull.main; + } + else if ((m_renderpass.draw_mode == rendermode::shadows) && (m_lightshadowcull != nullptr)) + { + // the map of a set of headlights or the scenery part of the map of a lamp, which picked its culling + passcull = m_lightshadowcull; + m_shadowcasters.clear(); + } + else if ((m_renderpass.draw_mode == rendermode::shadows) && Global.gfx_gpucull_shadows && (m_shadowcascade >= 0) && (m_shadowcascade < static_cast(cull.cascades.size())) && + (false == m_cubeshadows)) + { + // the cascades of the sun + passcull = &cull.cascades[m_shadowcascade]; + m_shadowcasters.clear(); + } + else if ((m_renderpass.draw_mode == rendermode::reflections) && Global.gfx_gpucull_reflections && (Global.reflectiontune.fidelity >= 1)) + { + passcull = &cull.reflections; + } + } + } + Begin_Pass_Cull(passcull, (passcull == &cull.main)); +} + +bool vulkan14_renderer::walk_instances_begin() +{ + if (m_indirectcolourpending) + { + // the instances culled and recorded by the depth pass of the whole scene, the buckets weren't queued for the cpu + render_instances_indirect_pending(); + } + if (m_instancecullpass) + { + if (m_passcull->drawing()) + { + // the buckets of the pass weren't queued for the cpu, which leaves the loop of the walk empty + if (m_passcull == &m_context->scene.cull.main) + { + render_instances_indirect(); + } + else + { + render_instances_indirect_pass(); + } + } + else + { + // the view of the pass is in place here, the one Render_Instanced() takes + suspend_rendering(); + pass_cull().dispatch(m_context->commands, OpenGLMatrices.data(GL_MODELVIEW)); + resume_rendering(); + // what the cpu drew of the sleepers and the models drawn on their own + Compare_Pass_Cull(); + } + } + return m_instancecullpass; +} + +void vulkan14_renderer::walk_instances_end() +{ + End_Pass_Cull(); +} + +// the rendering ended and begun again around the compute passes + +void vulkan14_renderer::remember_rendering(VkRenderingInfo const &Info, std::vector const &Uses) +{ + auto &resume{m_draw->resume}; + resume.info = Info; + resume.info.pNext = nullptr; + resume.info.flags = 0; + auto const colours{std::min(Info.colorAttachmentCount, static_cast(resume.colours.size()))}; + for (std::uint32_t i = 0; i < colours; ++i) + { + resume.colours[i] = Info.pColorAttachments[i]; + // what was drawn is drawn over + resume.colours[i].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + } + resume.info.colorAttachmentCount = colours; + resume.info.pColorAttachments = (colours > 0 ? resume.colours.data() : nullptr); + resume.has_depth = (Info.pDepthAttachment != nullptr); + if (resume.has_depth) + { + resume.depth = *Info.pDepthAttachment; + resume.depth.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + } + resume.info.pDepthAttachment = (resume.has_depth ? &resume.depth : nullptr); + resume.info.pStencilAttachment = nullptr; + resume.uses = Uses; + resume.valid = true; +} + +void vulkan14_renderer::suspend_rendering() +{ + auto &draw{*m_draw}; + if (false == draw.rendering) + { + return; + } + m_context->commands->end_rendering(); + draw.rendering = false; +} + +void vulkan14_renderer::resume_rendering() +{ + auto &draw{*m_draw}; + auto &resume{draw.resume}; + if (draw.rendering || (false == resume.valid) || (draw.targets == nullptr)) + { + return; + } + auto const commands{m_context->commands}; + // what the rendering stored before is loaded again + vk14::barriers(commands, resume.uses); + commands->begin_rendering(&resume.info); + // the dynamic state starts anew with the rendering, the state of the walk goes on + auto const set{draw.set}; + auto const shadows{draw.shadows}; + auto const blending{draw.blending}; + auto const cullface{draw.cullface}; + auto const glare{draw.glare}; + auto const depthmask_off{draw.depthmask_off}; + auto *program{draw.program}; + auto const textures{draw.textures}; + start_draws(*draw.targets, static_cast(draw.extent.width), static_cast(draw.extent.height), set, shadows); + draw.blending = blending; + draw.cullface = cullface; + draw.glare = glare; + draw.depthmask_off = depthmask_off; + draw.program = program; + draw.textures = textures; + if (draw.depthpass) + { + // the bias of the depth pass of the scene (render_scene_depth()) + commands->set_depth_bias_enable(VK_TRUE); + commands->set_depth_bias(-1.f, 0.f, -1.f); + } +} + +// the draws + +vk14::scene_program *vulkan14_renderer::multidraw_program(vk14::scene_program &Program) +{ + if (Program.multidraw_tried) + { + return Program.multidraw.get(); + } + Program.multidraw_tried = true; + if (Program.vertex_name.empty() || Program.fragment_name.empty() || (Program.vertices != vk14::vertex_kind::scene)) + { + return nullptr; + } + auto &ctx{*m_context}; + try + { + auto variant{std::make_shared()}; + variant->defines = Program.defines + "#define MODEL_DRAWS\n"; + variant->vertex = ctx.shaders.get(Program.vertex_name, variant->defines); + variant->fragment = ctx.shaders.get(Program.fragment_name, variant->defines); + variant->vertex_name = Program.vertex_name; + variant->fragment_name = Program.fragment_name; + variant->vertices = Program.vertices; + variant->name = Program.name + " (multi draws)"; + variant->multidraw_tried = true; + Program.multidraw = std::move(variant); + } + catch (gfx::shader_exception const &Exception) + { + vk14::log_error(Exception.what()); + } + return Program.multidraw.get(); +} + +bool vulkan14_renderer::draw_indirect(vk14::instance_cull &Cull, command_set const Set, std::size_t const First, std::uint32_t const Count, vk14::scene_program &Program, vk14::blend_mode const Blend) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &draw{*m_draw}; + auto const &first{m_indirectdraws[First]}; + if ((false == draw.rendering) || (Count == 0) || (Program.vertices != vk14::vertex_kind::scene) || (Cull.command_buffer() == VK_NULL_HANDLE)) + { + return false; + } + auto const &geometry{first.geometry}; + if ((geometry.bank == 0) || (geometry.bank > m_banks.size()) || (m_banks[geometry.bank - 1] == nullptr)) + { + return false; + } + // the buffers of the bank of the chunks, with their data on the gpu (the chunks of a multi draw are of one bank) + gfx::indirect_source source; + if (false == m_geometry.indirect_source(geometry, source)) + { + return false; + } + auto const &chunk{static_cast(m_banks[geometry.bank - 1])->last()}; + if (false == bind_scene_pipeline(Program, *draw.targets, Blend, static_cast(chunk.topology))) + { + return false; + } + apply_draw_state(); + // the blocks of the draws, one after another in the window of the blocks: the multi draw picks them by the number of the draw + std::uint32_t block{~0u}; + if (Count == 1) + { + model_ubs = first.ubs; + block = push_draw_block(); + } + else + { + auto &draws{scene.draws}; + auto const size{static_cast(Count) * draws.stride}; + auto const slice{size <= draws.window ? draws.ring.allocate(size, draws.stride) : vk14::ring_slice{}}; + if (slice) + { + if ((false == draws.open) || (slice.offset + size > draws.base + draws.window)) + { + draws.base = slice.offset; + draws.open = true; + draw.set_dirty = true; + } + for (std::uint32_t i = 0; i < Count; ++i) + { + std::memcpy(slice.data + i * draws.stride, &m_indirectdraws[First + i].ubs, sizeof(gl::model_ubs)); + } + block = static_cast((slice.offset - draws.base) / draws.stride); + } + } + // the next draw of the walk pushes a block of its own + draw.block = ~0u; + if (block == ~0u) + { + return false; + } + if (draw.set_dirty) + { + bind_pass_set(); + } + if (draw.textures_dirty) + { + push_textures(); + } + auto const commands{draw.commands}; + if (draw.pushed_draw != block) + { + vk14::draw_constants constants; + constants.draw = block; + VkPushConstantsInfo info{}; + info.sType = VK_STRUCTURE_TYPE_PUSH_CONSTANTS_INFO; + info.layout = scene.scene_layout; + info.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + info.offset = 0; + info.size = sizeof(constants); + info.pValues = &constants; + commands->push_constants2(&info); + draw.pushed_draw = block; + } + if ((chunk.vertices != draw.vertices) || (chunk.userdata != draw.userdata)) + { + VkBuffer const buffers[2]{chunk.vertices, chunk.userdata}; + VkDeviceSize const offsets[2]{0, 0}; + commands->bind_vertex_buffers(0, 2, buffers, offsets); + draw.vertices = chunk.vertices; + draw.userdata = chunk.userdata; + } + auto const offset{static_cast(Cull.draw_command_offset(Set, First))}; + if (chunk.indices != VK_NULL_HANDLE) + { + if (chunk.indices != draw.indices) + { + commands->bind_index_buffer(chunk.indices, 0, VK_INDEX_TYPE_UINT32); + draw.indices = chunk.indices; + } + commands->draw_indexed_indirect(Cull.command_buffer(), offset, Count, sizeof(gl::instance_draw_command)); + } + else + { + commands->draw_indirect(Cull.command_buffer(), offset, Count, sizeof(gl::instance_draw_command)); + } + draw.draws += Count; + return true; +} + +vulkan14_renderer::indirect_counts vulkan14_renderer::draw_indirect_colour(vk14::instance_cull &Cull, command_set const Set) +{ + auto &draw{*m_draw}; + auto const &scene{m_context->scene}; + // the draws were recorded with the seed of scenery models and sleepers, 0 (TAnimModel::RaPrepare()); vehicles drawn since + // leave their own, which would pick other texture variants than those the draws were recorded and ordered with + TSubModel::iVariantSeed = 0; + indirect_counts counts; + auto const count{m_indirectdraws.size()}; + auto const longest{static_cast(scene.draws.window / scene.draws.stride)}; + for (std::size_t index = 0; index < count;) + { + auto const blending{m_indirectdraws[index].blending}; + if (blending) + { + walk_blending(true); + } + walk_material(m_indirectdraws[index].material, m_indirectdraws[index].submodel); + auto *program{draw.program}; + auto *multidraw{(m_indirectmultidraw && (program != nullptr)) ? multidraw_program(*program) : nullptr}; + auto last{index + 1}; + if (multidraw != nullptr) + { + // the draws which follow with the same state, in one call + while ((last < count) && (last - index < longest) && Indirect_Mergeable(index, last)) + { + ++last; + } + } + if (program != nullptr) + { + auto const drawcount{static_cast(last - index)}; + if (draw_indirect(Cull, Set, index, drawcount, (multidraw != nullptr ? *multidraw : *program), (draw.blending ? vk14::blend_mode::scene_alpha : vk14::blend_mode::none))) + { + counts.draws += drawcount; + } + ++counts.calls; + } + index = last; + if (blending) + { + walk_blending(false); + } + } + if (count > 0) + { + // the model block the draws made one by one leave behind + model_ubs = m_indirectdraws.back().ubs; + } + model_ubs.emission = 0.0f; + return counts; +} + +vulkan14_renderer::indirect_counts vulkan14_renderer::draw_indirect_depth(vk14::instance_cull &Cull, command_set const Set) +{ + auto &draw{*m_draw}; + auto &scene{m_context->scene}; + auto const commands{m_context->commands}; + // as in draw_indirect_colour(): the seed the draws were recorded with + TSubModel::iVariantSeed = 0; + indirect_counts counts; + auto const count{m_indirectdraws.size()}; + auto const longest{static_cast(scene.draws.window / scene.draws.stride)}; + // whether the prepass of the class draws the draw, and with which diffuse texture (class 2) + auto const prepass_texture = [this](std::size_t const Index, int const Prepassclass, texture_handle &Texture) -> bool { + auto const &entry{m_indirectdraws[Index]}; + if ((entry.blending) || (entry.ubs.opacity < 0.f)) + { + return false; + } + if ((entry.type != GL_TRIANGLES) && (entry.type != GL_TRIANGLE_STRIP) && (entry.type != GL_TRIANGLE_FAN)) + { + return false; + } + if (prepass_class(entry.material) != Prepassclass) + { + return false; + } + Texture = null_handle; + if (Prepassclass == 2) + { + // the diffuse texture of the colour pass, the same choice as walk_material() makes + auto const &material{m_materials.material(entry.material)}; + auto const &textures{material.texture_variants.empty() ? material.textures : material.GetTextures(TSubModel::VariantId(entry.submodel))}; + if (textures[0] == null_handle) + { + return false; + } + Texture = textures[0]; + } + return true; + }; + // inside the colour pass the depth goes a little farther than the surface, without the colour (the depth pass of the scene has + // its own state) + auto const insidecolour{false == draw.depthpass}; + if (insidecolour) + { + commands->set_depth_bias_enable(VK_TRUE); + commands->set_depth_bias(-1.f, 0.f, -1.f); + } + walk_blending(false); + // the draws without the alpha test first, then the ones with it, one program each + for (auto prepassclass = 1; prepassclass <= 2; ++prepassclass) + { + auto &program{prepassclass == 1 ? *scene.prepass : *scene.prepass_alpha}; + auto *multidraw{m_indirectmultidraw ? multidraw_program(program) : nullptr}; + for (std::size_t index = 0; index < count;) + { + texture_handle texture; + if (false == prepass_texture(index, prepassclass, texture)) + { + ++index; + continue; + } + if (prepassclass == 2) + { + walk_texture(0, texture); + } + auto const &entry{m_indirectdraws[index]}; + auto last{index + 1}; + if (multidraw != nullptr) + { + // the draws which follow in the same bank and of the same primitive, with the same texture for the alpha test. the + // material doesn't matter to the depth + texture_handle othertexture; + while ((last < count) && (last - index < longest) && (m_indirectdraws[last].group == entry.group) && (m_indirectdraws[last].type == entry.type) && + (m_indirectcommands[last].indexed == m_indirectcommands[index].indexed) && (prepass_texture(last, prepassclass, othertexture)) && (othertexture == texture)) + { + ++last; + } + } + auto const drawcount{static_cast(last - index)}; + if (draw_indirect(Cull, Set, index, drawcount, (multidraw != nullptr ? *multidraw : program), (insidecolour ? vk14::blend_mode::no_colour : vk14::blend_mode::none))) + { + counts.draws += drawcount; + } + ++counts.calls; + index = last; + } + } + if (insidecolour) + { + commands->set_depth_bias_enable(VK_FALSE); + } + return counts; +} + +vulkan14_renderer::indirect_counts vulkan14_renderer::draw_indirect_shadow(vk14::instance_cull &Cull, command_set const Set) +{ + auto &scene{m_context->scene}; + indirect_counts counts; + auto const count{m_indirectdraws.size()}; + auto const longest{static_cast(scene.draws.window / scene.draws.stride)}; + for (std::size_t index = 0; index < count;) + { + auto const &entry{m_indirectdraws[index]}; + auto const texture{Indirect_Shadow_Texture(entry.material)}; + auto &program{m_cubeshadows ? (texture != null_handle ? *scene.cube_alpha_shadow : *scene.cube_shadow) : (texture != null_handle ? *scene.alpha_shadow : *scene.shadow)}; + auto *multidraw{m_indirectmultidraw ? multidraw_program(program) : nullptr}; + auto last{index + 1}; + if (multidraw != nullptr) + { + // the draws which follow in the same bank and of the same primitive, with the same texture for the alpha test + while ((last < count) && (last - index < longest) && (m_indirectdraws[last].group == entry.group) && (m_indirectdraws[last].type == entry.type) && + (m_indirectcommands[last].indexed == m_indirectcommands[index].indexed) && (Indirect_Shadow_Texture(m_indirectdraws[last].material) == texture)) + { + ++last; + } + } + if (texture != null_handle) + { + walk_texture(0, texture); + } + auto const drawcount{static_cast(last - index)}; + if (draw_indirect(Cull, Set, index, drawcount, (multidraw != nullptr ? *multidraw : program), vk14::blend_mode::none)) + { + counts.draws += drawcount; + } + ++counts.calls; + index = last; + } + if (count > 0) + { + model_ubs = m_indirectdraws.back().ubs; + } + return counts; +} + +void vulkan14_renderer::record_indirect_draws(vk14::instance_cull &Cull, bool const Cascade) +{ + m_indirectcull = &Cull; + Walk_Indirect_Draws(Cull, Cascade); + auto const orderstart{std::chrono::steady_clock::now()}; + // with multi draws the draws with the same state follow one another; the order goes into the commands, so it comes first + m_indirectmultidraw = Global.gfx_gpucull_multidraw; + if (m_indirectmultidraw && (false == m_indirectdraws.empty())) + { + Order_Indirect_Draws(); + } + Cull.fill_commands(m_context->commands, m_indirectcommands); + m_walkcounts.ordertime += std::chrono::duration(std::chrono::steady_clock::now() - orderstart).count(); +} + +// the instances of the main view: the depth pyramid of what the pass drew so far, the culling, the walks, the depth first (the +// depth pass of the scene, gfx.depthprepass), the colour; in two phases the instances seen in the previous frame first +void vulkan14_renderer::render_instances_indirect() +{ + auto const cpustart{std::chrono::steady_clock::now()}; + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &cull{scene.cull.main}; + auto &draw{*m_draw}; + auto const commands{ctx.commands}; + auto const viewport{static_cast(m_current_viewport - m_viewports.data())}; + auto *targets{viewport < scene.targets.size() ? &scene.targets[viewport] : nullptr}; + auto const projection{OpenGLMatrices.data(GL_PROJECTION)}; + + suspend_rendering(); + // the occlusion test against the depth drawn so far in the pass: cab, terrain, shapes, tracks, vehicles, other models + vk14::cull_occlusion occlusion; + auto const build = [&]() { + vk14::barriers(commands, {vk14::use_of(*targets->depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, compute_stage, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT)}); + if (scene.cull.hiz.build(commands, ctx.device.handle(), ctx.memory, ctx.deletions, ctx.uploads.current_value(), targets->depth->view(), ctx.sampling.clamp_nearest, static_cast(targets->extent.width), + static_cast(targets->extent.height))) + { + occlusion = scene.cull.hiz.occlusion(ctx.sampling.clamp_nearest, projection); + } + }; + auto const useocclusion{Global.gfx_gpucull_occlusion && (false == Global.gfx_skippipeline) && (targets != nullptr) && targets->depth}; + if (useocclusion) + { + build(); + } + // in two phases the instances seen in the previous frame are drawn first, and the others are tested against a pyramid made + // again with their depth in it (not in wireframe, which doesn't draw surfaces) + cull.dispatch(commands, OpenGLMatrices.data(GL_MODELVIEW), (occlusion.pyramid != VK_NULL_HANDLE ? &occlusion : nullptr), (Global.gfx_gpucull_occlusion_instances && (false == Global.bWireFrame))); + // the draws, recorded while the gpu culls + record_indirect_draws(cull, false); + + auto const twophase{cull.two_phase()}; + auto const draws{false == m_indirectdraws.empty()}; + // the depth comes first in the depth pass of the whole scene, which leaves the colour to the colour pass, and with the depth + // prepass of the instances + auto const depthfirst{draw.depthpass || (Global.gfx_depthprepass && (false == Global.bWireFrame))}; + indirect_counts prepasscounts; + indirect_counts colourcounts; + auto colourset{command_set::first}; + resume_rendering(); + if (twophase) + { + if (draws) + { + // the instances seen in the previous frame, then the pyramid again, with them in it + use_cull_pass_sets(&cull); + if (depthfirst) + { + prepasscounts += draw_indirect_depth(cull, command_set::first); + } + else + { + colourcounts += draw_indirect_colour(cull, command_set::first); + } + use_cull_pass_sets(nullptr); + } + suspend_rendering(); + if (draws) + { + build(); + } + // the others are tested against it; with no draws the instances are only marked for the next frame + cull.dispatch_second(commands, occlusion); + resume_rendering(); + colourset = (depthfirst ? command_set::all : command_set::second); + } + if (draws) + { + use_cull_pass_sets(&cull); + if (depthfirst) + { + prepasscounts += draw_indirect_depth(cull, (twophase ? command_set::second : command_set::first)); + } + if (false == draw.depthpass) + { + colourcounts += draw_indirect_colour(cull, colourset); + } + use_cull_pass_sets(nullptr); + } + if (draw.depthpass) + { + // the colour pass takes the draws from here, and counts them + m_indirectcolourpending = true; + m_indirectcolourset = colourset; + m_indirectcoloursleepers = m_sleeperspass; + m_indirectcolourmodels = m_modelspass; + m_renderpass.draw_stats.drawcalls += static_cast(prepasscounts.calls); + colourcounts.draws = static_cast(m_indirectdraws.size()); + } + else + { + m_renderpass.draw_stats.instanced_drawcalls += static_cast(colourcounts.calls); + m_renderpass.draw_stats.drawcalls += static_cast(prepasscounts.calls); + // the count of the frame the gpu reported last + m_renderpass.draw_stats.instances += static_cast(cull.last_count()); + m_renderpass.draw_stats.models += static_cast(cull.last_count()); + } + cull.draw_result(colourcounts.draws, prepasscounts.draws, colourcounts.calls, prepasscounts.calls, std::chrono::duration(std::chrono::steady_clock::now() - cpustart).count()); +} + +// the instances of a pass other than the main view: the culling of the pass with its own camera, no occlusion test, the draws +// recorded the same way, then drawn once with the programs the pass draws with on the cpu +void vulkan14_renderer::render_instances_indirect_pass() +{ + auto const cpustart{std::chrono::steady_clock::now()}; + auto &cull{pass_cull()}; + auto &cascades{m_context->scene.cull.cascades}; + suspend_rendering(); + cull.dispatch(m_context->commands, OpenGLMatrices.data(GL_MODELVIEW)); + record_indirect_draws(cull, (&cull >= cascades.data()) && (&cull < cascades.data() + cascades.size())); + resume_rendering(); + + indirect_counts counts; + if (false == m_indirectdraws.empty()) + { + use_cull_pass_sets(&cull); + counts = (m_renderpass.draw_mode == rendermode::shadows ? draw_indirect_shadow(cull, command_set::first) : draw_indirect_colour(cull, command_set::first)); + use_cull_pass_sets(nullptr); + } + m_renderpass.draw_stats.instanced_drawcalls += static_cast(counts.calls); + // the count of the frame the gpu reported last + m_renderpass.draw_stats.instances += static_cast(cull.last_count()); + m_renderpass.draw_stats.models += static_cast(cull.last_count()); + cull.draw_result(counts.draws, 0, counts.calls, 0, std::chrono::duration(std::chrono::steady_clock::now() - cpustart).count()); +} + +// the colour of the instances culled and recorded by the depth pass of the whole scene +void vulkan14_renderer::render_instances_indirect_pending() +{ + m_indirectcolourpending = false; + auto &cull{m_context->scene.cull.main}; + indirect_counts colourcounts; + if (false == m_indirectdraws.empty()) + { + auto const cpustart{std::chrono::steady_clock::now()}; + use_cull_pass_sets(&cull); + colourcounts = draw_indirect_colour(cull, m_indirectcolourset); + use_cull_pass_sets(nullptr); + cull.draw_result(colourcounts.draws, cull.prepass_draws(), colourcounts.calls, cull.prepass_calls(), cull.cpu_time() + std::chrono::duration(std::chrono::steady_clock::now() - cpustart).count()); + } + m_renderpass.draw_stats.instanced_drawcalls += static_cast(colourcounts.calls); + m_renderpass.draw_stats.instances += static_cast(cull.last_count()); + m_renderpass.draw_stats.models += static_cast(cull.last_count()); +} + +// the cascades of the sun shadows fitted to the depth of the main view (opengl46_renderer::Shadow_Fit()) + +char const *vulkan14_renderer::shadow_fit_unavailable(viewport_config const &Viewport) const +{ + auto const shadowviews{std::count_if(m_viewports.begin(), m_viewports.end(), [](viewport_config const &View) { return View.shadow; })}; + return gfx::shadow_fit::unavailable(false == Global.gfx_skippipeline, Viewport.shadow, static_cast(shadowviews)); +} + +bool vulkan14_renderer::shadow_fit(viewport_config const &Viewport, std::array &Splits, gfx::shadow_fit::areas &Areas) +{ + auto &ctx{*m_context}; + auto &cull{ctx.scene.cull}; + auto const *unavailable{Global.gfx_shadow_sdsm ? shadow_fit_unavailable(Viewport) : nullptr}; + if (Global.gfx_shadow_sdsm && (unavailable == nullptr)) + { + if (cull.shadowfit) + { + // takes in what came back + cull.shadowfit.latest(ctx.completed()); + } + else + { + unavailable = "no compute passes"; + } + } + return cull.shadowfit.fit(m_framestamp, m_colorpass.pass_camera, unavailable, Splits, Areas, m_shadowfitstate); +} + +void vulkan14_renderer::measure_shadow_fit(std::size_t const Viewport) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &cull{scene.cull}; + auto const &vp{m_viewports[Viewport]}; + if ((false == vp.main) || (false == Global.gfx_shadow_sdsm) || (shadow_fit_unavailable(vp) != nullptr) || (Viewport >= scene.targets.size()) || (false == bool(scene.targets[Viewport].depth))) + { + return; + } + if ((false == bool(cull.shadowfit)) && (false == cull.shadowfit_tried)) + { + cull.shadowfit_tried = true; + if (false == cull.shadowfit.init(ctx.device.handle(), ctx.pipelines, ctx.shaders, scene.pass_layout, ctx.memory, static_cast(ctx.frames.size()))) + { + ErrorLog("vulkan: the cascades of the sun shadows can't be fitted to the depth of the view (gfx.shadow.sdsm)"); + cull.shadowfit.destroy(ctx.device.handle()); + } + } + if (false == bool(cull.shadowfit)) + { + return; + } + auto &targets{scene.targets[Viewport]}; + auto const commands{ctx.commands}; + vk14::barriers(commands, {vk14::use_of(*targets.depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, compute_stage, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT)}); + gfx::shadow_fit::view const view{m_colorpass.pass_camera.position(), m_colorpass.pass_camera.projection(), glm::mat3{m_colorpass.pass_camera.modelview()}, + gfx::shadow_fit::light_rotation(m_sunlight.direction), gfx::shadow_fit::fixed_splits().back()}; + ctx.passes.begin_pass("sun shadows fitted to the depth"); + cull.shadowfit.measure(commands, targets.depth->view(), ctx.sampling.clamp_nearest, static_cast(targets.extent.width), static_cast(targets.extent.height), view, m_framestamp, ctx.slot, ctx.timelinevalue + 1); + ctx.passes.end_pass(); +} diff --git a/rendering/vulkan14/vulkan14lamps.cpp b/rendering/vulkan14/vulkan14lamps.cpp new file mode 100644 index 00000000..0a36aeac --- /dev/null +++ b/rendering/vulkan14/vulkan14lamps.cpp @@ -0,0 +1,359 @@ +/* +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/. +*/ + +// the shadow maps of the lamps in the vulkan renderer (the stage P4 of the plan; opengl46_renderer::Render_local_shadows() and the +// rest): the lamp of the occupied cab, the lamps of the scenery and the headlights of the vehicles, which gfx::local_shadows picks, +// drawn by the shadow pass of the shared walk of the scene to the maps of vk14::local_shadow_maps + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14context.h" +#include "rendering/vulkan14/vulkan14commands.h" +#include "model/AnimModel.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "vehicle/DynObj.h" + +namespace +{ + +VkPipelineStageFlags2 const depth_stage{VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT}; +VkAccessFlags2 const depth_access{VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT}; +VkPipelineStageFlags2 const sampled_stage{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT}; + +} // namespace + +// the rendering of the map the maps set up last (begin(), begin_lamp(), begin_headlight()), and the pass of the walk into it +void vulkan14_renderer::begin_local_shadow_rendering() +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto const &target{scene.lamps.target()}; + auto const commands{ctx.commands}; + if ((target.map == nullptr) || (target.view == VK_NULL_HANDLE)) + { + return; + } + auto const use{vk14::use_of(*target.map, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)}; + vk14::barriers(commands, {use}); + VkRenderingAttachmentInfo depth{}; + depth.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; + depth.imageView = target.view; + depth.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; + depth.loadOp = (target.clear ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD); + depth.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + // the farthest depth + depth.clearValue.depthStencil = {0.f, 0}; + VkRenderingInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + rendering.renderArea = {{0, 0}, {target.size, target.size}}; + rendering.layerCount = 1; + rendering.viewMask = target.view_mask; + rendering.pDepthAttachment = &depth; + commands->begin_rendering(&rendering); + remember_rendering(rendering, {use}); + // the cube maps: one sided walls (of cabs, of buildings) face the lamp, they are to cast shadows as well. the maps of the + // headlights: the back faces, as in the shadows of the sun + start_draws(target.cube ? scene.cube_formats : scene.shadow_formats, static_cast(target.size), static_cast(target.size), vk14::scene_state::set_plain, (false == target.cube)); + m_draw->cullface = (false == target.cube); + walk_blending(false); + upload_scene_block(); + upload_light_header(); +} + +void vulkan14_renderer::end_local_shadow_rendering() +{ + if (m_draw->rendering) + { + m_context->commands->end_rendering(); + } + end_draws(); +} + +// the maps in the layout the shading reads them in: the ones made or drawn since +void vulkan14_renderer::ready_lamp_maps() +{ + auto &lamps{m_context->scene.lamps}; + std::vector uses; + for (auto *map : {lamps.cab_map(), lamps.lamp_maps(), lamps.headlight_maps()}) + { + if (map != nullptr) + { + uses.push_back(vk14::use_of(*map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT)); + } + } + if (false == uses.empty()) + { + vk14::barriers(m_context->commands, uses); + } +} + +void vulkan14_renderer::render_local_shadows() +{ + auto &lamps{m_context->scene.lamps}; + // only the light buffers with the lamps read the maps + auto const *vehicle{Global.gfx_lights_clustered ? m_locallights.cab_lamp_vehicle() : nullptr}; + if (false == lamps.update_wanted(vehicle, (vehicle != nullptr ? vehicle->mdKabina : nullptr), m_locallights.cab_lamp_local(), m_framestamp)) + { + return; + } + m_context->passes.begin_pass("lamp shadows"); + // the cab drawn by the shadow pass, with the programs of the cube map. the camera is put at the vehicle, which Render_cab() + // places the cab relative to, and the modelview takes the cab model to the space of the map + m_renderpass.draw_mode = rendermode::shadows; + m_renderpass.pass_camera.position() = vehicle->GetPosition(); + m_renderpass.draw_stats = {}; + OpenGLMatrices.mode(GL_MODELVIEW); + OpenGLMatrices.load_matrix(glm::mat4{glm::translate(glm::dmat4{1.0}, -glm::dvec3{lamps.lamp()}) * glm::inverse(vehicle->mMatrix)}); + lamps.begin(); + begin_local_shadow_rendering(); + m_cubeshadows = true; + Render_cab(vehicle, 0.0f, false); + m_cubeshadows = false; + end_local_shadow_rendering(); + lamps.end(static_cast(m_renderpass.draw_stats.drawcalls)); + m_renderpass = m_colorpass; + m_context->passes.end_pass(); +} + +void vulkan14_renderer::render_lamp_shadows() +{ + auto &lamps{m_context->scene.lamps}; + m_lampscenerydrawn = 0; + auto const &slots{lamps.lamps()}; + if (slots.empty() || (false == Global.gfx_lights_clustered)) + { + return; + } + // vehicles near the camera cast the shadows of the lamps, and the occupied cab + lamps.lamp_vehicles(Global.pCamera.Pos); + auto const vehiclesrange{static_cast(Global.gfx_lights_shadows_vehicles_range)}; + auto *cab{vehiclesrange > 0.0 ? lamps.shadow_cab() : nullptr}; + + auto timing{false}; + for (auto const idx : slots) + { + auto const &slot{lamps.slot(idx)}; + if (lamps.scenery_wanted(idx)) + { + if (false == timing) + { + m_context->passes.begin_pass("scenery lamp shadows"); + lamps.lamps_timer_begin(); + timing = true; + } + render_lamp_shadow(idx, false, nullptr); + } + if (false == slot.scenery) + { + continue; + } + // the vehicles by the lamp, within its reach + auto const &slotvehicles{lamps.slot_vehicles(slot)}; + auto *slotcab{(cab != nullptr) && (glm::length(cab->GetPosition() - slot.position) <= slot.range + cab->radius()) ? cab : nullptr}; + if (slotvehicles.empty() && (slotcab == nullptr)) + { + lamps.no_vehicles(idx); + continue; + } + if (false == lamps.vehicles_wanted(idx, m_framestamp)) + { + continue; + } + if (false == timing) + { + m_context->passes.begin_pass("scenery lamp shadows"); + lamps.lamps_timer_begin(); + timing = true; + } + render_lamp_shadow(idx, true, slotcab); + } + if (timing) + { + lamps.lamps_timer_end(); + m_context->passes.end_pass(); + } +} + +void vulkan14_renderer::render_lamp_shadow(std::size_t const Slot, bool const Vehicles, TDynamicObject *Cab) +{ + auto &lamps{m_context->scene.lamps}; + auto const &slot{lamps.slot(Slot)}; + // the shadow pass from the lamp, in world axes: the camera of the pass is the lamp, its frustum the box of its reach. the + // distances which pick the details of the models are measured from the real camera, as in the shadows of the sun + m_renderpass = m_colorpass; + m_renderpass.draw_mode = rendermode::shadows; + m_renderpass.draw_range = slot.range; + m_renderpass.draw_stats = {}; + auto &camera{m_renderpass.pass_camera}; + camera.position() = slot.position; + camera.modelview() = glm::mat4{glm::translate(glm::dmat4{1.0}, -slot.position)}; + auto const reach{slot.range}; + camera.projection() = glm::ortho(-reach, reach, -reach, reach, -reach, reach); + camera.update_frustum(); + OpenGLMatrices.mode(GL_MODELVIEW); + OpenGLMatrices.load_matrix(glm::mat4{1.f}); + + lamps.begin_lamp(Slot, Vehicles, m_instancerecordsversion, m_framestamp); + begin_local_shadow_rendering(); + m_cubeshadows = true; + if (Vehicles) + { + for (auto *vehicle : lamps.slot_vehicles()) + { + Render(vehicle); + } + if (Cab != nullptr) + { + Render_cab(Cab, 0.0f, false); + } + } + else + { + m_cubescenery = true; + m_cubeskipinstance = static_cast(slot.owner); + // the instances culled on the gpu, each scenery part drawn in a frame with a culling of its own + m_lightshadowcull = light_shadow_cull(m_context->scene.cull.lamps, m_lampscenerydrawn++, Global.gfx_gpucull_shadows_lamps); + Render(simulation::Region); + m_lightshadowcull = nullptr; + m_cubeskipinstance = nullptr; + m_cubescenery = false; + } + m_cubeshadows = false; + end_local_shadow_rendering(); + lamps.end_lamp(static_cast(m_renderpass.draw_stats.drawcalls)); + m_renderpass = m_colorpass; +} + +void vulkan14_renderer::render_headlight_shadows() +{ + auto &lamps{m_context->scene.lamps}; + // the headlights which shine, the most important first (in the order of update_lights()), whose beam may be seen within the + // reach of the shadows + auto const &picked{lamps.pick_headlights(simulation::Lights, m_colorpass.pass_camera)}; + lamps.update_headlights(picked, m_framestamp); + auto const &slots{lamps.headlights()}; + if (slots.empty()) + { + return; + } + // the occupied cab, as in render_lamp_shadows() + auto *cab{lamps.shadow_cab()}; + auto timing{false}; + for (auto const idx : slots) + { + auto const &slot{lamps.headlight(idx)}; + light_array::light_record const *light{nullptr}; + for (auto const *candidate : picked) + { + if ((candidate->owner == slot.owner) && (candidate->index == slot.index)) + { + light = candidate; + break; + } + } + if (light == nullptr) + { + continue; + } + glm::dvec3 projector; + glm::mat3 rotation; + glm::mat4 projection; + float nearplane, fieldofview; + headlight_projector(*light, projector, rotation, projection, nearplane, fieldofview); + // a vehicle on the move needs its map anew, the things around it and the vehicles passing by only now and then + auto const moved{(glm::length(projector - slot.projector) > 0.01) || (glm::dot(light->direction, slot.direction) < 0.99999f)}; + if (false == lamps.headlight_wanted(idx, moved, m_framestamp)) + { + continue; + } + if (false == timing) + { + m_context->passes.begin_pass("headlight shadows"); + lamps.headlights_timer_begin(); + timing = true; + } + render_headlight_shadow(idx, *light, cab); + } + if (timing) + { + lamps.headlights_timer_end(); + m_context->passes.end_pass(); + } +} + +void vulkan14_renderer::render_headlight_shadow(std::size_t const Slot, light_array::light_record const &Light, TDynamicObject *Cab) +{ + auto &lamps{m_context->scene.lamps}; + glm::dvec3 projector; + glm::mat3 rotation; + glm::mat4 projection; + float nearplane, fieldofview; + headlight_projector(Light, projector, rotation, projection, nearplane, fieldofview); + auto const reach{nearplane + Global.gfx_lights_shadows_headlights_range}; + // the shadow pass from the projector of the beam, with its projection. the distances which pick the details of the models + // are measured from the real camera, as in the shadows of the sun + m_renderpass = m_colorpass; + m_renderpass.draw_mode = rendermode::shadows; + m_renderpass.draw_range = reach; + m_renderpass.draw_stats = {}; + auto &camera{m_renderpass.pass_camera}; + camera.position() = projector; + camera.modelview() = glm::mat4{glm::dmat4{glm::dmat3{rotation}} * glm::translate(glm::dmat4{1.0}, -projector)}; + camera.projection() = projection; + // the projection goes on to the infinity, what the shadows are cast by ends at their reach + camera.update_frustum(glm::perspective(fieldofview, 1.f, nearplane, reach)); + setup_matrices(); + auto const colorprojection{scene_ubs.projection}; + scene_ubs.projection = projection; + + lamps.begin_headlight(Slot, projector, rotation, projection, Light.direction, m_framestamp); + begin_local_shadow_rendering(); + m_shadowskipvehicle = Light.owner; + // the instances culled on the gpu, each slot with a culling of its own + m_lightshadowcull = light_shadow_cull(m_context->scene.cull.headlights, Slot, Global.gfx_gpucull_shadows_headlights); + Render(simulation::Region); + m_lightshadowcull = nullptr; + if ((Cab != nullptr) && (Cab != Light.owner) && (glm::length(Cab->GetPosition() - projector) <= reach + Cab->radius())) + { + Render_cab(Cab, 0.0f, false); + } + m_shadowskipvehicle = nullptr; + end_local_shadow_rendering(); + lamps.end_headlight(static_cast(m_renderpass.draw_stats.drawcalls)); + scene_ubs.projection = colorprojection; + m_renderpass = m_colorpass; +} + +vk14::instance_cull *vulkan14_renderer::light_shadow_cull(std::vector> &Culls, std::size_t const Index, bool const Enabled) +{ + auto &ctx{*m_context}; + auto &cull{ctx.scene.cull}; + // the same conditions as those of the other passes in walk_region_begin() + if ((Global.gfx_gpucull == 0) || (false == Enabled) || (m_current_viewport == nullptr) || (false == m_current_viewport->main) || (false == cull.ready)) + { + return nullptr; + } + if (Culls.size() <= Index) + { + Culls.resize(Index + 1); + } + if (!Culls[Index]) + { + // the buffers of its own, the records and the kernels of the main view + auto culling{std::make_unique()}; + vk14::cull_environment environment; + if (false == culling->init(environment, ctx.device.handle(), ctx.pipelines, ctx.shaders, ctx.scene.pass_layout, &cull.main)) + { + culling->destroy(ctx.device.handle()); + return nullptr; + } + Culls[Index] = std::move(culling); + } + return Culls[Index].get(); +} diff --git a/rendering/vulkan14/vulkan14memory.cpp b/rendering/vulkan14/vulkan14memory.cpp index 2ee82dea..5c0a7315 100644 --- a/rendering/vulkan14/vulkan14memory.cpp +++ b/rendering/vulkan14/vulkan14memory.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/vulkan14/vulkan14memory.h" +#include "rendering/vulkan14/vulkan14commands.h" #include "rendering/vulkan14/vulkan14instance.h" #include "rendering/vulkan14/vulkan14device.h" @@ -278,14 +279,28 @@ void image::destroy() state = image_state{}; } -void transition(VkCommandBuffer Commands, image &Image, VkImageLayout Layout, VkPipelineStageFlags2 Stage, VkAccessFlags2 Access) +void transition(command_stream *Commands, image &Image, VkImageLayout Layout, VkPipelineStageFlags2 Stage, VkAccessFlags2 Access) { auto const writes{VK_ACCESS_2_SHADER_WRITE_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT | VK_ACCESS_2_HOST_WRITE_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT}; auto const &from{Image.state}; - // reads after reads in the same layout need nothing + // reads after reads in the same layout need nothing, unless they are in stages the barrier after the last write didn't wait + // for: those wait for the stages which read the image already (which came after the write) if ((from.layout == Layout) && ((from.access & writes) == 0) && ((Access & writes) == 0)) { + if ((Stage & ~from.stage) != 0) + { + VkMemoryBarrier2 chain{}; + chain.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; + chain.srcStageMask = (from.stage != VK_PIPELINE_STAGE_2_NONE ? from.stage : VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT); + chain.dstStageMask = Stage; + chain.dstAccessMask = Access; + VkDependencyInfo dependency{}; + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency.memoryBarrierCount = 1; + dependency.pMemoryBarriers = &chain; + Commands->pipeline_barrier2(&dependency); + } Image.state.stage |= Stage; Image.state.access |= Access; return; @@ -307,7 +322,7 @@ void transition(VkCommandBuffer Commands, image &Image, VkImageLayout Layout, Vk dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.imageMemoryBarrierCount = 1; dependency.pImageMemoryBarriers = &barrier; - vkCmdPipelineBarrier2(Commands, &dependency); + Commands->pipeline_barrier2(&dependency); Image.state = image_state{Layout, Stage, Access}; } diff --git a/rendering/vulkan14/vulkan14memory.h b/rendering/vulkan14/vulkan14memory.h index d97b2874..607ed917 100644 --- a/rendering/vulkan14/vulkan14memory.h +++ b/rendering/vulkan14/vulkan14memory.h @@ -177,7 +177,7 @@ class image // a barrier of an image from its state to the given one, the state of the image updated. nothing when the image already is // in the layout and was only read by the same stage -void transition(VkCommandBuffer Commands, image &Image, VkImageLayout Layout, VkPipelineStageFlags2 Stage, VkAccessFlags2 Access); +void transition(command_stream *Commands, image &Image, VkImageLayout Layout, VkPipelineStageFlags2 Stage, VkAccessFlags2 Access); // objects destroyed once the gpu is done with the frame which used them last class deletion_queue diff --git a/rendering/vulkan14/vulkan14particles.cpp b/rendering/vulkan14/vulkan14particles.cpp new file mode 100644 index 00000000..11cfd65a --- /dev/null +++ b/rendering/vulkan14/vulkan14particles.cpp @@ -0,0 +1,342 @@ +/* +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/vulkan14/vulkan14particles.h" +#include "rendering/vulkan14/vulkan14commands.h" +#include "rendering/vulkan14/vulkan14context.h" +#include "utilities/Logs.h" + +#include + +namespace vk14 +{ + +namespace +{ + +// the push constants of particles_smoke_simulate.comp (std430) +struct simulate_constants +{ + std::uint32_t places; + std::uint32_t pad0[3]; + glm::vec4 step; + glm::vec4 wind; + std::uint32_t seed; + std::uint32_t pad1[3]; +}; +static_assert(offsetof(simulate_constants, step) == 16 && offsetof(simulate_constants, seed) == 48, "the layout of the constants of particles_smoke_simulate.comp"); + +// the push constants of particles_smoke_prepare.comp (std430) +struct prepare_constants +{ + std::uint32_t places; + std::uint32_t pad0[3]; + glm::vec3 origin; + float pad1; + glm::mat4 view; + glm::vec4 options; + glm::vec4 planes[5]; +}; +static_assert(offsetof(prepare_constants, origin) == 16 && offsetof(prepare_constants, view) == 32 && offsetof(prepare_constants, options) == 96 && offsetof(prepare_constants, planes) == 112, + "the layout of the constants of particles_smoke_prepare.comp"); + +// the block of particles_precipitation.comp (std140) +struct precipitation_block +{ + glm::uvec4 counts; + glm::vec4 offsets[3]; + glm::vec4 sizes; + glm::vec4 velocity; + glm::vec4 timing; + glm::mat4 view; + glm::vec4 options; + glm::vec4 planes[5]; +}; +static_assert(offsetof(precipitation_block, view) == 112 && offsetof(precipitation_block, planes) == 192 && sizeof(precipitation_block) == 272, "the layout of the block of particles_precipitation.comp"); + +std::uint32_t groups(std::uint32_t const Count) +{ + return (Count + gl::PARTICLE_GROUP_SIZE - 1) / gl::PARTICLE_GROUP_SIZE; +} + +VkBufferUsageFlags constexpr storage_usage{VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT}; + +} // namespace + +gpu_particles::~gpu_particles() +{ + destroy(); +} + +void gpu_particles::init(VkDevice const Device, pipeline_library &Pipelines, shader_library &Shaders, VkDescriptorSetLayout const Passlayout, allocator &Memory, uploader &Uploads, deletion_queue &Deletions, + frame_ring &Ring, VkDeviceSize const Alignment, std::uint32_t const Frames, host Host) +{ + m_device = Device; + m_pipelines = &Pipelines; + m_shaders = &Shaders; + m_passlayout = Passlayout; + m_memory = &Memory; + m_uploads = &Uploads; + m_deletions = &Deletions; + m_ring = &Ring; + m_alignment = std::max(Alignment, 16); + m_frames = Frames; + m_host = std::move(Host); +} + +void gpu_particles::destroy() +{ + if (m_device == VK_NULL_HANDLE) + { + return; + } + // the device is idle: everything goes at once + m_deletions = nullptr; + m_smokesimulate.destroy(m_device); + m_smokeprepare.destroy(m_device); + m_precipitationprepare.destroy(m_device); + m_smokedraw.reset(); + m_precipitationdraw.reset(); + for (auto *buffer : {&m_particles, &m_sources, &m_owners, &m_smokeoutput, &m_smokecommand, &m_precipitationoutput, &m_precipitationcommand}) + { + buffer->reset(); + } + m_smokereadback.destroy(); + m_precipitationreadback.destroy(); + m_device = VK_NULL_HANDLE; +} + +std::shared_ptr gpu_particles::make_buffer(VkDeviceSize const Size, VkBufferUsageFlags const Usage, char const *Name) +{ + auto result{std::make_shared()}; + if (false == result->create(*m_memory, std::max(Size, 16), Usage, memory_kind::device, Name)) + { + ErrorLog(std::string("particles: no buffer for the ") + Name); + return nullptr; + } + return result; +} + +void gpu_particles::let_go(std::shared_ptr &Buffer) +{ + if ((Buffer != nullptr) && (m_deletions != nullptr)) + { + // the frames in flight may still read it + m_deletions->push(m_uploads->current_value(), [old = std::move(Buffer)]() {}); + } + Buffer.reset(); +} + +bool gpu_particles::gpu_init() +{ + if ((m_device == VK_NULL_HANDLE) || (false == bool(m_host.compute)) || (false == bool(m_host.draw))) + { + return false; + } + try + { + auto kernel = [&](compute_kernel &Kernel, char const *Shader, char const *Name) { + if (false == Kernel.create(m_device, *m_pipelines, m_shaders->get(Shader), m_passlayout, {}, Name)) + { + throw gfx::shader_exception(std::string("no compute pipeline of ") + Name); + } + }; + kernel(m_smokesimulate, "particles_smoke_simulate.comp", "smoke simulation"); + kernel(m_smokeprepare, "particles_smoke_prepare.comp", "smoke"); + kernel(m_precipitationprepare, "particles_precipitation.comp", "precipitation"); + auto program = [&](char const *Vertex, char const *Fragment) { + auto result{std::make_shared()}; + result->vertex = m_shaders->get(Vertex); + result->fragment = m_shaders->get(Fragment); + result->fragment_name = Fragment; + result->vertex_name = Vertex; + result->vertices = vertex_kind::particle_instance; + result->name = std::string(Vertex) + " + " + Fragment; + return result; + }; + m_smokedraw = program("particles_smoke.vert", "particles_smoke.frag"); + m_precipitationdraw = program("particles_precipitation.vert", "particles_precipitation.frag"); + } + catch (gfx::shader_exception const &e) + { + ErrorLog("particles: can't use the programs of the particles of the gpu, the smoke stays on the cpu: " + std::string(e.what())); + return false; + } + auto const commandusage{VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT}; + m_smokecommand = make_buffer(4 * sizeof(std::uint32_t), commandusage, "command of the smoke"); + m_precipitationcommand = make_buffer(4 * sizeof(std::uint32_t), commandusage, "command of the precipitation"); + if ((m_smokecommand == nullptr) || (m_precipitationcommand == nullptr) || (false == m_smokereadback.create(*m_memory, 4 * sizeof(std::uint32_t), m_frames, "smoke read back")) || + (false == m_precipitationreadback.create(*m_memory, 4 * sizeof(std::uint32_t), m_frames, "precipitation read back"))) + { + ErrorLog("particles: no buffers of the commands of the particles of the gpu, the smoke stays on the cpu"); + return false; + } + return true; +} + +void gpu_particles::gpu_clear() +{ + for (auto *buffer : {&m_particles, &m_sources, &m_owners, &m_smokeoutput, &m_precipitationoutput}) + { + let_go(*buffer); + } +} + +void gpu_particles::gpu_layout(std::uint32_t const Places, std::uint32_t const Kept, std::vector const &Owners, std::size_t const Sources) +{ + auto const commands{m_host.compute()}; + auto particles{make_buffer(static_cast(Places) * sizeof(gl::particle_smoke), storage_usage, "smoke particles")}; + if (particles == nullptr) + { + return; + } + // the particles of the places kept go over to the new storage, the new places are dead + memory_barrier(commands, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_CLEAR_BIT, VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT); + commands->fill_buffer(particles->handle(), 0, VK_WHOLE_SIZE, 0); + if ((m_particles != nullptr) && (Kept > 0)) + { + memory_barrier(commands, VK_PIPELINE_STAGE_2_CLEAR_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT); + VkBufferCopy2 region{}; + region.sType = VK_STRUCTURE_TYPE_BUFFER_COPY_2; + region.size = std::min(static_cast(Kept) * sizeof(gl::particle_smoke), m_particles->size()); + VkCopyBufferInfo2 copy{}; + copy.sType = VK_STRUCTURE_TYPE_COPY_BUFFER_INFO_2; + copy.srcBuffer = m_particles->handle(); + copy.dstBuffer = particles->handle(); + copy.regionCount = 1; + copy.pRegions = ®ion; + commands->copy_buffer2(©); + } + memory_barrier(commands, VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_CLEAR_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT); + let_go(m_particles); + m_particles = std::move(particles); + + let_go(m_owners); + let_go(m_smokeoutput); + let_go(m_sources); + m_owners = make_buffer(static_cast(Places) * sizeof(std::uint32_t), storage_usage, "smoke owners"); + m_smokeoutput = make_buffer(static_cast(Places) * sizeof(gl::particle_draw), storage_usage | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, "smoke draws"); + m_sources = make_buffer(static_cast(std::max(Sources, 1)) * sizeof(gl::particle_smoke_source), storage_usage, "smoke sources"); + if (m_owners != nullptr) + { + // before the commands of the frame, the kernels of the frame read it + m_uploads->upload_buffer(m_owners, Owners.data(), static_cast(Owners.size() * sizeof(std::uint32_t))); + } +} + +void gpu_particles::gpu_simulate(smoke_simulation const &Simulation, std::vector const &Records) +{ + if ((m_particles == nullptr) || (m_sources == nullptr) || (m_owners == nullptr)) + { + return; + } + if (false == Records.empty()) + { + m_uploads->upload_buffer(m_sources, Records.data(), static_cast(Records.size() * sizeof(gl::particle_smoke_source))); + } + auto const commands{m_host.compute()}; + simulate_constants constants{}; + constants.places = Simulation.places; + constants.step = Simulation.step; + constants.wind = Simulation.wind; + constants.seed = Simulation.seed; + m_host.bind(m_smokesimulate); + m_smokesimulate.dispatch(commands, + {buffer_descriptor(m_particles->handle(), 0, VK_WHOLE_SIZE), buffer_descriptor(m_sources->handle(), 0, VK_WHOLE_SIZE), buffer_descriptor(m_owners->handle(), 0, VK_WHOLE_SIZE)}, + constants, groups(Simulation.places)); + compute_barrier(commands); +} + +void gpu_particles::reset_command(command_stream *const Commands, buffer const &Command) +{ + std::uint32_t const command[4]{4, 0, 0, 0}; + update_buffer(Commands, Command, command, sizeof(command), 0); +} + +void gpu_particles::collect(readback_ring &Readback, std::uint64_t const Completed, std::size_t &Drawn) +{ + std::uint32_t command[4]; + if (Readback.pop(Completed, command, sizeof(command))) + { + Drawn = command[1]; + } +} + +void gpu_particles::gpu_render_smoke(smoke_draw const &Draw, std::size_t &Drawn) +{ + collect(m_smokereadback, m_host.completed(), Drawn); + if ((m_particles == nullptr) || (m_sources == nullptr) || (m_owners == nullptr) || (m_smokeoutput == nullptr)) + { + return; + } + auto const commands{m_host.compute()}; + reset_command(commands, *m_smokecommand); + prepare_constants constants{}; + constants.places = Draw.places; + constants.origin = Draw.origin; + constants.view = Draw.view; + constants.options = Draw.options; + std::copy(Draw.planes.begin(), Draw.planes.end(), constants.planes); + m_host.bind(m_smokeprepare); + m_smokeprepare.dispatch(commands, + {buffer_descriptor(m_particles->handle(), 0, VK_WHOLE_SIZE), buffer_descriptor(m_sources->handle(), 0, VK_WHOLE_SIZE), buffer_descriptor(m_owners->handle(), 0, VK_WHOLE_SIZE), + buffer_descriptor(m_smokecommand->handle(), 0, VK_WHOLE_SIZE), buffer_descriptor(m_smokeoutput->handle(), 0, VK_WHOLE_SIZE)}, + constants, groups(Draw.places)); + compute_barrier(commands); + // the count of the instances is what the draw takes, read back once the gpu is done with the frame + m_smokereadback.push(commands, m_host.slot(), m_host.value(), m_smokecommand->handle(), 0, 4 * sizeof(std::uint32_t)); + m_host.draw(*m_smokedraw, m_smokeoutput->handle(), m_smokecommand->handle(), Draw.soft, glm::vec4{0.f}); +} + +void gpu_particles::gpu_collect_precipitation(std::size_t &Drawn) +{ + collect(m_precipitationreadback, m_host.completed(), Drawn); +} + +void gpu_particles::gpu_precipitation_storage(std::uint32_t const Capacity) +{ + let_go(m_precipitationoutput); + m_precipitationoutput = make_buffer(static_cast(Capacity) * sizeof(gl::particle_draw), storage_usage | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, "precipitation draws"); +} + +void gpu_particles::gpu_render_precipitation(precipitation_draw const &Draw) +{ + if (m_precipitationoutput == nullptr) + { + return; + } + precipitation_block block{}; + block.counts = Draw.counts; + std::copy(Draw.offsets.begin(), Draw.offsets.end(), block.offsets); + block.sizes = Draw.sizes; + block.velocity = Draw.velocity; + block.timing = Draw.timing; + block.view = Draw.view; + block.options = Draw.options; + std::copy(Draw.planes.begin(), Draw.planes.end(), block.planes); + auto const slice{m_ring->allocate(sizeof(block), m_alignment)}; + if (false == bool(slice)) + { + return; + } + std::memcpy(slice.data, &block, sizeof(block)); + auto const commands{m_host.compute()}; + reset_command(commands, *m_precipitationcommand); + m_host.bind(m_precipitationprepare); + m_precipitationprepare.run(commands, + {buffer_descriptor(m_precipitationcommand->handle(), 0, VK_WHOLE_SIZE), buffer_descriptor(m_precipitationoutput->handle(), 0, VK_WHOLE_SIZE), + buffer_descriptor(slice.buffer, slice.offset, sizeof(block))}, + groups(Draw.count)); + compute_barrier(commands); + m_precipitationreadback.push(commands, m_host.slot(), m_host.value(), m_precipitationcommand->handle(), 0, 4 * sizeof(std::uint32_t)); + m_host.draw(*m_precipitationdraw, m_precipitationoutput->handle(), m_precipitationcommand->handle(), Draw.soft, Draw.pixel); +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14particles.h b/rendering/vulkan14/vulkan14particles.h new file mode 100644 index 00000000..a0291d23 --- /dev/null +++ b/rendering/vulkan14/vulkan14particles.h @@ -0,0 +1,105 @@ +/* +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/. +*/ + +// the particles of the gpu in the vulkan renderer (the stage P6 of the plan, opengl46_gpu_particles): the storage buffers, the +// compute kernels and the indirect draws of gfx::gpu_particles. the compute passes run between two parts of the rendering of +// the colour pass (the host ends it and begins it again); the instances counted by a pass are read back once the gpu is done +// with the frame, as the opengl renderer reads them a few frames later + +#pragma once + +#include "rendering/common/gpuparticles.h" +#include "rendering/vulkan14/vulkan14compute.h" +#include "rendering/vulkan14/vulkan14texture.h" + +#include +#include + +namespace vk14 +{ + +struct scene_program; + +class gpu_particles : public gfx::gpu_particles +{ + public: + // what the renderer does for the particles + struct host + { + // the rendering under way ended for the compute passes; the commands of the frame + std::function compute; + // the pass set of the pass bound for the kernel + std::function bind; + // the rendering begun again and the draw of the instances recorded: Program with the blending of the scene, the instances + // from Output, the command from Command, Data0 and Data1 in the push constants (data[0], data[1]) + std::function draw; + // the slot of the frame in flight, the value of the timeline the gpu reaches when it's done with the frame, the one it reached + std::function slot; + std::function value; + std::function completed; + }; + + gpu_particles() = default; + ~gpu_particles() override; + + // called once, before anything is made. Frames: the frames in flight + void init(VkDevice Device, pipeline_library &Pipelines, shader_library &Shaders, VkDescriptorSetLayout Passlayout, allocator &Memory, uploader &Uploads, deletion_queue &Deletions, frame_ring &Ring, + VkDeviceSize Alignment, std::uint32_t Frames, host Host); + // everything goes (the device is idle) + void destroy(); + + protected: + bool gpu_init() override; + void gpu_layout(std::uint32_t const Places, std::uint32_t const Kept, std::vector const &Owners, std::size_t const Sources) override; + void gpu_simulate(smoke_simulation const &Simulation, std::vector const &Records) override; + void gpu_render_smoke(smoke_draw const &Draw, std::size_t &Drawn) override; + void gpu_collect_precipitation(std::size_t &Drawn) override; + void gpu_precipitation_storage(std::uint32_t const Capacity) override; + void gpu_render_precipitation(precipitation_draw const &Draw) override; + void gpu_clear() override; + + private: + std::shared_ptr make_buffer(VkDeviceSize const Size, VkBufferUsageFlags const Usage, char const *Name); + void let_go(std::shared_ptr &Buffer); + // the command of a draw reset before the compute pass counts its instances + void reset_command(command_stream *Commands, buffer const &Command); + static void collect(readback_ring &Readback, std::uint64_t Completed, std::size_t &Drawn); + + VkDevice m_device{VK_NULL_HANDLE}; + pipeline_library *m_pipelines{nullptr}; + shader_library *m_shaders{nullptr}; + VkDescriptorSetLayout m_passlayout{VK_NULL_HANDLE}; + allocator *m_memory{nullptr}; + uploader *m_uploads{nullptr}; + deletion_queue *m_deletions{nullptr}; + frame_ring *m_ring{nullptr}; + VkDeviceSize m_alignment{256}; + std::uint32_t m_frames{0}; + host m_host; + + compute_kernel m_smokesimulate; + compute_kernel m_smokeprepare; + compute_kernel m_precipitationprepare; + std::shared_ptr m_smokedraw; + std::shared_ptr m_precipitationdraw; + + // smoke + std::shared_ptr m_particles; + std::shared_ptr m_sources; + std::shared_ptr m_owners; + std::shared_ptr m_smokeoutput; + std::shared_ptr m_smokecommand; + readback_ring m_smokereadback; + // rain and snow + std::shared_ptr m_precipitationoutput; + std::shared_ptr m_precipitationcommand; + readback_ring m_precipitationreadback; +}; + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14passes.cpp b/rendering/vulkan14/vulkan14passes.cpp index 316ab101..1c0f884c 100644 --- a/rendering/vulkan14/vulkan14passes.cpp +++ b/rendering/vulkan14/vulkan14passes.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/vulkan14/vulkan14passes.h" +#include "rendering/vulkan14/vulkan14commands.h" #include "rendering/vulkan14/vulkan14device.h" namespace vk14 @@ -19,7 +20,7 @@ image_use use_of(image &Image, VkImageLayout Layout, VkPipelineStageFlags2 Stage return image_use{Image.handle(), Image.aspect(), &Image.state, Layout, Stage, Access}; } -void barriers(VkCommandBuffer Commands, std::vector const &Uses) +void barriers(command_stream *Commands, std::vector const &Uses) { auto const writes{VK_ACCESS_2_SHADER_WRITE_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT | VK_ACCESS_2_HOST_WRITE_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT}; @@ -31,9 +32,23 @@ void barriers(VkCommandBuffer Commands, std::vector const &Uses) continue; } auto &state{*use.state}; - // reads after reads in the same layout need nothing + // reads after reads in the same layout need nothing, unless they are in stages the barrier after the last write didn't + // wait for: those wait for the stages which read the image already (which came after the write) if ((state.layout == use.layout) && ((state.access & writes) == 0) && ((use.access & writes) == 0)) { + if ((use.stage & ~state.stage) != 0) + { + VkMemoryBarrier2 chain{}; + chain.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; + chain.srcStageMask = (state.stage != VK_PIPELINE_STAGE_2_NONE ? state.stage : VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT); + chain.dstStageMask = use.stage; + chain.dstAccessMask = use.access; + VkDependencyInfo dependency{}; + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency.memoryBarrierCount = 1; + dependency.pMemoryBarriers = &chain; + Commands->pipeline_barrier2(&dependency); + } state.stage |= use.stage; state.access |= use.access; continue; @@ -61,7 +76,7 @@ void barriers(VkCommandBuffer Commands, std::vector const &Uses) dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.imageMemoryBarrierCount = static_cast(list.size()); dependency.pImageMemoryBarriers = list.data(); - vkCmdPipelineBarrier2(Commands, &dependency); + Commands->pipeline_barrier2(&dependency); } pass_list::~pass_list() @@ -114,7 +129,7 @@ void pass_list::destroy() m_device = VK_NULL_HANDLE; } -void pass_list::begin_frame(VkCommandBuffer Commands, std::uint32_t Frame, std::uint64_t Number) +void pass_list::begin_frame(command_stream *Commands, std::uint32_t Frame, std::uint64_t Number) { m_commands = Commands; m_frame = Frame; @@ -124,7 +139,7 @@ void pass_list::begin_frame(VkCommandBuffer Commands, std::uint32_t Frame, std:: frame.used = 0; frame.number = Number; frame.recorded = true; - vkCmdResetQueryPool(Commands, frame.pool, 0, max_passes * 2); + Commands->reset_query_pool(frame.pool, 0, max_passes * 2); } void pass_list::begin_pass(char const *Name, std::vector const &Uses) @@ -139,7 +154,7 @@ void pass_list::begin_pass(char const *Name, std::vector const &Uses) // (no timestamps on a queue without valid bits; the device is chosen with them, this is for safety) if ((frame.used < max_passes) && (m_mask != 0)) { - vkCmdWriteTimestamp2(m_commands, VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, frame.pool, frame.used * 2); + m_commands->write_timestamp2(VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, frame.pool, frame.used * 2); frame.names.emplace_back(Name); m_open = static_cast(frame.used); ++frame.used; @@ -158,7 +173,7 @@ void pass_list::end_pass() } if (m_open < static_cast(max_passes)) { - vkCmdWriteTimestamp2(m_commands, VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT, m_frames[m_frame].pool, static_cast(m_open) * 2 + 1); + m_commands->write_timestamp2(VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT, m_frames[m_frame].pool, static_cast(m_open) * 2 + 1); } end_label(m_commands); m_open = -1; @@ -167,7 +182,7 @@ void pass_list::end_pass() void pass_list::end_frame() { end_pass(); - m_commands = VK_NULL_HANDLE; + m_commands = nullptr; } void pass_list::collect(std::uint32_t Frame) diff --git a/rendering/vulkan14/vulkan14passes.h b/rendering/vulkan14/vulkan14passes.h index 74fed9d1..a2b1b481 100644 --- a/rendering/vulkan14/vulkan14passes.h +++ b/rendering/vulkan14/vulkan14passes.h @@ -39,7 +39,7 @@ struct image_use image_use use_of(image &Image, VkImageLayout Layout, VkPipelineStageFlags2 Stage, VkAccessFlags2 Access); // the barriers which bring the images from their states to the uses, the states updated -void barriers(VkCommandBuffer Commands, std::vector const &Uses); +void barriers(command_stream *Commands, std::vector const &Uses); class pass_list { @@ -53,7 +53,7 @@ class pass_list void destroy(); // the frame Frame (index of the frame in flight) is recorded to Commands from now on; Number counts the frames - void begin_frame(VkCommandBuffer Commands, std::uint32_t Frame, std::uint64_t Number); + void begin_frame(command_stream *Commands, std::uint32_t Frame, std::uint64_t Number); // a pass: the barriers of its images, its label and the timestamp of its start void begin_pass(char const *Name, std::vector const &Uses = {}); void end_pass(); @@ -85,7 +85,7 @@ class pass_list float m_period{1.f}; // ns per tick std::uint64_t m_mask{~0ull}; // the valid bits of the timestamps std::vector m_frames; - VkCommandBuffer m_commands{VK_NULL_HANDLE}; + command_stream *m_commands{nullptr}; std::uint32_t m_frame{0}; int m_open{-1}; // averaged times of the parts, ms diff --git a/rendering/vulkan14/vulkan14renderer.cpp b/rendering/vulkan14/vulkan14renderer.cpp index 04c6ff11..65566e5f 100644 --- a/rendering/vulkan14/vulkan14renderer.cpp +++ b/rendering/vulkan14/vulkan14renderer.cpp @@ -37,6 +37,17 @@ float srgb_to_linear(float const Value) return (Value <= 0.04045f ? Value / 12.92f : std::pow((Value + 0.055f) / 1.055f, 2.4f)); } +// the threads of the renderer (gfx.threads), the calling one with them: 0, the cores less two +std::size_t render_threads() +{ + if (Global.gfx_threads > 0) + { + return static_cast(Global.gfx_threads); + } + auto const cores{static_cast(std::thread::hardware_concurrency())}; + return (cores > 3 ? cores - 2 : 1); +} + } // namespace bool vulkan14_renderer::context::create_frames(std::uint32_t const Count, std::string &Reason) @@ -69,25 +80,250 @@ bool vulkan14_renderer::context::create_frames(std::uint32_t const Count, std::s Reason = "no command pool"; return false; } - VkCommandBuffer buffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE}; VkCommandBufferAllocateInfo bufferinfo{}; bufferinfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; bufferinfo.commandPool = frames[i].pool; bufferinfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - bufferinfo.commandBufferCount = 2; - if (!VK14_VERIFY(vkAllocateCommandBuffers(handle, &bufferinfo, buffers))) + bufferinfo.commandBufferCount = 1; + if (!VK14_VERIFY(vkAllocateCommandBuffers(handle, &bufferinfo, &frames[i].uploads))) { Reason = "no command buffer"; return false; } - frames[i].uploads = buffers[0]; - frames[i].commands = buffers[1]; instance.set_object_name(handle, VK_OBJECT_TYPE_COMMAND_BUFFER, reinterpret_cast(frames[i].uploads), "uploads " + std::to_string(i)); - instance.set_object_name(handle, VK_OBJECT_TYPE_COMMAND_BUFFER, reinterpret_cast(frames[i].commands), "frame " + std::to_string(i)); + } + if (Global.gfx_vulkan_renderthread) + { + start_render_thread(); } return true; } +bool vulkan14_renderer::context::record(frame &Frame, bool const Uploads) +{ + auto const handle{device.handle()}; + auto const start{std::chrono::steady_clock::now()}; + Frame.upload_stream.finish(); + Frame.commands.finish(); + VkCommandBufferBeginInfo begin{}; + begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + if (Uploads) + { + VK14_VERIFY(vkBeginCommandBuffer(Frame.uploads, &begin)); + Frame.upload_stream.replay(Frame.uploads); + VK14_VERIFY(vkEndCommandBuffer(Frame.uploads)); + } + // the chunks split evenly between the workers, each with a command pool of its own (a pool is used by one thread at a time); + // which worker records a chunk doesn't matter: each chunk has a command buffer of its own, they go to the queue in their order + auto const chunks{Frame.commands.chunks()}; + auto const threads{std::max(1, std::min(render_threads(), chunks))}; + while (Frame.workers.size() < threads) + { + VkCommandPoolCreateInfo poolinfo{}; + poolinfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + poolinfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT; + poolinfo.queueFamilyIndex = device.families().graphics; + VkCommandPool pool{VK_NULL_HANDLE}; + if (!VK14_VERIFY(vkCreateCommandPool(handle, &poolinfo, nullptr, &pool))) + { + return false; + } + Frame.workers.emplace_back().pool = pool; + } + Frame.chunks.assign(chunks, VK_NULL_HANDLE); + workers.run(threads, threads, [&](std::size_t const Worker) { + auto &commands{Frame.workers[Worker]}; + for (auto chunk = chunks * Worker / threads; chunk < chunks * (Worker + 1) / threads; ++chunk) + { + if (commands.used == commands.buffers.size()) + { + VkCommandBufferAllocateInfo bufferinfo{}; + bufferinfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + bufferinfo.commandPool = commands.pool; + bufferinfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + bufferinfo.commandBufferCount = 1; + VkCommandBuffer buffer{VK_NULL_HANDLE}; + if (!VK14_VERIFY(vkAllocateCommandBuffers(handle, &bufferinfo, &buffer))) + { + return; + } + commands.buffers.push_back(buffer); + } + auto const buffer{commands.buffers[commands.used++]}; + VK14_VERIFY(vkBeginCommandBuffer(buffer, &begin)); + Frame.commands.replay(chunk, buffer); + VK14_VERIFY(vkEndCommandBuffer(buffer)); + Frame.chunks[chunk] = buffer; + } + }); + record_threads = threads; + record_time = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + record_calls = Frame.commands.calls(); + record_chunks = chunks; + return std::find(Frame.chunks.begin(), Frame.chunks.end(), VK_NULL_HANDLE) == Frame.chunks.end(); +} + +bool vulkan14_renderer::context::execute(render_job &Job, char const *&Failure) +{ + auto &frame{*Job.target}; + if (false == record(frame, Job.uploaded)) + { + Failure = "recording a frame"; + return false; + } + // the uploads first, in the same submit: their barriers make them done before the frame uses what they copied + std::vector commands; + if (Job.uploaded) + { + VkCommandBufferSubmitInfo info{}; + info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; + info.commandBuffer = frame.uploads; + commands.push_back(info); + } + // the chunks of the frame in their order: a rendering suspended at the end of one is resumed at the start of the next + for (auto const chunk : frame.chunks) + { + VkCommandBufferSubmitInfo info{}; + info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; + info.commandBuffer = chunk; + commands.push_back(info); + } + VkSubmitInfo2 submit{}; + submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2; + submit.waitSemaphoreInfoCount = static_cast(Job.waits.size()); + submit.pWaitSemaphoreInfos = Job.waits.data(); + submit.commandBufferInfoCount = static_cast(commands.size()); + submit.pCommandBufferInfos = commands.data(); + submit.signalSemaphoreInfoCount = static_cast(Job.signals.size()); + submit.pSignalSemaphoreInfos = Job.signals.data(); + if (!VK14_VERIFY(vkQueueSubmit2(device.graphics_queue(), 1, &submit, VK_NULL_HANDLE))) + { + Failure = "submitting a frame"; + return false; + } + if ((false == Job.windows.empty()) && (false == present(Job.windows))) + { + Failure = "presenting"; + return false; + } + return true; +} + +bool vulkan14_renderer::context::present(std::vector const &Windows) +{ + std::vector waits; + std::vector swapchains; + std::vector images; + std::vector results(Windows.size(), VK_SUCCESS); + for (auto *window : Windows) + { + waits.push_back(window->present_semaphore()); + swapchains.push_back(window->handle()); + images.push_back(window->image_index()); + } + VkPresentInfoKHR info{}; + info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + info.waitSemaphoreCount = static_cast(waits.size()); + info.pWaitSemaphores = waits.data(); + info.swapchainCount = static_cast(swapchains.size()); + info.pSwapchains = swapchains.data(); + info.pImageIndices = images.data(); + info.pResults = results.data(); + auto const result{vkQueuePresentKHR(device.graphics_queue(), &info)}; + for (std::size_t i = 0; i < Windows.size(); ++i) + { + Windows[i]->presented(results[i]); + } + if ((result == VK_ERROR_DEVICE_LOST) || (result == VK_ERROR_SURFACE_LOST_KHR)) + { + vk14::log_error(std::string("vkQueuePresentKHR failed with ") + vk14::result_string(result)); + return false; + } + return true; +} + +void vulkan14_renderer::context::start_render_thread() +{ + render_stop = false; + render_thread = std::thread([this]() { + std::unique_lock lock(render_mutex); + while (true) + { + render_wake.wait(lock, [this]() { return render_stop || render_queued; }); + if (false == render_queued) + { + // stopped, with nothing left to do + break; + } + lock.unlock(); + char const *failure{nullptr}; + auto done{false}; + try + { + done = execute(render_pending, failure); + } + catch (std::exception const &Error) + { + vk14::log_error(std::string("the render thread failed: ") + Error.what()); + failure = "recording a frame"; + } + catch (...) + { + vk14::log_error("the render thread failed"); + failure = "recording a frame"; + } + lock.lock(); + if ((false == done) && (render_failure == nullptr)) + { + render_failure = (failure != nullptr ? failure : "recording a frame"); + } + render_queued = false; + render_idle.notify_all(); + } + }); +} + +void vulkan14_renderer::context::stop_render_thread() +{ + if (false == render_thread.joinable()) + { + return; + } + { + std::lock_guard lock(render_mutex); + render_stop = true; + } + render_wake.notify_all(); + render_thread.join(); +} + +void vulkan14_renderer::context::hand_over(render_job &&Job) +{ + { + std::unique_lock lock(render_mutex); + render_idle.wait(lock, [this]() { return false == render_queued; }); + render_pending = std::move(Job); + render_queued = true; + } + render_wake.notify_one(); +} + +char const *vulkan14_renderer::context::drain() +{ + if (false == render_threaded()) + { + return nullptr; + } + auto const start{std::chrono::steady_clock::now()}; + std::unique_lock lock(render_mutex); + render_idle.wait(lock, [this]() { return false == render_queued; }); + render_wait = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + auto const failure{render_failure}; + render_failure = nullptr; + return failure; +} + // the parts of the renderer made once the device is there bool vulkan14_renderer::context::create_core(std::uint32_t const Frames, std::string &Reason) { @@ -102,7 +338,7 @@ bool vulkan14_renderer::context::create_core(std::uint32_t const Frames, std::st } auto const &limits{device.caps().limits}; (void)limits; - if (false == ring.create(memory, ring_size, Frames, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, "frame ring", ring_tail)) + if (false == ring.create(memory, ring_size, Frames, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, "frame ring", ring_tail)) { Reason = "no ring for the data of the frames"; return false; @@ -151,6 +387,8 @@ bool vulkan14_renderer::context::wait(std::uint64_t const Value) const void vulkan14_renderer::context::destroy() { auto const handle{device.handle()}; + // the frame given to the render thread goes to the queue first + stop_render_thread(); if (handle != VK_NULL_HANDLE) { // the end of the renderer, not a frame: everything on the gpu has to be done @@ -178,6 +416,10 @@ void vulkan14_renderer::context::destroy() { vkDestroyCommandPool(handle, frame.pool, nullptr); } + for (auto &worker : frame.workers) + { + vkDestroyCommandPool(handle, worker.pool, nullptr); + } } if (timeline != VK_NULL_HANDLE) { @@ -288,9 +530,11 @@ bool vulkan14_renderer::Init(GLFWwindow *Window) vk14::log_info(std::to_string(framesinflight) + " frames in flight"); // until the stages of the plan bring them, these parts of the simulator aren't there with vulkan - vk14::log_info("not available with vulkan yet: the atmosphere, the sky ambient light, automatic exposure and bloom, stars (P1), the weather fog and clouds (P2), " - "ambient occlusion and contact shadows (P3), shadows of lamps and headlights (P4), the culling on the gpu (P5), the heightmap terrain, grass and the particles " - "of the gpu (P6), the map panel, python screens in their own windows (N5), panels in their own windows (R5b), VR (X4)"); + vk14::log_info("not available with vulkan yet: the map panel, python screens in their own windows (N5), panels in their own windows (R5b), VR (X4)"); + // the settings of eu07.ini which only the opengl renderer takes; everything else of gfx.* is taken here as there + vk14::log_info("settings of the opengl renderer without effect with vulkan: compresstex (the textures go to the gpu as the files give them), gfx.depthprepass.replay (the scene " + "is walked again for its depth), gfx.bufferstreaming, gfx.geometry.shared, gfx.format.color, gfx.format.depth, gfx.postfx.motionblur.format (the formats of the " + "targets are fixed), gfx.gldebug (gfx.vulkan.validation instead)"); WriteLog("Gfx Renderer: setup complete"); update_info(); return true; @@ -318,6 +562,12 @@ bool vulkan14_renderer::AddViewport(global_settings::extraviewport_config const return false; } auto &ctx{*m_context}; + // the windows the render thread presents are left as they are until it's done + if (auto const *failure{ctx.drain()}) + { + report_device_lost(failure); + return false; + } auto swapchain{std::make_unique()}; std::string reason; if (false == swapchain->create(ctx.instance, ctx.device, window, "viewport " + conf.monitor, static_cast(ctx.frames.size()), reason)) @@ -343,7 +593,8 @@ void vulkan14_renderer::Shutdown() { if (m_context && (m_context->device.handle() != VK_NULL_HANDLE)) { - // the end of the renderer, not a frame: everything on the gpu has to be done + // the end of the renderer, not a frame: everything on the gpu has to be done, the frame of the render thread with it + m_context->stop_render_thread(); vkDeviceWaitIdle(m_context->device.handle()); } // the geometry on the gpu goes with the context; the data of the banks stays for the simulation @@ -389,34 +640,11 @@ void vulkan14_renderer::SwapBuffers() auto &ctx{*m_context}; Timer::subsystem.gfx_swap.start(); + // with the render thread the windows are presented by it (ctx.acquired was handed over to it) if (false == ctx.acquired.empty()) { - std::vector waits; - std::vector swapchains; - std::vector images; - std::vector results(ctx.acquired.size(), VK_SUCCESS); - for (auto *window : ctx.acquired) + if (false == ctx.present(ctx.acquired)) { - waits.push_back(window->present_semaphore()); - swapchains.push_back(window->handle()); - images.push_back(window->image_index()); - } - VkPresentInfoKHR present{}; - present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; - present.waitSemaphoreCount = static_cast(waits.size()); - present.pWaitSemaphores = waits.data(); - present.swapchainCount = static_cast(swapchains.size()); - present.pSwapchains = swapchains.data(); - present.pImageIndices = images.data(); - present.pResults = results.data(); - auto const result{vkQueuePresentKHR(ctx.device.graphics_queue(), &present)}; - for (std::size_t i = 0; i < ctx.acquired.size(); ++i) - { - ctx.acquired[i]->presented(results[i]); - } - if ((result == VK_ERROR_DEVICE_LOST) || (result == VK_ERROR_SURFACE_LOST_KHR)) - { - vk14::log_error(std::string("vkQueuePresentKHR failed with ") + vk14::result_string(result)); report_device_lost("presenting"); } ctx.acquired.clear(); @@ -484,6 +712,17 @@ void vulkan14_renderer::Update(double const Deltatime) { m_materials.on_weather_change(); } + if (((m_simulationstate.season != simulationstate.season) && (false == simulationstate.season.empty())) || + ((m_simulationstate.weather != simulationstate.weather) && (false == simulationstate.weather.empty()))) + { + // the heightmap terrain keeps copies of the textures of its layer materials + if (m_context && m_context->scene.terrain) + { + m_context->scene.terrain->on_environment_change(); + } + // the seasonal variants of the models are shown and hidden anew: the walks kept by the gpu culling go + m_walkrecords.clear(); + } } if (m_context && m_context->textures && (true == Global.ResourceSweep) && (true == simulation::is_ready)) @@ -511,7 +750,9 @@ void vulkan14_renderer::update_info() auto const &caps{ctx.device.caps()}; m_infotimes = "vulkan " + vk14::version_string(caps.api_version) + ": " + caps.device_name + " (" + caps.driver_name + ")\n" + "frames: " + std::to_string(ctx.framecount) + ", in flight " + std::to_string(ctx.frames.size()) + "\n" + "mainloop total: " + to_string(Timer::subsystem.mainloop_total.average(), 2) + " ms, frame cpu: " + to_string(Timer::subsystem.gfx_total.average(), 2) + " ms, ui: " + to_string(Timer::subsystem.gfx_gui.average(), 2) + - " ms, present: " + to_string(Timer::subsystem.gfx_swap.average(), 2) + " ms\n" + "gpu frame parts:\n" + ctx.passes.info(); + " ms, present: " + to_string(Timer::subsystem.gfx_swap.average(), 2) + " ms\n" + "command buffers recorded: " + to_string(ctx.record_time, 2) + " ms, " + std::to_string(ctx.record_calls) + " calls in " + + std::to_string(ctx.record_chunks) + " chunks, " + std::to_string(ctx.record_threads) + (ctx.record_threads == 1 ? " thread" : " threads") + + (ctx.render_threaded() ? ", on the render thread; waited for it: " + to_string(ctx.render_wait, 2) + " ms\n" : "\n") + "gpu frame parts:\n" + ctx.passes.info(); VkDeviceSize usage{0}, budget{0}; ctx.memory.budget(usage, budget); m_infostats = "device memory: " + vk14::memory_size_string(usage) + " of " + vk14::memory_size_string(budget) + (ctx.device.memory_budget_enabled() ? "" : " (no budget from the driver)") + "\n" + "images: " + std::to_string(ctx.images.size()) + @@ -522,6 +763,9 @@ void vulkan14_renderer::update_info() auto const extent{window->extent()}; m_infostats += "window: " + std::to_string(extent.width) + "x" + std::to_string(extent.height) + (window->srgb() ? ", srgb" : ", unorm") + (window->present_mode() == VK_PRESENT_MODE_FIFO_KHR ? ", vsync" : ", no vsync") + "\n"; } + // the statistics of the scene and of the cullings, as the opengl renderer gives them + collect_cluster_stats(); + m_infostats += "\n" + scene_info() + cull_info(); } bool vulkan14_renderer::Debug_Ui_State(std::optional State) diff --git a/rendering/vulkan14/vulkan14renderer.h b/rendering/vulkan14/vulkan14renderer.h index 263179fd..5cc03851 100644 --- a/rendering/vulkan14/vulkan14renderer.h +++ b/rendering/vulkan14/vulkan14renderer.h @@ -24,17 +24,27 @@ http://mozilla.org/MPL/2.0/. #include "rendering/lightarray.h" #include "rendering/common/scenewalk.h" #include "rendering/common/locallights.h" +#include "rendering/common/skylight.h" +#include "rendering/common/weatherstate.h" +#include "rendering/common/shadowfit.h" +#include "rendering/common/gpuparticles.h" #include "rendering/particles.h" #include "model/material.h" #include "vehicle/Camera.h" #include "gl/ubo.h" class world_environment; +struct VkRenderingInfo; namespace vk14 { struct scene_program; struct target_formats; +class image; +class compute_kernel; +struct draw_constants; enum class blend_mode; +class instance_cull; +struct image_use; } // namespace vk14 class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk @@ -69,6 +79,10 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk bool Blendable_Material(material_handle const Material) override; void Bind_Material(material_handle const Material, TSubModel const *sm = nullptr, lighting_data const *lighting = nullptr) override; IMaterial const *Material(material_handle const Material) const override; + material_handle Terrain_Material(material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement, + int const HoleSamples = 0, std::uint8_t const *Holes = nullptr) override; + void Terrain_Release(material_handle const Material) override; + void Terrain_Grass(std::vector const &Kinds, std::vector const &Chunks) override; // shaders std::shared_ptr Fetch_Shader(std::string const &name) override; std::string Shader_Name(std::shared_ptr const &Program) const override; @@ -158,6 +172,89 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk void walk_glare_draw(TSubModel *Submodel, gfx::geometry_handle const &Geometry) override; void walk_light_query(TSubModel *Submodel, bool const Begin) override; void walk_editor_cursor() override; + gfx::geometrybank_manager &walk_geometry() override; + bool walk_blending_enabled() const override; + void walk_region_begin() override; + bool walk_instances_begin() override; + void walk_instances_end() override; + + // the gpu culling of the instances (vulkan14indirect.cpp, the stage P5; opengl46_renderer::Render_Instances_Indirect() and the + // rest): the cullings of the passes and the depth pyramid, made when the renderer starts + bool init_cull(); + void destroy_cull(); + // the rendering of the pass under way, kept to be begun again where it was: it's ended for the compute passes of the culling + // in the middle of the walk, and begun again with what it drew loaded, with the state of the draws as it was + void remember_rendering(VkRenderingInfo const &Info, std::vector const &Uses); + void suspend_rendering(); + void resume_rendering(); + // the pass sets with the output of the culling as the instance buffer, made when the pass sets or the output changed; bound by + // the draws until they're dropped (nullptr) + void use_cull_pass_sets(vk14::instance_cull *Cull); + // writes the pass sets (Cull null) or those of the culling + void write_pass_sets(vk14::instance_cull *Cull); + // draws of the instances made, and the calls which made them + struct indirect_counts + { + std::uint32_t draws{0}; + std::uint32_t calls{0}; + indirect_counts &operator+=(indirect_counts const &Other) + { + draws += Other.draws; + calls += Other.calls; + return *this; + } + }; + using command_set = gfx::instance_cull::command_set; + // the instances of the main view drawn from its culling: the depth pyramid, the culling, the walks, the depth and the colour, + // in two phases with the occlusion test of the instances drawn first; inside the depth pass of the scene the depth alone, + // the colour waits for the colour pass (render_instances_indirect_pending()) + void render_instances_indirect(); + // the same for the passes other than the main view: no occlusion test, no depth prepass, one phase + void render_instances_indirect_pass(); + void render_instances_indirect_pending(); + // the walks of the culled groups (gfx::scene_walk::Walk_Indirect_Draws()), the draws put in order for the multi draws, and the + // commands filled in by the culling + void record_indirect_draws(vk14::instance_cull &Cull, bool const Cascade); + // the recorded draws with the commands of the set: their colour with the programs of their materials, their depth with the + // programs of the depth prepass, the shadows with the programs of the shadows + indirect_counts draw_indirect_colour(vk14::instance_cull &Cull, command_set const Set); + indirect_counts draw_indirect_depth(vk14::instance_cull &Cull, command_set const Set); + indirect_counts draw_indirect_shadow(vk14::instance_cull &Cull, command_set const Set); + // Count recorded draws from First with the program (a multi draw with more than one) and the state of the draws + bool draw_indirect(vk14::instance_cull &Cull, command_set const Set, std::size_t const First, std::uint32_t const Count, vk14::scene_program &Program, vk14::blend_mode const Blend); + // the variant of a program for the multi draws, nullptr when there's none + vk14::scene_program *multidraw_program(vk14::scene_program &Program); + vk14::instance_cull &pass_cull(); + // the shadow maps of the lamps (vulkan14lamps.cpp, the stage P4; opengl46_renderer::Render_local_shadows() and the rest): the cab + // lamp, the scenery lamps, one of them, the headlights, one set of them; the culling of a map (nullptr when its instances go + // through the cpu); the rendering of the map set up by the maps; the maps put in the layout the shading reads them in + void render_local_shadows(); + void render_lamp_shadows(); + void render_lamp_shadow(std::size_t const Slot, bool const Vehicles, TDynamicObject *Cab); + void render_headlight_shadows(); + void render_headlight_shadow(std::size_t const Slot, light_array::light_record const &Light, TDynamicObject *Cab); + vk14::instance_cull *light_shadow_cull(std::vector> &Culls, std::size_t const Index, bool const Enabled); + void begin_local_shadow_rendering(); + void end_local_shadow_rendering(); + void ready_lamp_maps(); + bool m_cubeshadows{false}; // the shadow pass draws to a cube map, with the programs of the cube maps + vk14::instance_cull *m_lightshadowcull{nullptr}; // the culling of the map of headlights or of a lamp being drawn + std::size_t m_lampscenerydrawn{0}; // scenery parts of the maps of lamps drawn in this frame so far + unsigned int m_instancecullframe{~0u}; // the frame the culling of the main view ran in last + int m_shadowcascade{-1}; // the cascade of the sun shadows being drawn + bool m_indirectmultidraw{false}; // the recorded draws are in order for the multi draws + vk14::instance_cull *m_indirectcull{nullptr}; // the culling the recorded draws were made for + command_set m_indirectcolourset{command_set::first}; // the commands the colour pass draws the pending instances with + // the cascades of the sun shadows fitted to the depth of the main view (gfx.shadow.sdsm): the cascades of the frame fitted to the + // latest measurement (opengl46_renderer::Shadow_Fit()), the measurement of the depth of the finished view, the state for the panel, + // the area of the cascade being set up and the sizes setup_pass() found for it + bool shadow_fit(viewport_config const &Viewport, std::array &Splits, gfx::shadow_fit::areas &Areas); + char const *shadow_fit_unavailable(viewport_config const &Viewport) const; + void measure_shadow_fit(std::size_t const Viewport); + gfx::shadow_fit::fit_state m_shadowfitstate; + std::array const *m_shadowfitarea{nullptr}; + float m_shadowslicesize{0.f}; + float m_shadowdrawnsize{0.f}; // the scene (vulkan14scene.cpp) bool init_scene(std::string &Reason); @@ -196,6 +293,14 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk void setup_shadow_matrices(); // the light clusters of the pass: the grid and the lists of the lights void update_clusters(glm::ivec2 const &Targetsize); + // which program of the depth prepass stands for the program of the material: 0 none, 1 without the alpha test, 2 with it + int prepass_class(material_handle const Material); + // the depth of the whole opaque scene of the colour pass of the main viewport before its colour, for the contact shadows + // (Contactonly: with them alone) or for itself (gfx.depthprepass.scene), and the contact shadows set up to read it + void render_scene_depth(std::size_t const Viewport, glm::mat4 const &Future); + bool contact_shadows_wanted(viewport_config const &Viewport) const; + void setup_contact_shadows(std::size_t const Viewport); + bool m_contactshadows{false}; // the materials material_handle invalid_material() const { @@ -215,6 +320,44 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk // the screenshot copied with a frame the gpu is done with, saved void update_screenshot(); void bind_pass_set(); + void update_pass_sets(); + // the pass set of the colour of the main view bound for the compute kernels, before the dispatches of Kernel (the kernels share + // the layout of set 0 and the push constants, so the set stays bound for the ones which follow) + void bind_compute_set(vk14::compute_kernel const &Kernel); + // the light of the sky (vulkan14sky.cpp): the resources, their first state, the light of the frame worked out before its + // passes, the environment map prefiltered with the GGX lobe, the stars of the sky pass + bool init_sky(std::string &Reason); + void destroy_sky(); + void clear_sky_images(); + void update_sky(); + void prefilter_environment(); + void render_stars(glm::mat4 const &View); + // the automatic exposure of the image of the main view, and its bloom chain; the result of the chain, null without it + void measure_exposure(vk14::image &Image); + vk14::image *render_bloom(std::size_t const Viewport, vk14::image &Image); + // the weather (vulkan14weather.cpp): the resources, the noises of the clouds, the block of the pass, what the weather is for the + // frame (before its passes), the clouds and the fog volume of the colour pass of the main view, the clouds and the fog over the sky + bool init_weather(std::string &Reason); + void destroy_weather(); + void make_cloud_noise(); + void upload_weather_block(); + void update_weather(); + void render_weather_view(glm::mat4 const &Projection, glm::mat4 const &View, glm::dvec3 const &Camera, glm::ivec2 const &Size, int const Cablamp); + void render_sky_weather(glm::mat4 const &Projection, bool const Mainview); + // the ambient occlusion (vulkan14ao.cpp): the resources, and the occlusion of a viewport multiplied into its colour + bool init_ao(std::string &Reason); + void destroy_ao(); + void render_ao(std::size_t const Viewport, int const Mode); + // the particles of the gpu: what the colour pass under way draws them with, and the depth of the scene drawn so far copied for + // the soft particles (false: they aren't soft) + gfx::gpu_particles::view particles_view() const; + bool setup_particles_depth(std::size_t const Viewport); + bool m_particlesoft{false}; + // the grass of the heightmap terrain seen from the main view, over the opaque scene (opengl46_renderer::Render_grass()) + void render_grass(glm::mat4 const &Future); + // a pass of the post-processing: the fragment shader drawn over the whole of the target, reading the images (Pass: a pass of + // its own, for the gpu times). Repeated: a bit for each input read texel by texel and repeated (the noise of the ssao) + void post_pass(char const *Fragment, vk14::image &Target, std::vector const &Inputs, vk14::draw_constants const &Constants, char const *Pass = nullptr, unsigned const Repeated = 0); bool bind_scene_pipeline(vk14::scene_program &Program, vk14::target_formats const &Targets, vk14::blend_mode const Blend, int const Topology); void apply_draw_state(); void push_textures(); @@ -223,6 +366,11 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk void update_texture_release(); std::unique_ptr m_context; + // the light of the sky and the weather of the frame, shared with the opengl renderer + gfx::sky_light m_skylight; + gfx::weather_state m_weatherstate; + // the environment map prefiltered with the GGX lobe is bound for the pass (light_data.env_ggx) + bool m_envmap_bound{false}; // the data of the geometry, which the simulation reads back, and the banks with it on the gpu gfx::geometrybank_manager m_geometry; std::vector m_banks; @@ -295,6 +443,24 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk debug_stats m_laststats; void update_info(); + // the debug windows (vulkan14debug.cpp, opengl46_renderer::draw_debug_ui()), the lines of the cullings and of the scene for them + // and for the statistics, the counters of the light clusters read back, and the heatmap of the clusters over the output of a + // viewport (opengl46_renderer::Render_cluster_heatmap()) + void draw_debug_ui(); + std::string cull_info(); + std::string scene_info(); + void collect_cluster_stats(); + void render_cluster_heatmap(std::size_t const Viewport); + // the headlights (opengl46_renderer::headlight_config_s), set by the debug window + struct headlight_config + { + float in_cutoff{1.005f}; + float out_cutoff{0.993f}; + float falloff_linear{0.15f}; + float falloff_quadratic{0.15f}; + float intensity{1.0f}; + float ambient{0.0f}; + } m_headlightconfig; // the device was lost: what was on the gpu goes to the log void report_device_lost(char const *Where); diff --git a/rendering/vulkan14/vulkan14scene.cpp b/rendering/vulkan14/vulkan14scene.cpp index bb25001d..09383c56 100644 --- a/rendering/vulkan14/vulkan14scene.cpp +++ b/rendering/vulkan14/vulkan14scene.cpp @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/vulkan14/vulkan14context.h" +#include "rendering/vulkan14/vulkan14commands.h" #include "rendering/nullrenderer.h" #include "model/Model3d.h" #include "model/AnimModel.h" @@ -22,6 +23,8 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Logs.h" #include "utilities/color.h" +#include + namespace { @@ -64,6 +67,11 @@ vk14::vertex_layout layout_of(vk14::vertex_kind const Kind) layout.bindings = {{0, sizeof(glm::vec3), VK_VERTEX_INPUT_RATE_VERTEX}, {1, sizeof(glm::vec3), VK_VERTEX_INPUT_RATE_VERTEX}}; layout.attributes = {{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, {1, 1, VK_FORMAT_R32G32B32_SFLOAT, 0}}; break; + case vk14::vertex_kind::star: + // gfx::weather_state::star_vertices(): the direction and the magnitude, then the colour and the phase, of each star + layout.bindings = {{0, 2 * sizeof(glm::vec4), VK_VERTEX_INPUT_RATE_VERTEX}}; + layout.attributes = {{0, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0}, {1, 0, VK_FORMAT_R32G32B32A32_SFLOAT, sizeof(glm::vec4)}}; + break; case vk14::vertex_kind::particle: // position, colour, texture coordinates (opengl46_particles::particle_vertex) layout.bindings = {{0, 36, VK_VERTEX_INPUT_RATE_VERTEX}}; @@ -73,6 +81,11 @@ vk14::vertex_layout layout_of(vk14::vertex_kind const Kind) layout.bindings = {{0, sizeof(glm::vec3), VK_VERTEX_INPUT_RATE_VERTEX}, {1, sizeof(glm::vec2), VK_VERTEX_INPUT_RATE_VERTEX}}; layout.attributes = {{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, {1, 1, VK_FORMAT_R32G32_SFLOAT, 0}}; break; + case vk14::vertex_kind::particle_instance: + // the instances drawn from the storage the compute passes of the particles fill (gl::particle_draw) + layout.bindings = {{0, sizeof(gl::particle_draw), VK_VERTEX_INPUT_RATE_INSTANCE}}; + layout.attributes = {{0, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0}, {1, 0, VK_FORMAT_R32G32B32A32_SFLOAT, sizeof(glm::vec4)}, {2, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 2 * sizeof(glm::vec4)}}; + break; case vk14::vertex_kind::none: default: break; @@ -120,6 +133,112 @@ std::vector vertex_input_locations(std::vector con return result; } +// the type of a binding of the pass set +VkDescriptorType pass_binding_type(std::uint32_t const Binding) +{ + using namespace vk14::bindings; + switch (Binding) + { + case SCENE_UBO: + case CLUSTER_UBO: + case WEATHER_UBO: + return VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; + case DRAW_SSBO: + case INSTANCE_SSBO: + case LIGHT_SSBO: + return VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC; + case CLUSTER_RANGE_SSBO: + case CLUSTER_INDEX_SSBO: + case SKY_SSBO: + return VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + case ATMOSPHERE_UBO: + case WEATHER_HORIZON_UBO: + return VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + default: + return VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + } +} + +// the views of the images each pass set holds, by the binding (the buffers and the beams of the headlights aside): the real +// images where the set reads them, the stand-ins elsewhere +std::array, 3> pass_set_images(vk14::scene_state const &scene) +{ + using namespace vk14::bindings; + std::array, 3> result{}; + static_assert(vk14::scene_state::set_count == 3, "the pass sets"); + for (std::size_t set = 0; set < result.size(); ++set) + { + auto &views{result[set]}; + auto const colour{set == vk14::scene_state::set_colour}; + auto const plain{set == vk14::scene_state::set_plain}; + views[SHADOW_TEX] = ((false == plain) && scene.shadow_map ? scene.shadow_map->view() : scene.shadow_empty->view()); + views[ENV_TEX] = (colour && scene.env_map ? scene.env_map->view() : scene.env_empty->view()); + views[SKY_VIEW_TEX] = scene.sky.skyview->view(); + views[ENV_GGX_TEX] = (colour && scene.sky.ggx ? scene.sky.ggx->view() : scene.env_empty->view()); + auto const volume{scene.volume_empty->view()}; + views[FOG_VOLUME_TEX] = (colour && scene.weather.fog_integrated ? scene.weather.fog_integrated->view() : volume); + views[CLOUD_SHADOW_TEX] = (scene.weather.shadow_map ? scene.weather.shadow_map->view() : scene.white->view()); + views[CLOUD_SHAPE_TEX] = (scene.weather.shape_noise ? scene.weather.shape_noise->view() : volume); + views[CLOUD_DETAIL_TEX] = (scene.weather.detail_noise ? scene.weather.detail_noise->view() : volume); + views[CLOUD_COVERAGE_TEX] = (scene.weather.coverage_noise ? scene.weather.coverage_noise->view() : scene.white->view()); + // the shadow maps of the lamps and the headlights, which the light buffers of the colour passes read + auto const *cab{scene.lamps.cab_map()}; + auto const *lamps{scene.lamps.lamp_maps()}; + auto const *headlights{scene.lamps.headlight_maps()}; + views[LOCAL_SHADOW_TEX] = (cab != nullptr ? cab->view() : scene.cube_shadow_empty->view()); + views[LOCAL_SHADOW_LAMPS_TEX] = (lamps != nullptr ? lamps->view() : scene.cube_shadow_empty->view()); + views[HEADLIGHT_SHADOW_TEX] = (headlights != nullptr ? headlights->view() : scene.headlight_shadow_empty->view()); + // the depth of the scene of the main view drawn before its colour, which the contact shadows read + views[SCENE_DEPTH_TEX] = (colour && (false == scene.targets.empty()) && scene.targets.front().scene_depth ? scene.targets.front().scene_depth->view() : scene.depth_empty->view()); + } + return result; +} + +// a pool for Count pass sets, and the sets +VkDescriptorPool make_pass_pool(VkDevice const Device, VkDescriptorSetLayout const Layout, std::uint32_t const Count, VkDescriptorSet *Sets) +{ + using namespace vk14::bindings; + std::array perset{}; + for (std::uint32_t binding = 0; binding < PASS_BINDING_COUNT; ++binding) + { + switch (pass_binding_type(binding)) + { + case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC: ++perset[0]; break; + case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC: ++perset[1]; break; + case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER: ++perset[2]; break; + case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER: ++perset[3]; break; + default: break; + } + } + std::array sizes{{{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, perset[0] * Count}, + {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, perset[1] * Count}, + {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, perset[2] * Count}, + {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, perset[3] * Count}, + {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, (PASS_BINDING_COUNT - perset[0] - perset[1] - perset[2] - perset[3]) * Count}}}; + VkDescriptorPoolCreateInfo poolinfo{}; + poolinfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolinfo.maxSets = Count; + poolinfo.poolSizeCount = static_cast(sizes.size()); + poolinfo.pPoolSizes = sizes.data(); + VkDescriptorPool pool{VK_NULL_HANDLE}; + if (!VK14_VERIFY(vkCreateDescriptorPool(Device, &poolinfo, nullptr, &pool))) + { + return VK_NULL_HANDLE; + } + std::vector layouts(Count, Layout); + VkDescriptorSetAllocateInfo allocation{}; + allocation.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + allocation.descriptorPool = pool; + allocation.descriptorSetCount = Count; + allocation.pSetLayouts = layouts.data(); + if (!VK14_VERIFY(vkAllocateDescriptorSets(Device, &allocation, Sets))) + { + vkDestroyDescriptorPool(Device, pool, nullptr); + return VK_NULL_HANDLE; + } + return pool; +} + // a view of one layer (and the first level) of an image VkImageView layer_view(VkDevice Device, vk14::image const &Image, std::uint32_t const Layer, std::string const &Name) { @@ -150,15 +269,15 @@ bool vulkan14_renderer::init_scene(std::string &Reason) auto const frames{static_cast(ctx.frames.size())}; using namespace vk14::bindings; - // the pass set: the blocks of the frame with dynamic offsets, the clusters, the maps + // the pass set: the blocks of the frame with dynamic offsets, the clusters, the maps, the light of the sky, the weather, the + // shadows of the lamps and the depth of the scene. the compute kernels read it too { - auto const stages{VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT}; - std::vector bindings{ - {SCENE_UBO, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1, stages, nullptr}, {DRAW_SSBO, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1, stages, nullptr}, - {INSTANCE_SSBO, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1, stages, nullptr}, {LIGHT_SSBO, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1, stages, nullptr}, - {CLUSTER_UBO, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1, stages, nullptr}, {CLUSTER_RANGE_SSBO, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, stages, nullptr}, - {CLUSTER_INDEX_SSBO, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, stages, nullptr}, {SHADOW_TEX, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, stages, nullptr}, - {ENV_TEX, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, stages, nullptr}, {HEADLIGHT_TEX, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, stages, nullptr}}; + auto const stages{VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_COMPUTE_BIT}; + std::vector bindings; + for (std::uint32_t binding = 0; binding < PASS_BINDING_COUNT; ++binding) + { + bindings.push_back({binding, pass_binding_type(binding), 1, stages, nullptr}); + } VkDescriptorSetLayoutCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; info.bindingCount = static_cast(bindings.size()); @@ -190,13 +309,15 @@ bool vulkan14_renderer::init_scene(std::string &Reason) Reason = "no layout of the pipelines of the scene"; return false; } - // the post-processing: the images it reads, pushed + // the post-processing: the images it reads and the exposure, pushed { std::vector bindings; - for (std::uint32_t image = 0; image < 4; ++image) + for (std::uint32_t image = 0; image < POST_TEXTURES; ++image) { bindings.push_back({POST_TEXTURE_FIRST + image, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}); } + static_assert(POST_EXPOSURE_SSBO == POST_TEXTURE_FIRST + POST_TEXTURES, "the exposure follows the images of the template"); + bindings.push_back({POST_EXPOSURE_SSBO, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}); if (false == scene.post_set.create(device, bindings, POST_SET, VK_PIPELINE_BIND_POINT_GRAPHICS, sizeof(VkDescriptorImageInfo), "post-processing images")) { Reason = "no layout of the post-processing"; @@ -345,9 +466,65 @@ bool vulkan14_renderer::init_scene(std::string &Reason) return false; } } + { + // the stand-ins of the images of the stages P + vk14::image_desc desc; + desc.type = VK_IMAGE_TYPE_3D; + desc.view_type = VK_IMAGE_VIEW_TYPE_3D; + desc.format = VK_FORMAT_R8G8B8A8_UNORM; + desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (false == make_image(scene.volume_empty, desc, "empty volume")) + { + return false; + } + desc = vk14::image_desc{}; + desc.format = VK_FORMAT_D32_SFLOAT; + desc.view_type = VK_IMAGE_VIEW_TYPE_CUBE_ARRAY; + desc.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; + desc.layers = 6; + desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (false == make_image(scene.cube_shadow_empty, desc, "empty cube shadow maps")) + { + return false; + } + desc = vk14::image_desc{}; + desc.format = VK_FORMAT_D32_SFLOAT; + desc.view_type = VK_IMAGE_VIEW_TYPE_2D_ARRAY; + desc.layers = static_cast(gl::MAX_HEADLIGHT_SHADOWS); + desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (false == make_image(scene.headlight_shadow_empty, desc, "empty headlight shadow maps")) + { + return false; + } + desc = vk14::image_desc{}; + desc.format = VK_FORMAT_R32_SFLOAT; + desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (false == make_image(scene.depth_empty, desc, "empty scene depth")) + { + return false; + } + if (false == scene.weather_horizon.create(ctx.memory, sizeof(glm::vec4) * gl::FOG_HORIZON_SAMPLES, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + vk14::memory_kind::device, "fog horizon")) + { + Reason = "no buffer of the colours of the fog"; + return false; + } + } + { + VkFormatProperties properties{}; + vkGetPhysicalDeviceFormatProperties(ctx.device.physical(), scene.bloom_format, &properties); + auto const needed{VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT}; + if ((properties.optimalTilingFeatures & needed) != needed) + { + scene.bloom_format = VK_FORMAT_R16G16B16A16_SFLOAT; + } + } // the formats of the passes which don't depend on the viewport scene.shadow_formats.depth = VK_FORMAT_D32_SFLOAT; scene.shadow_formats.id = ++scene.formats_count; + scene.cube_formats.depth = VK_FORMAT_D32_SFLOAT; + scene.cube_formats.view_mask = 0x3f; + scene.cube_formats.id = ++scene.formats_count; scene.reflection_formats.colour[0] = VK_FORMAT_R16G16B16A16_SFLOAT; scene.reflection_formats.colours = 1; scene.reflection_formats.depth = VK_FORMAT_D32_SFLOAT; @@ -364,7 +541,8 @@ bool vulkan14_renderer::init_scene(std::string &Reason) if ((false == clusters.aabbs.create(ctx.memory, sizeof(gl::cluster_aabb) * gl::CLUSTER_COUNT, usage, vk14::memory_kind::device, "cluster boxes")) || (false == clusters.ranges.create(ctx.memory, sizeof(gl::cluster_range) * gl::CLUSTER_COUNT, usage, vk14::memory_kind::device, "cluster ranges")) || (false == clusters.indices.create(ctx.memory, sizeof(std::uint32_t) * gl::CLUSTER_COUNT * gl::MAX_LIGHTS_PER_CLUSTER, usage, vk14::memory_kind::device, "cluster lights")) || - (false == clusters.counters.create(ctx.memory, sizeof(gl::cluster_counters), usage, vk14::memory_kind::device, "cluster counters"))) + (false == clusters.counters.create(ctx.memory, sizeof(gl::cluster_counters), usage | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, vk14::memory_kind::device, "cluster counters")) || + (false == clusters.counters_readback.create(ctx.memory, sizeof(gl::cluster_counters), frames, "cluster counters read back"))) { Reason = "no buffers of the light clusters"; return false; @@ -397,6 +575,7 @@ bool vulkan14_renderer::init_scene(std::string &Reason) result->vertex = ctx.shaders.get(Vertex); result->fragment = ctx.shaders.get(Fragment); result->fragment_name = Fragment; + result->vertex_name = Vertex; result->vertices = Kind; result->name = std::string(Vertex) + " + " + Fragment; return result; @@ -412,6 +591,10 @@ bool vulkan14_renderer::init_scene(std::string &Reason) scene.skydome = program("vbocolor.vert", "color.frag", vk14::vertex_kind::colour); scene.smoke = program("smoke.vert", "smoke.frag", vk14::vertex_kind::particle); scene.precipitation = program("precipitation.vert", "precipitation.frag", vk14::vertex_kind::position_uv); + scene.prepass = program("depthprepass.vert", "depthprepass.frag", vk14::vertex_kind::scene); + scene.prepass_alpha = program("depthprepass.vert", "depthprepass_alpha.frag", vk14::vertex_kind::scene); + scene.cube_shadow = program("shadowcube.vert", "shadowmap.frag", vk14::vertex_kind::scene); + scene.cube_alpha_shadow = program("shadowcube.vert", "alphashadowmap.frag", vk14::vertex_kind::scene); scene.quad = ctx.shaders.get("quad.vert"); for (auto const *name : {"copy.frag", "present.frag", "postfx_tonemapping.frag", "postfx_motionblur.frag", "postfx_chromaticaberration.frag"}) { @@ -428,6 +611,109 @@ bool vulkan14_renderer::init_scene(std::string &Reason) return false; } + // the light of the sky, the exposure, the weather + if ((false == init_sky(Reason)) || (false == init_weather(Reason)) || (false == init_ao(Reason))) + { + return false; + } + + scene.lamps.init(device, ctx.memory, ctx.deletions); + // the layer maps of the heightmap terrain drawn into the layers of its arrays by the copy shader + scene.terrain = std::make_unique(m_materials, *ctx.textures); + scene.grass.init(ctx.resources); + { + vk14::gpu_particles::host host; + host.compute = [this]() { + suspend_rendering(); + return m_context->commands; + }; + host.bind = [this](vk14::compute_kernel const &Kernel) { bind_compute_set(Kernel); }; + host.draw = [this](vk14::scene_program &Program, VkBuffer const Output, VkBuffer const Command, glm::vec4 const &Data0, glm::vec4 const &Data1) { + // the rendering goes on where the compute passes ended it; the model block is the one the caller set up + resume_rendering(); + auto &draw{*m_draw}; + if ((false == draw.rendering) || (draw.targets == nullptr) || (false == bind_scene_pipeline(Program, *draw.targets, vk14::blend_mode::scene_alpha, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP))) + { + return; + } + auto const cullface{draw.cullface}; + draw.cullface = false; + apply_draw_state(); + if (draw.set_dirty) + { + bind_pass_set(); + } + auto const block{push_draw_block()}; + if (block != ~0u) + { + if (draw.textures_dirty) + { + push_textures(); + } + vk14::draw_constants constants; + constants.draw = block; + constants.data[0] = Data0; + constants.data[1] = Data1; + push_constants(&constants); + VkDeviceSize const offset{0}; + draw.commands->bind_vertex_buffers(0, 1, &Output, &offset); + draw.commands->draw_indirect(Command, 0, 1, 4 * sizeof(std::uint32_t)); + draw.vertices = VK_NULL_HANDLE; + draw.userdata = VK_NULL_HANDLE; + ++draw.draws; + } + draw.cullface = cullface; + apply_draw_state(); + }; + host.slot = [this]() { return m_context->slot; }; + host.value = [this]() { return m_context->timelinevalue + 1; }; + host.completed = [this]() { return m_context->completed(); }; + scene.particles.init(device, ctx.pipelines, ctx.shaders, scene.pass_layout, ctx.memory, ctx.uploads, ctx.deletions, ctx.ring, ctx.device.caps().limits.minUniformBufferOffsetAlignment, frames, + std::move(host)); + } + scene.terrain->init(ctx.resources, [this](vk14::command_stream *const Commands, VkImageView const Target, VkFormat const Format, VkExtent2D const Extent, VkImageView const Source, VkSampler const Sampler) { + auto &scene{m_context->scene}; + auto const pipeline{post_pipeline("copy.frag", static_cast(Format))}; + if (pipeline == nullptr) + { + return; + } + VkRenderingAttachmentInfo attachment{}; + attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; + attachment.imageView = Target; + attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + VkRenderingInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + rendering.renderArea = {{0, 0}, Extent}; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &attachment; + Commands->begin_rendering(&rendering); + vk14::set_draw_state(Commands, Extent); + Commands->bind_pipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, reinterpret_cast(pipeline)); + Commands->set_primitive_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP); + // the texture as its material samples it, as the copy of the opengl renderer draws it + std::array descriptors; + for (std::size_t i = 0; i < vk14::bindings::POST_TEXTURES; ++i) + { + descriptors[i] = vk14::image_descriptor(Sampler, Source, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + } + descriptors[vk14::bindings::POST_EXPOSURE_SSBO] = vk14::buffer_descriptor(scene.exposure.data.handle(), 0, sizeof(gl::exposure_data)); + scene.post_set.push(Commands, scene.post_layout, descriptors.data()); + vk14::draw_constants const constants; + VkPushConstantsInfo info{}; + info.sType = VK_STRUCTURE_TYPE_PUSH_CONSTANTS_INFO; + info.layout = scene.post_layout; + info.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + info.size = sizeof(constants); + info.pValues = &constants; + Commands->push_constants2(&info); + Commands->draw(4, 1, 0, 0); + Commands->end_rendering(); + }); + m_draw = std::make_unique(); m_draw->textures.fill(null_handle); return true; @@ -446,6 +732,18 @@ void vulkan14_renderer::destroy_scene() { return; } + destroy_cull(); + scene.lamps.destroy(); + scene.terrain.reset(); + scene.grass.destroy(); + scene.particles.destroy(); + scene.grass_program.reset(); + scene.grass_tried = false; + scene.cluster_heatmap.reset(); + scene.cluster_heatmap_tried = false; + destroy_sky(); + destroy_weather(); + destroy_ao(); for (auto *pool : scene.occlusion_pools) { vkDestroyQueryPool(device, pool, nullptr); @@ -499,6 +797,11 @@ void vulkan14_renderer::destroy_scene() scene.env_depth.reset(); scene.pick_colour.reset(); scene.pick_depth.reset(); + scene.volume_empty.reset(); + scene.cube_shadow_empty.reset(); + scene.headlight_shadow_empty.reset(); + scene.depth_empty.reset(); + scene.weather_horizon.destroy(); scene.pick_controls = {}; scene.pick_scenery = {}; scene.cursor = {}; @@ -507,16 +810,16 @@ void vulkan14_renderer::destroy_scene() scene.clusters.ranges.destroy(); scene.clusters.indices.destroy(); scene.clusters.counters.destroy(); + scene.clusters.counters_readback.destroy(); scene.draws.ring.destroy(); scene.instances.ring.destroy(); } // the pass sets, made anew when the images they read changed: the old ones go once the gpu is done with them -void vulkan14_renderer::bind_pass_set() +void vulkan14_renderer::update_pass_sets() { auto &ctx{*m_context}; auto &scene{ctx.scene}; - auto &draw{*m_draw}; auto const device{ctx.device.handle()}; using namespace vk14::bindings; @@ -537,100 +840,187 @@ void vulkan14_renderer::bind_pass_set() { scene.pass_sets_stale = true; } - if (scene.pass_sets_stale) + auto const pass_images{pass_set_images(scene)}; + if (pass_images != scene.pass_images) { - scene.pass_sets_stale = false; - scene.headlights_view = headlights; - if (scene.pass_pool != VK_NULL_HANDLE) - { - ctx.deletions.push(ctx.uploads.current_value(), [device, pool = scene.pass_pool]() { vkDestroyDescriptorPool(device, pool, nullptr); }); - scene.pass_pool = VK_NULL_HANDLE; - } - auto const count{static_cast(vk14::scene_state::set_count)}; - std::array sizes{{{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 2 * count}, - {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 3 * count}, - {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2 * count}, - {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 3 * count}}}; - VkDescriptorPoolCreateInfo poolinfo{}; - poolinfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; - poolinfo.maxSets = count; - poolinfo.poolSizeCount = static_cast(sizes.size()); - poolinfo.pPoolSizes = sizes.data(); - if (!VK14_VERIFY(vkCreateDescriptorPool(device, &poolinfo, nullptr, &scene.pass_pool))) - { - scene.pass_pool = VK_NULL_HANDLE; - return; - } - std::array layouts; - layouts.fill(scene.pass_layout); - VkDescriptorSetAllocateInfo allocation{}; - allocation.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; - allocation.descriptorPool = scene.pass_pool; - allocation.descriptorSetCount = count; - allocation.pSetLayouts = layouts.data(); - if (!VK14_VERIFY(vkAllocateDescriptorSets(device, &allocation, scene.pass_sets.data()))) - { - return; - } - VkDescriptorBufferInfo const sceneblock{ctx.ring.handle(), 0, sizeof(gl::scene_ubs)}; - VkDescriptorBufferInfo const draws{scene.draws.ring.handle(), 0, draw_window}; - VkDescriptorBufferInfo const instances{scene.instances.ring.handle(), 0, instance_window}; - VkDescriptorBufferInfo const lights{ctx.ring.handle(), 0, sizeof(gl::light_data)}; - VkDescriptorBufferInfo const clusterblock{ctx.ring.handle(), 0, sizeof(gl::cluster_ubs)}; - VkDescriptorBufferInfo const ranges{scene.clusters.ranges.handle(), 0, VK_WHOLE_SIZE}; - VkDescriptorBufferInfo const indices{scene.clusters.indices.handle(), 0, VK_WHOLE_SIZE}; - VkDescriptorImageInfo const headlightsinfo{headlightssampler, headlights, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; - VkDescriptorImageInfo const emptyshadows{ctx.sampling.shadow, scene.shadow_empty->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; - VkDescriptorImageInfo const shadows{ctx.sampling.shadow, scene.shadow_map ? scene.shadow_map->view() : scene.shadow_empty->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; - VkDescriptorImageInfo const emptyenvironment{ctx.sampling.clamp_linear, scene.env_empty->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; - VkDescriptorImageInfo const environment{ctx.sampling.clamp_linear, scene.env_map ? scene.env_map->view() : scene.env_empty->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; - std::vector writes; - auto write = [&](VkDescriptorSet Set, std::uint32_t Binding, VkDescriptorType Type, VkDescriptorBufferInfo const *Buffer, VkDescriptorImageInfo const *Image) { - VkWriteDescriptorSet entry{}; - entry.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - entry.dstSet = Set; - entry.dstBinding = Binding; - entry.descriptorCount = 1; - entry.descriptorType = Type; - entry.pBufferInfo = Buffer; - entry.pImageInfo = Image; - writes.push_back(entry); - }; - for (std::uint32_t set = 0; set < count; ++set) - { - auto const handle{scene.pass_sets[set]}; - write(handle, SCENE_UBO, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, &sceneblock, nullptr); - write(handle, DRAW_SSBO, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, &draws, nullptr); - write(handle, INSTANCE_SSBO, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, &instances, nullptr); - write(handle, LIGHT_SSBO, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, &lights, nullptr); - write(handle, CLUSTER_UBO, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, &clusterblock, nullptr); - write(handle, CLUSTER_RANGE_SSBO, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, &ranges, nullptr); - write(handle, CLUSTER_INDEX_SSBO, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, &indices, nullptr); - write(handle, SHADOW_TEX, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, nullptr, (set == vk14::scene_state::set_plain ? &emptyshadows : &shadows)); - write(handle, ENV_TEX, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, nullptr, (set == vk14::scene_state::set_colour ? &environment : &emptyenvironment)); - write(handle, HEADLIGHT_TEX, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, nullptr, &headlightsinfo); - } - vkUpdateDescriptorSets(device, static_cast(writes.size()), writes.data(), 0, nullptr); + scene.pass_sets_stale = true; } + if (false == scene.pass_sets_stale) + { + return; + } + scene.pass_sets_stale = false; + scene.headlights_view = headlights; + scene.pass_images = pass_images; + if (scene.pass_pool != VK_NULL_HANDLE) + { + ctx.deletions.push(ctx.uploads.current_value(), [device, pool = scene.pass_pool]() { vkDestroyDescriptorPool(device, pool, nullptr); }); + scene.pass_pool = VK_NULL_HANDLE; + } + scene.headlights_sampler = headlightssampler; + scene.pass_pool = make_pass_pool(device, scene.pass_layout, static_cast(vk14::scene_state::set_count), scene.pass_sets.data()); + if (scene.pass_pool == VK_NULL_HANDLE) + { + return; + } + write_pass_sets(nullptr); + ++scene.pass_generation; +} + +// the bindings of the pass sets, or of those of a culling: the same but for the instance buffer, its output +void vulkan14_renderer::write_pass_sets(vk14::instance_cull *Cull) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto const device{ctx.device.handle()}; + auto const count{static_cast(vk14::scene_state::set_count)}; + using namespace vk14::bindings; + auto const &pass_images{scene.pass_images}; + auto const *sets{Cull != nullptr ? Cull->sets.data() : scene.pass_sets.data()}; + VkDescriptorBufferInfo const sceneblock{ctx.ring.handle(), 0, sizeof(gl::scene_ubs)}; + VkDescriptorBufferInfo const draws{scene.draws.ring.handle(), 0, scene.draws.window}; + VkDescriptorBufferInfo const instances{Cull != nullptr ? VkDescriptorBufferInfo{Cull->output_buffer(), 0, Cull->output_size()} : VkDescriptorBufferInfo{scene.instances.ring.handle(), 0, scene.instances.window}}; + VkDescriptorBufferInfo const lights{ctx.ring.handle(), 0, sizeof(gl::light_data)}; + VkDescriptorBufferInfo const clusterblock{ctx.ring.handle(), 0, sizeof(gl::cluster_ubs)}; + VkDescriptorBufferInfo const ranges{scene.clusters.ranges.handle(), 0, VK_WHOLE_SIZE}; + VkDescriptorBufferInfo const indices{scene.clusters.indices.handle(), 0, VK_WHOLE_SIZE}; + VkDescriptorBufferInfo const skyblock{scene.sky.sky.handle(), 0, sizeof(gl::sky_data)}; + VkDescriptorBufferInfo const atmosphereblock{scene.sky.atmosphere.handle(), 0, sizeof(gl::atmosphere_ubs)}; + VkDescriptorBufferInfo const weatherblock{ctx.ring.handle(), 0, offsetof(gl::weather_ubs, fog_horizon)}; + VkDescriptorBufferInfo const horizonblock{scene.weather_horizon.handle(), 0, sizeof(glm::vec4) * gl::FOG_HORIZON_SAMPLES}; + VkDescriptorImageInfo const headlightsinfo{scene.headlights_sampler, scene.headlights_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + std::vector writes; + std::vector> images; + auto write = [&](VkDescriptorSet Set, std::uint32_t Binding, VkDescriptorBufferInfo const *Buffer, VkDescriptorImageInfo const *Image) { + VkWriteDescriptorSet entry{}; + entry.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + entry.dstSet = Set; + entry.dstBinding = Binding; + entry.descriptorCount = 1; + entry.descriptorType = pass_binding_type(Binding); + entry.pBufferInfo = Buffer; + entry.pImageInfo = Image; + writes.push_back(entry); + }; + auto image = [&](VkSampler Sampler, VkImageView View) { + images.push_back(std::make_unique(VkDescriptorImageInfo{Sampler, View, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL})); + return images.back().get(); + }; + for (std::uint32_t set = 0; set < count; ++set) + { + auto const handle{sets[set]}; + auto const &views{pass_images[set]}; + write(handle, SCENE_UBO, &sceneblock, nullptr); + write(handle, DRAW_SSBO, &draws, nullptr); + write(handle, INSTANCE_SSBO, &instances, nullptr); + write(handle, LIGHT_SSBO, &lights, nullptr); + write(handle, CLUSTER_UBO, &clusterblock, nullptr); + write(handle, CLUSTER_RANGE_SSBO, &ranges, nullptr); + write(handle, CLUSTER_INDEX_SSBO, &indices, nullptr); + write(handle, SKY_SSBO, &skyblock, nullptr); + write(handle, ATMOSPHERE_UBO, &atmosphereblock, nullptr); + write(handle, WEATHER_UBO, &weatherblock, nullptr); + write(handle, WEATHER_HORIZON_UBO, &horizonblock, nullptr); + write(handle, HEADLIGHT_TEX, nullptr, &headlightsinfo); + write(handle, SHADOW_TEX, nullptr, image(ctx.sampling.shadow, views[SHADOW_TEX])); + write(handle, ENV_TEX, nullptr, image(ctx.sampling.clamp_linear, views[ENV_TEX])); + write(handle, SKY_VIEW_TEX, nullptr, image(ctx.sampling.clamp_linear, views[SKY_VIEW_TEX])); + write(handle, ENV_GGX_TEX, nullptr, image(ctx.sampling.clamp_linear, views[ENV_GGX_TEX])); + write(handle, FOG_VOLUME_TEX, nullptr, image(ctx.sampling.clamp_linear, views[FOG_VOLUME_TEX])); + write(handle, CLOUD_SHADOW_TEX, nullptr, image(ctx.sampling.clamp_linear, views[CLOUD_SHADOW_TEX])); + write(handle, CLOUD_SHAPE_TEX, nullptr, image(ctx.sampling.repeat_linear, views[CLOUD_SHAPE_TEX])); + write(handle, CLOUD_DETAIL_TEX, nullptr, image(ctx.sampling.repeat_linear, views[CLOUD_DETAIL_TEX])); + write(handle, CLOUD_COVERAGE_TEX, nullptr, image(ctx.sampling.repeat_linear, views[CLOUD_COVERAGE_TEX])); + write(handle, LOCAL_SHADOW_TEX, nullptr, image(ctx.sampling.shadow, views[LOCAL_SHADOW_TEX])); + write(handle, LOCAL_SHADOW_LAMPS_TEX, nullptr, image(ctx.sampling.shadow, views[LOCAL_SHADOW_LAMPS_TEX])); + write(handle, HEADLIGHT_SHADOW_TEX, nullptr, image(ctx.sampling.shadow, views[HEADLIGHT_SHADOW_TEX])); + write(handle, SCENE_DEPTH_TEX, nullptr, image(ctx.sampling.clamp_nearest, views[SCENE_DEPTH_TEX])); + } + vkUpdateDescriptorSets(device, static_cast(writes.size()), writes.data(), 0, nullptr); +} + +void vulkan14_renderer::use_cull_pass_sets(vk14::instance_cull *Cull) +{ + auto &draw{*m_draw}; + if (Cull == nullptr) + { + if (draw.cull_sets != nullptr) + { + draw.cull_sets = nullptr; + draw.set_dirty = true; + } + return; + } + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + update_pass_sets(); + if ((Cull->set_pool == VK_NULL_HANDLE) || (Cull->sets_generation != scene.pass_generation) || (Cull->sets_output != Cull->output_buffer())) + { + auto const device{ctx.device.handle()}; + if (Cull->set_pool != VK_NULL_HANDLE) + { + ctx.deletions.push(ctx.uploads.current_value(), [device, pool = Cull->set_pool]() { vkDestroyDescriptorPool(device, pool, nullptr); }); + } + Cull->set_pool = make_pass_pool(device, scene.pass_layout, static_cast(vk14::scene_state::set_count), Cull->sets.data()); + if (Cull->set_pool == VK_NULL_HANDLE) + { + draw.cull_sets = nullptr; + return; + } + write_pass_sets(Cull); + Cull->sets_generation = scene.pass_generation; + Cull->sets_output = Cull->output_buffer(); + } + draw.cull_sets = Cull->sets.data(); + draw.set_dirty = true; +} + +void vulkan14_renderer::bind_pass_set() +{ + auto &draw{*m_draw}; + update_pass_sets(); if (false == draw.rendering) { return; } + auto &scene{m_context->scene}; // the dynamic offsets in the order of the bindings - std::array const offsets{draw.scene_offset, static_cast(scene.draws.base), static_cast(scene.instances.base), draw.light_offset, draw.cluster_offset}; + // the instances of the indirect draws come from the output of the culling, in the pass sets of the culling + auto const instancebase{draw.cull_sets != nullptr ? 0u : static_cast(scene.instances.base)}; + std::array const offsets{draw.scene_offset, static_cast(scene.draws.base), instancebase, draw.light_offset, draw.cluster_offset, draw.weather_offset}; VkBindDescriptorSetsInfo info{}; info.sType = VK_STRUCTURE_TYPE_BIND_DESCRIPTOR_SETS_INFO; info.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; info.layout = scene.scene_layout; - info.firstSet = PASS_SET; + info.firstSet = vk14::bindings::PASS_SET; info.descriptorSetCount = 1; - info.pDescriptorSets = &scene.pass_sets[draw.set]; + info.pDescriptorSets = (draw.cull_sets != nullptr ? &draw.cull_sets[draw.set] : &scene.pass_sets[draw.set]); info.dynamicOffsetCount = static_cast(offsets.size()); info.pDynamicOffsets = offsets.data(); - vkCmdBindDescriptorSets2(draw.commands, &info); + draw.commands->bind_descriptor_sets2(&info); draw.set_dirty = false; } +// the pass set of the colour of the main view bound for the compute kernels (their layouts share set 0 and the push constants), +// with the offsets of the blocks the frame wrote last +void vulkan14_renderer::bind_compute_set(vk14::compute_kernel const &Kernel) +{ + auto &scene{m_context->scene}; + auto &draw{*m_draw}; + update_pass_sets(); + std::array const offsets{draw.scene_offset, static_cast(scene.draws.base), static_cast(scene.instances.base), draw.light_offset, draw.cluster_offset, draw.weather_offset}; + VkBindDescriptorSetsInfo info{}; + info.sType = VK_STRUCTURE_TYPE_BIND_DESCRIPTOR_SETS_INFO; + info.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + info.layout = Kernel.layout(); + info.firstSet = vk14::bindings::PASS_SET; + info.descriptorSetCount = 1; + info.pDescriptorSets = &scene.pass_sets[vk14::scene_state::set_colour]; + info.dynamicOffsetCount = static_cast(offsets.size()); + info.pDynamicOffsets = offsets.data(); + m_context->commands->bind_descriptor_sets2(&info); +} + // geometry gfx::geometrybank_handle vulkan14_renderer::Create_Bank() @@ -733,6 +1123,37 @@ IMaterial const *vulkan14_renderer::Material(material_handle const Material) con return &m_materials.material(Material); } +material_handle vulkan14_renderer::Terrain_Material(material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement, + int const HoleSamples, std::uint8_t const *Holes) +{ + if (m_context && m_context->scene.terrain) + { + auto const material{m_context->scene.terrain->make(Reuse, Layers, Samples, Weights, Placement, HoleSamples, Holes)}; + if (material != null_handle) + { + return material; + } + } + // the terrain shader can't be used, the chunk makes do with a plain material + return gfx_renderer::Terrain_Material(Reuse, Layers, Samples, Weights, Placement, HoleSamples, Holes); +} + +void vulkan14_renderer::Terrain_Release(material_handle const Material) +{ + if (m_context && m_context->scene.terrain) + { + m_context->scene.terrain->release(Material); + } +} + +void vulkan14_renderer::Terrain_Grass(std::vector const &Kinds, std::vector const &Chunks) +{ + if (m_context) + { + m_context->scene.grass.update(Kinds, Chunks); + } +} + std::shared_ptr vulkan14_renderer::Fetch_Shader(std::string const &name) { if (!m_context) @@ -750,6 +1171,7 @@ std::shared_ptr vulkan14_renderer::Fetch_Shader(std::string program->vertex = m_context->shaders.get("vertex.vert"); program->fragment = m_context->shaders.get("mat_" + name + ".frag"); program->fragment_name = "mat_" + name + ".frag"; + program->vertex_name = "vertex.vert"; program->vertices = vk14::vertex_kind::scene; program->name = "mat_" + name; // the directives of the material, of both stages as the opengl program has them @@ -875,10 +1297,54 @@ void vulkan14_renderer::walk_material(material_handle const Material, TSubModel model_ubs.opacity = (m_draw->blending ? 0.0f : 0.5f); } model_ubs.alpha_mult = (Submodel ? Submodel->fVisible : 1.0f); + if (m_indirectrecording) + { + // the walk of the groups of the gpu culling only notes what would be bound, it's bound when the draw is made + m_indirectmaterial = Material; + m_indirectsubmodel = Submodel; + return; + } // materials which declare texture variants provide a different texture set per model instance auto const &materialtextures{material.texture_variants.empty() ? material.textures : material.GetTextures(TSubModel::VariantId(Submodel))}; auto &draw{*m_draw}; + if (draw.depthpass) + { + // the depth of the whole scene: the programs of the depth prepass. what they can't draw (blended, materials of other programs) + // is left to the colour pass + draw.depthskip = true; + if (draw.blending || (model_ubs.opacity < 0.f)) + { + return; + } + auto const prepassclass{prepass_class(Material)}; + if (prepassclass == 0) + { + return; + } + auto &scene{m_context->scene}; + if (prepassclass == 2) + { + // a texture which isn't there (yet) would leave the one bound before to the alpha test + if (materialtextures[0] == null_handle) + { + return; + } + auto &texture{m_context->textures->mark_as_used(materialtextures[0])}; + if ((false == texture.create()) || (m_context->images.find(texture.get_id()) == nullptr)) + { + return; + } + walk_texture(0, materialtextures[0]); + draw.program = scene.prepass_alpha.get(); + } + else + { + draw.program = scene.prepass.get(); + } + draw.depthskip = false; + return; + } for (std::size_t unit = 0; unit < gl::MAX_TEXTURES; ++unit) { if (draw.textures[unit] != materialtextures[unit]) @@ -892,16 +1358,24 @@ void vulkan14_renderer::walk_material(material_handle const Material, TSubModel void vulkan14_renderer::walk_shadow_material(material_handle const Material) { + if (m_indirectrecording) + { + // the walk of the groups of the gpu culling of a shadow pass only notes the material + m_indirectmaterial = Material; + m_indirectsubmodel = nullptr; + return; + } auto &draw{*m_draw}; auto &scene{m_context->scene}; + // the cube maps of the lamps with programs of their own (the faces drawn by multiview) if ((Material != null_handle) && (m_materials.material(Material).textures[0] != null_handle)) { walk_texture(0, m_materials.material(Material).textures[0]); - draw.program = scene.alpha_shadow.get(); + draw.program = (m_cubeshadows ? scene.cube_alpha_shadow.get() : scene.alpha_shadow.get()); } else { - draw.program = scene.shadow.get(); + draw.program = (m_cubeshadows ? scene.cube_shadow.get() : scene.shadow.get()); } } @@ -953,6 +1427,16 @@ void vulkan14_renderer::walk_blending(bool const Alpha) m_draw->blending = Alpha; } +gfx::geometrybank_manager &vulkan14_renderer::walk_geometry() +{ + return m_geometry; +} + +bool vulkan14_renderer::walk_blending_enabled() const +{ + return (m_draw != nullptr) && m_draw->blending; +} + void vulkan14_renderer::walk_cull_face(bool const Enabled) { m_draw->cullface = Enabled; @@ -994,11 +1478,11 @@ void vulkan14_renderer::walk_light_query(TSubModel *Submodel, bool const Begin) } draw.query = static_cast(submodels.size()); submodels.push_back(Submodel); - vkCmdBeginQuery(draw.commands, scene.occlusion_pools[m_context->slot], draw.query, 0); + draw.commands->begin_query(scene.occlusion_pools[m_context->slot], draw.query, 0); } else if (draw.query != ~0u) { - vkCmdEndQuery(draw.commands, scene.occlusion_pools[m_context->slot], draw.query); + draw.commands->end_query(scene.occlusion_pools[m_context->slot], draw.query); draw.query = ~0u; } } @@ -1054,8 +1538,7 @@ void vulkan14_renderer::setup_sunlight_intensity(float const Factor) m_sunlight.factor = Factor; light_data.lights[0].intensity = m_sunlight.factor * (1.0f - Global.Overcast * 0.33f); light_data.ambient = m_sunlight.ambient * m_sunlight.factor; - // the directional ambient light of the sky comes with the stage P1 - light_data.sky_ambient_factor = 0.f; + light_data.sky_ambient_factor = (Global.gfx_ambient_sh ? m_sunlight.factor : 0.f); upload_light_header(); } @@ -1131,6 +1614,12 @@ void vulkan14_renderer::open_instance_window() void vulkan14_renderer::walk_draw(gfx::geometry_handle const &Geometry) { model_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW)); + if (m_indirectrecording) + { + // the draw is made later, from the gpu culling, with this state + Record_Indirect_Draw(Geometry); + return; + } draw_chunk(Geometry, true); } @@ -1165,6 +1654,11 @@ void vulkan14_renderer::draw_chunk(gfx::geometry_handle const &Geometry, bool co } auto const &chunk{static_cast(m_banks[Geometry.bank - 1])->last()}; + if (draw.depthpass && (draw.depthskip || (topology_class(chunk.topology) != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST))) + { + // the depth of the scene: what the programs of the prepass don't draw, and no points or lines + return; + } auto &ctx{*m_context}; auto &scene{ctx.scene}; auto const commands{draw.commands}; @@ -1202,14 +1696,14 @@ void vulkan14_renderer::draw_chunk(gfx::geometry_handle const &Geometry, bool co info.offset = 0; info.size = sizeof(constants); info.pValues = &constants; - vkCmdPushConstants2(commands, &info); + commands->push_constants2(&info); draw.pushed_draw = draw.block; } if ((chunk.vertices != draw.vertices) || (chunk.userdata != draw.userdata)) { VkBuffer const buffers[2]{chunk.vertices, chunk.userdata}; VkDeviceSize const offsets[2]{0, 0}; - vkCmdBindVertexBuffers(commands, 0, 2, buffers, offsets); + commands->bind_vertex_buffers(0, 2, buffers, offsets); draw.vertices = chunk.vertices; draw.userdata = chunk.userdata; } @@ -1219,14 +1713,14 @@ void vulkan14_renderer::draw_chunk(gfx::geometry_handle const &Geometry, bool co { if (chunk.indices != draw.indices) { - vkCmdBindIndexBuffer(commands, chunk.indices, 0, VK_INDEX_TYPE_UINT32); + commands->bind_index_buffer(chunk.indices, 0, VK_INDEX_TYPE_UINT32); draw.indices = chunk.indices; } - vkCmdDrawIndexed(commands, chunk.count, instances, chunk.first, chunk.basevertex, firstinstance); + commands->draw_indexed(chunk.count, instances, chunk.first, chunk.basevertex, firstinstance); } else { - vkCmdDraw(commands, chunk.count, instances, chunk.first, firstinstance); + commands->draw(chunk.count, instances, chunk.first, firstinstance); } ++draw.draws; } @@ -1260,7 +1754,7 @@ bool vulkan14_renderer::bind_scene_pipeline(vk14::scene_program &Program, vk14:: { try { - Program.fragment_single = ctx.shaders.get(Program.fragment_name, "#define NO_MOTION_OUTPUT\n"); + Program.fragment_single = ctx.shaders.get(Program.fragment_name, Program.defines + "#define NO_MOTION_OUTPUT\n"); } catch (gfx::shader_exception const &Exception) { @@ -1287,6 +1781,7 @@ bool vulkan14_renderer::bind_scene_pipeline(vk14::scene_program &Program, vk14:: desc.colour_formats.assign(Targets.colour.begin(), Targets.colour.begin() + Targets.colours); desc.depth_format = Targets.depth; desc.samples = Targets.samples; + desc.view_mask = Targets.view_mask; desc.specialization = {settings & 1u, (settings >> 1) & 1u, (settings >> 2) & 1u, (settings >> 3) & 1u, (settings >> 4) & 1u, (settings >> 5) & 1u}; desc.name = Program.name; pipeline = ctx.pipelines.get(desc); @@ -1298,12 +1793,12 @@ bool vulkan14_renderer::bind_scene_pipeline(vk14::scene_program &Program, vk14:: } if (pipeline != draw.pipeline) { - vkCmdBindPipeline(draw.commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + draw.commands->bind_pipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); draw.pipeline = pipeline; } if (topology != draw.topology) { - vkCmdSetPrimitiveTopology(draw.commands, topology); + draw.commands->set_primitive_topology(topology); draw.topology = topology; } return true; @@ -1316,19 +1811,19 @@ void vulkan14_renderer::apply_draw_state() auto const cull{draw.cullface ? (draw.shadows ? VK_CULL_MODE_FRONT_BIT : VK_CULL_MODE_BACK_BIT) : VK_CULL_MODE_NONE}; if (cull != draw.cull) { - vkCmdSetCullMode(draw.commands, cull); + draw.commands->set_cull_mode(cull); draw.cull = cull; } auto const depthtest{draw.glare ? 0 : 1}; if (depthtest != draw.depthtest) { - vkCmdSetDepthTestEnable(draw.commands, depthtest ? VK_TRUE : VK_FALSE); + draw.commands->set_depth_test_enable(depthtest ? VK_TRUE : VK_FALSE); draw.depthtest = depthtest; } auto const depthwrite{(draw.blending || draw.depthmask_off) ? 0 : 1}; if (depthwrite != draw.depthwrite) { - vkCmdSetDepthWriteEnable(draw.commands, depthwrite ? VK_TRUE : VK_FALSE); + draw.commands->set_depth_write_enable(depthwrite ? VK_TRUE : VK_FALSE); draw.depthwrite = depthwrite; } } @@ -1362,6 +1857,45 @@ void vulkan14_renderer::push_textures() draw.textures_dirty = false; } +// which program of the depth prepass stands for the program of the material (opengl46_renderer::Prepass_Class()): 0 none, 1 the +// one without the alpha test, 2 the one with it +int vulkan14_renderer::prepass_class(material_handle const Material) +{ + if (Material == null_handle) + { + return 0; + } + auto const &material{m_materials.material(Material)}; + if ((false == material.is_good) || (material.shader == nullptr)) + { + return 0; + } + auto &scene{m_context->scene}; + auto const lookup{scene.prepass_classes.find(material.shader.get())}; + if (lookup != scene.prepass_classes.end()) + { + return lookup->second; + } + // names the programs are fetched by: mat_.frag + static std::unordered_set const alphatested{"default", "default_1", "default_2", "default_detail", "default_specgloss", + "detail_normalmap", "detail_normalmap_specgloss", "normalmap", "normalmap_specgloss", "reflmap", + "reflmap_specgloss", "shadowlessnormalmap", "shadowlessnormalmap_specgloss", "sunlessnormalmap", "sunlessnormalmap_specgloss", + "trackblend", "trackblend_normalmap", "trackblend_specgloss"}; + static std::unordered_set const solid{"colored", "default_0", "terrain", "water", "water_specgloss", "invalid"}; + auto prepassclass{0}; + for (auto const &program : scene.materials) + { + if (program.second.get() != material.shader.get()) + { + continue; + } + prepassclass = (alphatested.count(program.first) > 0 ? 2 : solid.count(program.first) > 0 ? 1 : 0); + break; + } + scene.prepass_classes.emplace(material.shader.get(), prepassclass); + return prepassclass; +} + // textures: the files read and decoded as for the opengl renderer, made into images when they are first used texture_handle vulkan14_renderer::Fetch_Texture(std::string const &Filename, bool const Loadnow, GLint format_hint) diff --git a/rendering/vulkan14/vulkan14shaders.cpp b/rendering/vulkan14/vulkan14shaders.cpp index db8424ce..3196f453 100644 --- a/rendering/vulkan14/vulkan14shaders.cpp +++ b/rendering/vulkan14/vulkan14shaders.cpp @@ -9,8 +9,10 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/vulkan14/vulkan14shaders.h" +#include "rendering/vulkan14/vulkan14commands.h" #include "rendering/vulkan14/vulkan14device.h" #include "rendering/vulkan14/vulkan14bindings.h" +#include "rendering/common/localshadows.h" #include "gl/ubo.h" #include "utilities/Globals.h" #include "utilities/Logs.h" @@ -439,6 +441,7 @@ std::uint64_t pipeline_library::key_of(pipeline_desc const &Desc) const } hash_value(hash, Desc.depth_format); hash_value(hash, Desc.samples); + hash_value(hash, Desc.view_mask); for (auto const value : Desc.specialization) { hash_value(hash, value); @@ -546,6 +549,9 @@ VkPipeline pipeline_library::get(pipeline_desc const &Desc) { case blend_mode::none: break; + case blend_mode::no_colour: + attachment.colorWriteMask = 0; + break; case blend_mode::alpha: attachment.blendEnable = VK_TRUE; attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; @@ -582,6 +588,24 @@ VkPipeline pipeline_library::get(pipeline_desc const &Desc) attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; attachment.alphaBlendOp = VK_BLEND_OP_ADD; break; + case blend_mode::under_transmittance: + attachment.blendEnable = VK_TRUE; + attachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; + attachment.dstColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + attachment.colorBlendOp = VK_BLEND_OP_ADD; + attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + attachment.alphaBlendOp = VK_BLEND_OP_ADD; + break; + case blend_mode::multiply: + attachment.blendEnable = VK_TRUE; + attachment.srcColorBlendFactor = VK_BLEND_FACTOR_ZERO; + attachment.dstColorBlendFactor = VK_BLEND_FACTOR_SRC_COLOR; + attachment.colorBlendOp = VK_BLEND_OP_ADD; + attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + attachment.alphaBlendOp = VK_BLEND_OP_ADD; + break; case blend_mode::additive: attachment.blendEnable = VK_TRUE; attachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; @@ -609,6 +633,7 @@ VkPipeline pipeline_library::get(pipeline_desc const &Desc) rendering.colorAttachmentCount = static_cast(Desc.colour_formats.size()); rendering.pColorAttachmentFormats = Desc.colour_formats.data(); rendering.depthAttachmentFormat = Desc.depth_format; + rendering.viewMask = Desc.view_mask; VkGraphicsPipelineCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; @@ -703,23 +728,23 @@ std::string pipeline_library::info() const return "pipelines: " + std::to_string(m_pipelines.size()) + " (made in " + buffer + " ms)"; } -void set_draw_state(VkCommandBuffer Commands, VkExtent2D Extent) +void set_draw_state(command_stream *Commands, VkExtent2D Extent) { VkViewport viewport{0.f, 0.f, static_cast(Extent.width), static_cast(Extent.height), 0.f, 1.f}; - vkCmdSetViewport(Commands, 0, 1, &viewport); + Commands->set_viewport(0, 1, &viewport); VkRect2D scissor{{0, 0}, Extent}; - vkCmdSetScissor(Commands, 0, 1, &scissor); - vkCmdSetCullMode(Commands, VK_CULL_MODE_NONE); - vkCmdSetFrontFace(Commands, VK_FRONT_FACE_COUNTER_CLOCKWISE); - vkCmdSetPrimitiveTopology(Commands, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); - vkCmdSetDepthTestEnable(Commands, VK_FALSE); - vkCmdSetDepthWriteEnable(Commands, VK_FALSE); + Commands->set_scissor(0, 1, &scissor); + Commands->set_cull_mode(VK_CULL_MODE_NONE); + Commands->set_front_face(VK_FRONT_FACE_COUNTER_CLOCKWISE); + Commands->set_primitive_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); + Commands->set_depth_test_enable(VK_FALSE); + Commands->set_depth_write_enable(VK_FALSE); // reverse z, as the opengl renderer - vkCmdSetDepthCompareOp(Commands, VK_COMPARE_OP_GREATER_OR_EQUAL); - vkCmdSetDepthBiasEnable(Commands, VK_FALSE); - vkCmdSetDepthBias(Commands, 0.f, 0.f, 0.f); - vkCmdSetPrimitiveRestartEnable(Commands, VK_FALSE); - vkCmdSetStencilTestEnable(Commands, VK_FALSE); + Commands->set_depth_compare_op(VK_COMPARE_OP_GREATER_OR_EQUAL); + Commands->set_depth_bias_enable(VK_FALSE); + Commands->set_depth_bias(0.f, 0.f, 0.f); + Commands->set_primitive_restart_enable(VK_FALSE); + Commands->set_stencil_test_enable(VK_FALSE); } } // namespace vk14 @@ -746,6 +771,21 @@ std::string binding_preamble() define("SHADOW_TEX", SHADOW_TEX); define("ENV_TEX", ENV_TEX); define("HEADLIGHT_TEX", HEADLIGHT_TEX); + define("SKY_SSBO", SKY_SSBO); + define("ATMOSPHERE_UBO", ATMOSPHERE_UBO); + define("SKY_VIEW_TEX", SKY_VIEW_TEX); + define("ENV_GGX_TEX", ENV_GGX_TEX); + define("WEATHER_UBO", WEATHER_UBO); + define("WEATHER_HORIZON_UBO", WEATHER_HORIZON_UBO); + define("FOG_VOLUME_TEX", FOG_VOLUME_TEX); + define("CLOUD_SHADOW_TEX", CLOUD_SHADOW_TEX); + define("CLOUD_SHAPE_TEX", CLOUD_SHAPE_TEX); + define("CLOUD_DETAIL_TEX", CLOUD_DETAIL_TEX); + define("CLOUD_COVERAGE_TEX", CLOUD_COVERAGE_TEX); + define("LOCAL_SHADOW_TEX", LOCAL_SHADOW_TEX); + define("LOCAL_SHADOW_LAMPS_TEX", LOCAL_SHADOW_LAMPS_TEX); + define("HEADLIGHT_SHADOW_TEX", HEADLIGHT_SHADOW_TEX); + define("SCENE_DEPTH_TEX", SCENE_DEPTH_TEX); define("PASS_BINDING_COUNT", PASS_BINDING_COUNT); define("MATERIAL_TEXTURE_FIRST", MATERIAL_TEXTURE_FIRST); define("CL_CLUSTER_UBO", CL_CLUSTER_UBO); @@ -756,6 +796,9 @@ std::string binding_preamble() define("CL_LIGHT_SSBO", CL_LIGHT_SSBO); define("POST_SET", POST_SET); define("POST_TEXTURE_FIRST", POST_TEXTURE_FIRST); + define("POST_TEXTURES", POST_TEXTURES); + define("POST_EXPOSURE_SSBO", POST_EXPOSURE_SSBO); + define("COMPUTE_SET", COMPUTE_SET); define("PUSH_CONSTANTS_SIZE", PUSH_CONSTANTS_SIZE); define("SPEC_OUTPUT_GAMMA", SPEC_OUTPUT_GAMMA); define("SPEC_SHADOWMAP_ENABLED", SPEC_SHADOWMAP_ENABLED); @@ -776,6 +819,31 @@ std::string binding_preamble() define("MAX_LIGHTS_PER_CLUSTER", gl::MAX_LIGHTS_PER_CLUSTER); define("CLUSTER_BUILD_GROUP_SIZE", gl::CLUSTER_BUILD_GROUP_SIZE); define("CLUSTER_CULL_GROUP_SIZE", gl::CLUSTER_CULL_GROUP_SIZE); + // the sizes of the stages P: the light of the sky, the exposure, the weather, the culling on the gpu, the shadow fit, the particles + define("ENVMAP_GGX_LEVELS", static_cast(gl::ENVMAP_GGX_LEVELS), "U"); + preamble += "#define ENVMAP_GGX_ROUGHNESS_MIN " + std::to_string(gl::ENVMAP_GGX_ROUGHNESS_MIN) + "\n"; + define("ENVMAP_GGX_GROUP_SIZE", static_cast(gl::ENVMAP_GGX_GROUP_SIZE)); + define("EXPOSURE_HISTOGRAM_BINS", gl::EXPOSURE_HISTOGRAM_BINS); + define("EXPOSURE_HISTOGRAM_GROUP_SIZE", gl::EXPOSURE_HISTOGRAM_GROUP_SIZE); + define("ATMOSPHERE_TRANSMITTANCE_WIDTH", gl::ATMOSPHERE_TRANSMITTANCE_WIDTH); + define("ATMOSPHERE_TRANSMITTANCE_HEIGHT", gl::ATMOSPHERE_TRANSMITTANCE_HEIGHT); + define("ATMOSPHERE_MULTISCATTERING_SIZE", gl::ATMOSPHERE_MULTISCATTERING_SIZE); + define("ATMOSPHERE_SKYVIEW_WIDTH", gl::ATMOSPHERE_SKYVIEW_WIDTH); + define("ATMOSPHERE_SKYVIEW_HEIGHT", gl::ATMOSPHERE_SKYVIEW_HEIGHT); + define("ATMOSPHERE_AMBIENT_GROUP_SIZE", gl::ATMOSPHERE_AMBIENT_GROUP_SIZE); + define("MAX_CLOUD_LAYERS", static_cast(gl::MAX_CLOUD_LAYERS)); + define("FOG_HORIZON_SAMPLES", static_cast(gl::FOG_HORIZON_SAMPLES)); + define("WEATHER_GROUP_SIZE", gl::WEATHER_GROUP_SIZE); + preamble += "#define FOG_VOLUME_BIAS " + std::to_string(gl::FOG_VOLUME_BIAS) + "\n"; + define("INSTANCE_CULL_GROUP_SIZE", gl::INSTANCE_CULL_GROUP_SIZE); + define("INSTANCE_OFFSETS_GROUP_SIZE", gl::INSTANCE_OFFSETS_GROUP_SIZE); + define("INSTANCE_COMMAND_GROUP_SIZE", gl::INSTANCE_COMMAND_GROUP_SIZE); + define("INSTANCE_COUNTER_HEADER", gl::INSTANCE_COUNTER_HEADER); + define("INSTANCE_JOB_SLEEPERS", static_cast(gl::INSTANCE_JOB_SLEEPERS), "U"); + define("HIZ_GROUP_SIZE", gl::HIZ_GROUP_SIZE); + define("SHADOW_FIT_GROUP_SIZE", gl::SHADOW_FIT_GROUP_SIZE); + preamble += "#define LOCAL_SHADOW_NEAR " + std::to_string(gfx::local_shadows::shadow_near) + "\n"; + define("PARTICLE_GROUP_SIZE", gl::PARTICLE_GROUP_SIZE); return preamble; } diff --git a/rendering/vulkan14/vulkan14shaders.h b/rendering/vulkan14/vulkan14shaders.h index 29776f62..53ca32c1 100644 --- a/rendering/vulkan14/vulkan14shaders.h +++ b/rendering/vulkan14/vulkan14shaders.h @@ -79,7 +79,10 @@ enum class blend_mode additive, // the blending of the scene, as glBlendFunc() of the opengl renderer gives it to the colour and the alpha alike scene_alpha, // GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA - scene_additive // GL_SRC_ALPHA, GL_ONE (the glare of the lamps) + scene_additive, // GL_SRC_ALPHA, GL_ONE (the glare of the lamps) + under_transmittance, // GL_ONE, GL_SRC_ALPHA: the colour added, what is there kept by the alpha (the clouds and the fog over the sky) + multiply, // GL_ZERO, GL_SRC_COLOR: what is there times the colour (the ambient occlusion) + no_colour // the colour isn't written, only the depth (the depth prepass of the instances inside the colour pass) }; struct vertex_layout @@ -100,6 +103,8 @@ struct pipeline_desc std::vector colour_formats; VkFormat depth_format{VK_FORMAT_UNDEFINED}; VkSampleCountFlagBits samples{VK_SAMPLE_COUNT_1_BIT}; + // the views drawn at once (multiview, the cube shadow maps of the lamps), 0: none + std::uint32_t view_mask{0}; // the values of the specialization constants 0, 1, 2 ... of all the stages std::vector specialization; std::string name; @@ -132,6 +137,6 @@ class pipeline_library }; // the dynamic state of a pipeline in its plain form: the whole of Extent, no culling, no depth, triangles -void set_draw_state(VkCommandBuffer Commands, VkExtent2D Extent); +void set_draw_state(command_stream *Commands, VkExtent2D Extent); } // namespace vk14 diff --git a/rendering/vulkan14/vulkan14shadows.cpp b/rendering/vulkan14/vulkan14shadows.cpp new file mode 100644 index 00000000..1bdb6643 --- /dev/null +++ b/rendering/vulkan14/vulkan14shadows.cpp @@ -0,0 +1,167 @@ +/* +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/vulkan14/vulkan14shadows.h" +#include "rendering/vulkan14/vulkan14commands.h" + +namespace vk14 +{ + +local_shadow_maps::~local_shadow_maps() +{ + destroy(); +} + +void local_shadow_maps::init(VkDevice const Device, allocator &Memory, deletion_queue &Deletions) +{ + m_device = Device; + m_memory = &Memory; + m_deletions = &Deletions; +} + +void local_shadow_maps::destroy() +{ + // the device is idle: the maps go at once + m_deletions = nullptr; + let_go(m_cab, m_cabviews); + let_go(m_lamps, m_lampviews); + let_go(m_headlights, m_headlightviews); +} + +bool local_shadow_maps::make(std::shared_ptr &Map, std::vector &Views, int const Size, int const Layers, bool const Cube, char const *Name) +{ + if ((m_memory == nullptr) || (Size <= 0) || (Layers <= 0)) + { + return false; + } + image_desc desc; + desc.format = VK_FORMAT_D32_SFLOAT; + desc.extent = {static_cast(Size), static_cast(Size), 1}; + desc.layers = static_cast(Layers); + desc.view_type = (Cube ? VK_IMAGE_VIEW_TYPE_CUBE_ARRAY : VK_IMAGE_VIEW_TYPE_2D_ARRAY); + desc.flags = (Cube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0); + desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + Map = std::make_shared(); + if (false == Map->create(*m_memory, desc, Name)) + { + Map.reset(); + return false; + } + // the cubes drawn with multiview, six layers each, or the layers drawn one by one + auto const step{Cube ? 6 : 1}; + for (int first = 0; first < Layers; first += step) + { + VkImageViewCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + info.image = Map->handle(); + info.viewType = (Cube ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D); + info.format = desc.format; + info.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, static_cast(first), static_cast(step)}; + VkImageView view{VK_NULL_HANDLE}; + if (vkCreateImageView(m_device, &info, nullptr, &view) != VK_SUCCESS) + { + let_go(Map, Views); + return false; + } + Views.push_back(view); + } + return true; +} + +void local_shadow_maps::let_go(std::shared_ptr &Map, std::vector &Views) +{ + if (m_deletions != nullptr) + { + // the frames in flight may still read them + m_deletions->push(m_value, [device = m_device, views = std::move(Views), map = std::move(Map)]() { + for (auto *view : views) + { + vkDestroyImageView(device, view, nullptr); + } + }); + } + else + { + for (auto *view : Views) + { + vkDestroyImageView(m_device, view, nullptr); + } + } + Views.clear(); + Map.reset(); +} + +bool local_shadow_maps::gpu_allocate(int const Size) +{ + return make(m_cab, m_cabviews, Size, 6 * (cab_slot + 1), true, "lamp shadow map"); +} + +void local_shadow_maps::gpu_release() +{ + let_go(m_cab, m_cabviews); +} + +bool local_shadow_maps::gpu_allocate_lamps(int const Count, int const Size) +{ + // the scenery parts and the working copies + return make(m_lamps, m_lampviews, Size, 6 * 2 * Count, true, "scenery lamp shadow maps"); +} + +void local_shadow_maps::gpu_release_lamps() +{ + let_go(m_lamps, m_lampviews); +} + +bool local_shadow_maps::gpu_allocate_headlights(int const Count, int const Size) +{ + return make(m_headlights, m_headlightviews, Size, Count, false, "headlight shadow maps"); +} + +void local_shadow_maps::gpu_release_headlights() +{ + let_go(m_headlights, m_headlightviews); +} + +void local_shadow_maps::gpu_begin_cab() +{ + m_target = draw_target{m_cab.get(), m_cabviews.empty() ? VK_NULL_HANDLE : m_cabviews[cab_slot], static_cast(m_size), 0x3f, true, true}; +} + +void local_shadow_maps::gpu_begin_lamp(int const Scenerycube, int const Workingcube, bool const Vehicles) +{ + auto const cube{Vehicles ? Workingcube : Scenerycube}; + m_target = draw_target{m_lamps.get(), (static_cast(cube) < m_lampviews.size()) ? m_lampviews[cube] : VK_NULL_HANDLE, static_cast(m_lampssize), 0x3f, (false == Vehicles), true}; + if (Vehicles && m_lamps && (m_commands != nullptr)) + { + // the vehicles go over the scenery: the copy of the scenery part + transition(m_commands, *m_lamps, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT); + VkImageCopy2 region{}; + region.sType = VK_STRUCTURE_TYPE_IMAGE_COPY_2; + region.srcSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, static_cast(Scenerycube * 6), 6}; + region.dstSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, static_cast(Workingcube * 6), 6}; + region.extent = {static_cast(m_lampssize), static_cast(m_lampssize), 1}; + VkCopyImageInfo2 copy{}; + copy.sType = VK_STRUCTURE_TYPE_COPY_IMAGE_INFO_2; + copy.srcImage = m_lamps->handle(); + copy.srcImageLayout = VK_IMAGE_LAYOUT_GENERAL; + copy.dstImage = m_lamps->handle(); + copy.dstImageLayout = VK_IMAGE_LAYOUT_GENERAL; + copy.regionCount = 1; + copy.pRegions = ®ion; + m_commands->copy_image2(©); + } +} + +void local_shadow_maps::gpu_begin_headlight(std::size_t const Slot) +{ + m_target = draw_target{m_headlights.get(), (Slot < m_headlightviews.size()) ? m_headlightviews[Slot] : VK_NULL_HANDLE, static_cast(m_headlightssize), 0, true, false}; +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14shadows.h b/rendering/vulkan14/vulkan14shadows.h new file mode 100644 index 00000000..4cc42e0d --- /dev/null +++ b/rendering/vulkan14/vulkan14shadows.h @@ -0,0 +1,113 @@ +/* +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/. +*/ + +// the shadow maps of the lamps in the vulkan renderer (the stage P4 of the plan, opengl46_local_shadows): the maps of gfx::local_shadows, +// which tells what they hold. the six faces of a cube map are drawn at once by multiview (shadowcube.vert reads the face from +// gl_ViewIndex) instead of the geometry shader of the opengl renderer. the renderer draws to the target the class sets up +// (target()), the copies and the clears of the cubes of the lamps are recorded into the commands of the frame + +#pragma once + +#include "rendering/common/localshadows.h" +#include "rendering/vulkan14/vulkan14memory.h" + +#include +#include + +namespace vk14 +{ + +class local_shadow_maps : public gfx::local_shadows +{ + public: + // what the drawing begun last draws to + struct draw_target + { + image *map{nullptr}; + VkImageView view{VK_NULL_HANDLE}; // the layers of the cube (6, multiview) or of the headlights (1) + std::uint32_t size{0}; + std::uint32_t view_mask{0}; // 0x3f for a cube + bool clear{true}; // the cube or the layer starts cleared, the working copy of a lamp from the copy of its scenery part + bool cube{true}; + }; + + local_shadow_maps() = default; + ~local_shadow_maps() override; + + // Device, Memory, Deletions: what the maps are made and let go with; called once + void init(VkDevice Device, allocator &Memory, deletion_queue &Deletions); + // the maps go (the device is about to) + void destroy(); + // the commands of the frame being recorded and the value of the timeline the gpu reaches when it's done with it, before the drawing + void begin_frame(command_stream *Commands, std::uint64_t Value) + { + m_commands = Commands; + m_value = Value; + } + // the frame was recorded: no commands go to it any more + void end_frame() + { + m_commands = nullptr; + } + draw_target const &target() const + { + return m_target; + } + // the maps, for the pass sets (null: none, the stand-ins are read) + image *cab_map() const + { + return m_cab.get(); + } + image *lamp_maps() const + { + return m_lamps.get(); + } + image *headlight_maps() const + { + return m_headlights.get(); + } + + protected: + bool gpu_allocate(int const Size) override; + void gpu_release() override; + bool gpu_allocate_lamps(int const Count, int const Size) override; + void gpu_release_lamps() override; + bool gpu_allocate_headlights(int const Count, int const Size) override; + void gpu_release_headlights() override; + void gpu_begin_cab() override; + void gpu_end_cab() override {} + void gpu_begin_lamp(int const Scenerycube, int const Workingcube, bool const Vehicles) override; + void gpu_end_lamp() override {} + void gpu_begin_headlight(std::size_t const Slot) override; + void gpu_end_headlight() override {} + // the gpu times are those of the pass list + void gpu_lamps_timer(bool const Begin) override {} + void gpu_headlights_timer(bool const Begin) override {} + + private: + // a depth map array of Layers layers of Size x Size, its default view a cube array (Cube) or a 2d array, and a view of each + // cube (6 layers) or of each layer + bool make(std::shared_ptr &Map, std::vector &Views, int const Size, int const Layers, bool const Cube, char const *Name); + void let_go(std::shared_ptr &Map, std::vector &Views); + + VkDevice m_device{VK_NULL_HANDLE}; + allocator *m_memory{nullptr}; + deletion_queue *m_deletions{nullptr}; + command_stream *m_commands{nullptr}; + std::uint64_t m_value{0}; + std::shared_ptr m_cab; + std::vector m_cabviews; + std::shared_ptr m_lamps; + std::vector m_lampviews; + std::shared_ptr m_headlights; + std::vector m_headlightviews; + draw_target m_target; +}; + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14sky.cpp b/rendering/vulkan14/vulkan14sky.cpp new file mode 100644 index 00000000..46036125 --- /dev/null +++ b/rendering/vulkan14/vulkan14sky.cpp @@ -0,0 +1,482 @@ +/* +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/. +*/ + +// the light of the sky of the vulkan renderer (the stage P1 of the plan, opengl46_sky and opengl46_exposure of the opengl +// renderer): the look-up tables of the physical atmosphere made by compute kernels, the light of the frame worked out from +// them and the directional part of the ambient light (gfx::sky_light, shared with the opengl renderer), the environment map +// prefiltered with the GGX lobe, the stars of the catalogue; the automatic exposure and the bloom of the post-processing + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14context.h" +#include "rendering/vulkan14/vulkan14commands.h" +#include "rendering/common/exposure.h" +#include "simulation/simulation.h" +#include "simulation/simulationenvironment.h" +#include "utilities/Globals.h" +#include "utilities/Timer.h" + +namespace +{ + +VkPipelineStageFlags2 const shader_stages{VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT}; +VkPipelineStageFlags2 const sampled_stage{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT}; +VkAccessFlags2 const sampled_access{VK_ACCESS_2_SHADER_SAMPLED_READ_BIT}; +VkPipelineStageFlags2 const compute_stage{VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT}; +VkAccessFlags2 const storage_write{VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT}; + +std::uint32_t groups(std::uint32_t const Size, std::uint32_t const Group) +{ + return (Size + Group - 1) / Group; +} + +// a view of one level of a cube image as a cube, for a kernel writing the level +VkImageView level_view(VkDevice Device, vk14::image const &Image, std::uint32_t const Level, std::string const &Name) +{ + VkImageViewCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + info.image = Image.handle(); + info.viewType = VK_IMAGE_VIEW_TYPE_CUBE; + info.format = Image.desc().format; + info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, Level, 1, 0, 6}; + VkImageView view{VK_NULL_HANDLE}; + if (!VK14_VERIFY(vkCreateImageView(Device, &info, nullptr, &view))) + { + return VK_NULL_HANDLE; + } + vk14::name_object(Device, VK_OBJECT_TYPE_IMAGE_VIEW, vk14::handle_value(view), Name); + return view; +} + +// the push constants of the kernels, laid out as the blocks of the shaders (std430) +struct prefilter_constants +{ + float roughness; + std::int32_t target_size; + float source_size; + std::int32_t sample_count; +}; +struct histogram_constants +{ + glm::vec2 log_range; +}; +struct average_constants +{ + glm::vec2 log_range; + glm::vec2 pad; + glm::vec4 adaptation; + glm::vec2 exposure_range; +}; + +} // namespace + + +bool vulkan14_renderer::init_sky(std::string &Reason) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &sky{scene.sky}; + auto &exposure{scene.exposure}; + auto const device{ctx.device.handle()}; + auto const frames{static_cast(ctx.frames.size())}; + + // the buffers the shading reads + if ((false == sky.sky.create(ctx.memory, sizeof(gl::sky_data), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::device, "sky ambient")) || + (false == sky.atmosphere.create(ctx.memory, sizeof(gl::atmosphere_ubs), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::device, "atmosphere")) || + (false == sky.result.create(ctx.memory, sizeof(gl::atmosphere_result), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, vk14::memory_kind::device, "atmosphere result")) || + (false == sky.result_readback.create(ctx.memory, sizeof(gl::atmosphere_result), frames, "atmosphere result readback")) || + (false == exposure.data.create(ctx.memory, sizeof(gl::exposure_data), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::device, "exposure")) || + (false == exposure.readback.create(ctx.memory, 4 * sizeof(float), frames, "exposure readback"))) + { + Reason = "no buffers of the light of the sky"; + return false; + } + // the tables of the atmosphere, black until they are first made + auto make_image = [&](std::shared_ptr &Image, vk14::image_desc const &Desc, char const *Name) { + Image = std::make_shared(); + if (false == Image->create(ctx.memory, Desc, Name)) + { + Image.reset(); + Reason = std::string("no image for ") + Name; + return false; + } + return true; + }; + { + vk14::image_desc desc; + desc.format = VK_FORMAT_R16G16B16A16_SFLOAT; + desc.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + desc.extent = {gl::ATMOSPHERE_TRANSMITTANCE_WIDTH, gl::ATMOSPHERE_TRANSMITTANCE_HEIGHT, 1}; + if (false == make_image(sky.transmittance, desc, "atmosphere transmittance")) + { + return false; + } + desc.extent = {gl::ATMOSPHERE_MULTISCATTERING_SIZE, gl::ATMOSPHERE_MULTISCATTERING_SIZE, 1}; + if (false == make_image(sky.multiscattering, desc, "atmosphere multiple scattering")) + { + return false; + } + desc.extent = {gl::ATMOSPHERE_SKYVIEW_WIDTH, gl::ATMOSPHERE_SKYVIEW_HEIGHT, 1}; + if (false == make_image(sky.skyview, desc, "atmosphere sky-view")) + { + return false; + } + } + // the environment map prefiltered with the GGX lobe, made with the environment map + if (Global.gfx_envmap_enabled) + { + vk14::image_desc desc; + desc.format = VK_FORMAT_R16G16B16A16_SFLOAT; + desc.view_type = VK_IMAGE_VIEW_TYPE_CUBE; + desc.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; + desc.layers = 6; + desc.extent = {static_cast(gl::ENVMAP_GGX_SIZE), static_cast(gl::ENVMAP_GGX_SIZE), 1}; + desc.mips = static_cast(gl::ENVMAP_GGX_LEVELS); + desc.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (false == make_image(sky.ggx, desc, "environment map ggx")) + { + return false; + } + for (std::uint32_t level = 0; level < desc.mips; ++level) + { + sky.ggx_levels.push_back(level_view(device, *sky.ggx, level, "environment map ggx " + std::to_string(level))); + } + } + // the stars of the catalogue, in the equatorial frame + { + auto const stars{gfx::weather_state::star_vertices()}; + sky.star_count = static_cast(stars.size() / 2); + if (sky.star_count > 0) + { + auto vertices{std::make_shared()}; + auto const size{static_cast(stars.size() * sizeof(glm::vec4))}; + if ((false == vertices->create(ctx.memory, size, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::device, "stars")) || (false == ctx.uploads.upload_buffer(vertices, stars.data(), size))) + { + Reason = "no buffer of the stars"; + return false; + } + sky.star_vertices = vertices; + } + } + // the kernels and the programs + try + { + auto kernel = [&](vk14::compute_kernel &Kernel, char const *Shader, char const *Name) { + if (false == Kernel.create(device, ctx.pipelines, ctx.shaders.get(Shader), scene.pass_layout, {}, Name)) + { + Reason = std::string("no compute pipeline of ") + Name; + return false; + } + return true; + }; + if ((false == kernel(sky.transmittance_kernel, "atmosphere_transmittance.comp", "atmosphere transmittance")) || (false == kernel(sky.multiscattering_kernel, "atmosphere_multiscattering.comp", "atmosphere multiple scattering")) || + (false == kernel(sky.skyview_kernel, "atmosphere_skyview.comp", "atmosphere sky-view")) || (false == kernel(sky.ambient_kernel, "atmosphere_ambient.comp", "atmosphere ambient")) || + (false == kernel(sky.prefilter_kernel, "envmap_prefilter.comp", "environment map ggx")) || (false == kernel(exposure.histogram, "exposure_histogram.comp", "exposure histogram")) || + (false == kernel(exposure.average, "exposure_average.comp", "exposure average"))) + { + return false; + } + auto program = [&](char const *Vertex, char const *Fragment, vk14::vertex_kind const Kind) { + auto result{std::make_shared()}; + result->vertex = ctx.shaders.get(Vertex); + result->fragment = ctx.shaders.get(Fragment); + result->fragment_name = Fragment; + result->vertices = Kind; + result->name = std::string(Vertex) + " + " + Fragment; + return result; + }; + sky.atmosphere_dome = program("vbocolor.vert", "sky_atmosphere.frag", vk14::vertex_kind::colour); + sky.stars = program("stars.vert", "stars.frag", vk14::vertex_kind::star); + for (auto const *name : {"postfx_bloom_down.frag", "postfx_bloom_up.frag"}) + { + scene.post_fragments.emplace(name, ctx.shaders.get(name)); + } + } + catch (gfx::shader_exception const &e) + { + Reason = std::string("invalid shader: ") + e.what(); + return false; + } + return true; +} + +void vulkan14_renderer::destroy_sky() +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &sky{scene.sky}; + auto &exposure{scene.exposure}; + auto const device{ctx.device.handle()}; + for (auto *kernel : {&sky.transmittance_kernel, &sky.multiscattering_kernel, &sky.skyview_kernel, &sky.ambient_kernel, &sky.prefilter_kernel, &exposure.histogram, &exposure.average}) + { + kernel->destroy(device); + } + for (auto *view : sky.ggx_levels) + { + if (view != VK_NULL_HANDLE) + { + vkDestroyImageView(device, view, nullptr); + } + } + sky.ggx_levels.clear(); + sky.ggx.reset(); + sky.transmittance.reset(); + sky.multiscattering.reset(); + sky.skyview.reset(); + sky.sky.destroy(); + sky.atmosphere.destroy(); + sky.result.destroy(); + sky.result_readback.destroy(); + sky.atmosphere_dome.reset(); + sky.stars.reset(); + sky.star_vertices.reset(); + exposure.data.destroy(); + exposure.readback.destroy(); +} + +// the images of the sky put in their layouts the first time: black, ready to be read +void vulkan14_renderer::clear_sky_images() +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &sky{scene.sky}; + auto const commands{ctx.commands}; + std::vector uses; + for (auto *image : {sky.transmittance.get(), sky.multiscattering.get(), sky.skyview.get(), sky.ggx.get()}) + { + if (image != nullptr) + { + uses.push_back(vk14::use_of(*image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_CLEAR_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT)); + } + } + vk14::barriers(commands, uses); + VkClearColorValue const black{{0.f, 0.f, 0.f, 0.f}}; + for (auto *image : {sky.transmittance.get(), sky.multiscattering.get(), sky.skyview.get(), sky.ggx.get()}) + { + if (image != nullptr) + { + VkImageSubresourceRange const range{VK_IMAGE_ASPECT_COLOR_BIT, 0, image->desc().mips, 0, image->desc().layers}; + commands->clear_color_image(image->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); + } + } + uses.clear(); + for (auto *image : {sky.transmittance.get(), sky.multiscattering.get(), sky.skyview.get(), sky.ggx.get()}) + { + if (image != nullptr) + { + uses.push_back(vk14::use_of(*image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)); + } + } + vk14::barriers(commands, uses); + // the exposure starts from the key, not adapted yet + 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; + vk14::update_buffer(commands, scene.exposure.data, &data, sizeof(data), 0); + // the atmosphere valid from the start, and no directional ambient light + vk14::update_buffer(commands, sky.atmosphere, &m_skylight.atmosphere(), sizeof(gl::atmosphere_ubs), 0); + vk14::update_buffer(commands, sky.sky, &sky.uploaded, sizeof(gl::sky_data), 0); + sky.sky_uploaded = true; +} + +// the light of the sky of the frame (opengl46_sky::update()): the tables of the atmosphere made where their input changed, the +// result of an earlier frame read back, the sunlight, the ambient light and the fog colour of the frame worked out from it and +// the directional ambient light written for the shading. before the passes of the frame +void vulkan14_renderer::update_sky() +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &sky{scene.sky}; + auto const commands{ctx.commands}; + auto const &sun{simulation::Environment.sun()}; + auto const sundirection{sun.getDirection()}; + + m_skylight.begin_update(); + if (Global.gfx_atmosphere) + { + bool medium{false}; + if (m_skylight.atmosphere_changed(glm::normalize(sundirection), medium)) + { + ctx.passes.begin_pass("atmosphere"); + vk14::update_buffer(commands, sky.atmosphere, &m_skylight.atmosphere(), sizeof(gl::atmosphere_ubs), 0); + bind_compute_set(sky.transmittance_kernel); + auto const sampled = [&](vk14::image &Image) { return vk14::image_descriptor(ctx.sampling.clamp_linear, Image.view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); }; + if (medium) + { + vk14::barriers(commands, {vk14::use_of(*sky.transmittance, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write)}); + sky.transmittance_kernel.run(commands, {vk14::storage_image(sky.transmittance->view())}, groups(gl::ATMOSPHERE_TRANSMITTANCE_WIDTH, 8), groups(gl::ATMOSPHERE_TRANSMITTANCE_HEIGHT, 8)); + vk14::barriers(commands, {vk14::use_of(*sky.transmittance, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access), + vk14::use_of(*sky.multiscattering, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write)}); + sky.multiscattering_kernel.run(commands, {sampled(*sky.transmittance), vk14::storage_image(sky.multiscattering->view())}, gl::ATMOSPHERE_MULTISCATTERING_SIZE, gl::ATMOSPHERE_MULTISCATTERING_SIZE); + vk14::barriers(commands, {vk14::use_of(*sky.multiscattering, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); + } + vk14::barriers(commands, {vk14::use_of(*sky.skyview, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write)}); + sky.skyview_kernel.run(commands, {sampled(*sky.transmittance), sampled(*sky.multiscattering), vk14::storage_image(sky.skyview->view())}, groups(gl::ATMOSPHERE_SKYVIEW_WIDTH, 8), groups(gl::ATMOSPHERE_SKYVIEW_HEIGHT, 8)); + vk14::barriers(commands, {vk14::use_of(*sky.skyview, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); + vk14::memory_barrier(commands, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT, compute_stage, storage_write); + sky.ambient_kernel.run(commands, {sampled(*sky.transmittance), sampled(*sky.skyview), vk14::buffer_descriptor(sky.result.handle(), 0, sizeof(gl::atmosphere_result))}, 1); + sky.result_readback.push(commands, ctx.slot, ctx.timelinevalue + 1, sky.result.handle(), 0, sizeof(gl::atmosphere_result)); + ctx.passes.end_pass(); + } + gl::atmosphere_result result; + if (sky.result_readback.pop(ctx.completed(), &result, sizeof(result))) + { + m_skylight.set_result(result); + } + } + m_skylight.end_update(m_sunlight, m_fogcolor, simulation::Environment.skydome(), sundirection, sun.getAngle()); + + // the directional part of the ambient light for the shading + auto const &skydata{m_skylight.sky_data()}; + if ((false == sky.sky_uploaded) || (0 != std::memcmp(&skydata, &sky.uploaded, sizeof(skydata)))) + { + sky.uploaded = skydata; + vk14::update_buffer(commands, sky.sky, &sky.uploaded, sizeof(gl::sky_data), 0); + sky.sky_uploaded = true; + } +} + +// the levels of the GGX map prefiltered from the environment map with its mipmaps (opengl46_sky::prefilter()) +void vulkan14_renderer::prefilter_environment() +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &sky{scene.sky}; + if ((false == bool(sky.ggx)) || (false == bool(scene.env_map))) + { + return; + } + auto const commands{ctx.commands}; + ctx.passes.begin_pass("envmap ggx", {vk14::use_of(*scene.env_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, compute_stage, sampled_access), + vk14::use_of(*sky.ggx, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write)}); + for (std::size_t level = 0; level < gl::ENVMAP_GGX_LEVELS; ++level) + { + auto const size{static_cast(gl::ENVMAP_GGX_SIZE >> level)}; + prefilter_constants constants; + constants.roughness = gl::ENVMAP_GGX_ROUGHNESS_MIN + (1.f - gl::ENVMAP_GGX_ROUGHNESS_MIN) * static_cast(level) / static_cast(gl::ENVMAP_GGX_LEVELS - 1); + constants.target_size = size; + constants.source_size = static_cast(gl::ENVMAP_SIZE); + constants.sample_count = 64; + auto const count{groups(static_cast(size), static_cast(gl::ENVMAP_GGX_GROUP_SIZE))}; + sky.prefilter_kernel.dispatch(commands, {vk14::storage_image(sky.ggx_levels[level]), vk14::image_descriptor(ctx.sampling.clamp_linear, scene.env_map->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)}, constants, count, count, 6); + } + vk14::barriers(commands, {vk14::use_of(*sky.ggx, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access), vk14::use_of(*scene.env_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); + ctx.passes.end_pass(); + sky.ggx_ready = true; +} + +// the stars, in the sky pass (opengl46_weather::draw_stars()). View: the rotation of the camera of the pass +void vulkan14_renderer::render_stars(glm::mat4 const &View) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &sky{scene.sky}; + auto &draw{*m_draw}; + if ((sky.star_count == 0) || (false == bool(sky.star_vertices)) || (false == bool(sky.stars))) + { + return; + } + auto const &sun{simulation::Environment.sun()}; + glm::mat4 toworld, toview; + glm::vec4 params; + if (false == m_weatherstate.stars(sun.getAngle(), sun.getSiderealTime(), sun.getLatitude(), View, toworld, toview, params)) + { + return; + } + if (false == bind_scene_pipeline(*sky.stars, *draw.targets, vk14::blend_mode::additive, VK_PRIMITIVE_TOPOLOGY_POINT_LIST)) + { + return; + } + apply_draw_state(); + model_ubs.param[0] = params; + draw.block = push_draw_block(); + if (draw.block == ~0u) + { + return; + } + if (draw.set_dirty) + { + bind_pass_set(); + } + if (draw.textures_dirty) + { + push_textures(); + } + vk14::draw_constants constants; + constants.draw = draw.block; + for (int column = 0; column < 3; ++column) + { + constants.data[column] = toworld[column]; + constants.data[3 + column] = toview[column]; + } + push_constants(&constants); + VkDeviceSize const offset{0}; + auto const buffer{sky.star_vertices->handle()}; + draw.commands->bind_vertex_buffers(0, 1, &buffer, &offset); + draw.commands->draw(sky.star_count, 1, 0, 0); + draw.vertices = VK_NULL_HANDLE; + draw.userdata = VK_NULL_HANDLE; +} + +// the luminance of the image measured and the exposure adapted to it (opengl46_exposure::measure()), outside a rendering +void vulkan14_renderer::measure_exposure(vk14::image &Image) +{ + auto &ctx{*m_context}; + auto &exposure{ctx.scene.exposure}; + auto const commands{ctx.commands}; + float data[4]; + if (exposure.readback.pop(ctx.completed(), data, sizeof(data))) + { + std::copy(std::begin(data), std::end(data), std::begin(exposure.values)); + } + vk14::barriers(commands, {vk14::use_of(Image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, compute_stage, sampled_access)}); + // the reads of the tonemapping of the frame before + vk14::memory_barrier(commands, VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_TRANSFER_READ_BIT, compute_stage, VK_ACCESS_2_SHADER_STORAGE_READ_BIT | storage_write); + auto const buffer{vk14::buffer_descriptor(exposure.data.handle(), 0, sizeof(gl::exposure_data))}; + histogram_constants histogram{gfx::exposure_settings::histogram_range()}; + auto const groupsize{2 * gl::EXPOSURE_HISTOGRAM_GROUP_SIZE}; + exposure.histogram.dispatch(commands, {vk14::image_descriptor(ctx.sampling.clamp_nearest, Image.view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL), buffer}, histogram, groups(Image.desc().extent.width, groupsize), + groups(Image.desc().extent.height, groupsize)); + vk14::memory_barrier(commands, compute_stage, storage_write, compute_stage, VK_ACCESS_2_SHADER_STORAGE_READ_BIT | storage_write); + average_constants average{}; + average.log_range = gfx::exposure_settings::average_range(); + average.adaptation = gfx::exposure_settings::adaptation(Timer::GetDeltaRenderTime()); + average.exposure_range = gfx::exposure_settings::exposure_range(); + exposure.average.dispatch(commands, {buffer}, average, 1); + // read by the tonemapping, and copied for the debug window + vk14::memory_barrier(commands, compute_stage, storage_write, VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_TRANSFER_READ_BIT); + exposure.readback.push(commands, ctx.slot, ctx.timelinevalue + 1, exposure.data.handle(), offsetof(gl::exposure_data, adapted_luminance), 4 * sizeof(float)); +} + +// the image taken down and up the bloom chain of the viewport (opengl46_exposure::bloom()); the result, which the tonemapping +// blends in, or null without the chain +vk14::image *vulkan14_renderer::render_bloom(std::size_t const Index, vk14::image &Image) +{ + auto &ctx{*m_context}; + auto &targets{ctx.scene.targets[Index]}; + if (targets.bloom_sizes.empty()) + { + return nullptr; + } + // down, the first step from the image itself + for (std::size_t level = 0; level < targets.bloom_sizes.size(); ++level) + { + vk14::draw_constants constants; + constants.data[0] = gfx::exposure_settings::bloom_params(level); + post_pass("postfx_bloom_down.frag", *targets.bloom_down[level], {level == 0 ? &Image : targets.bloom_down[level - 1].get()}, constants); + } + // up, from the lowest level + for (std::size_t level = targets.bloom_up.size(); level-- > 0;) + { + auto *lower{(level + 1 < targets.bloom_up.size()) ? targets.bloom_up[level + 1].get() : targets.bloom_down[level + 1].get()}; + post_pass("postfx_bloom_up.frag", *targets.bloom_up[level], {lower, targets.bloom_down[level].get()}, vk14::draw_constants{}); + } + return targets.bloom_up.empty() ? targets.bloom_down.front().get() : targets.bloom_up.front().get(); +} diff --git a/rendering/vulkan14/vulkan14swapchain.cpp b/rendering/vulkan14/vulkan14swapchain.cpp index 33258290..5b82d03c 100644 --- a/rendering/vulkan14/vulkan14swapchain.cpp +++ b/rendering/vulkan14/vulkan14swapchain.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/vulkan14/vulkan14swapchain.h" +#include "rendering/vulkan14/vulkan14commands.h" #include "rendering/vulkan14/vulkan14instance.h" #include "rendering/vulkan14/vulkan14device.h" #include "utilities/Globals.h" @@ -360,7 +361,7 @@ void swapchain::presented(VkResult const Result) } } -void swapchain::record_clear(VkCommandBuffer Commands, glm::vec4 const &Colour) const +void swapchain::record_clear(command_stream *Commands, glm::vec4 const &Colour) const { VkImageMemoryBarrier2 barrier{}; barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; @@ -380,7 +381,7 @@ void swapchain::record_clear(VkCommandBuffer Commands, glm::vec4 const &Colour) dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.imageMemoryBarrierCount = 1; dependency.pImageMemoryBarriers = &barrier; - vkCmdPipelineBarrier2(Commands, &dependency); + Commands->pipeline_barrier2(&dependency); VkRenderingAttachmentInfo colour{}; colour.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; @@ -396,8 +397,8 @@ void swapchain::record_clear(VkCommandBuffer Commands, glm::vec4 const &Colour) rendering.layerCount = 1; rendering.colorAttachmentCount = 1; rendering.pColorAttachments = &colour; - vkCmdBeginRendering(Commands, &rendering); - vkCmdEndRendering(Commands); + Commands->begin_rendering(&rendering); + Commands->end_rendering(); barrier.srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; barrier.srcAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT; @@ -405,7 +406,7 @@ void swapchain::record_clear(VkCommandBuffer Commands, glm::vec4 const &Colour) barrier.dstAccessMask = VK_ACCESS_2_NONE; barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; - vkCmdPipelineBarrier2(Commands, &dependency); + Commands->pipeline_barrier2(&dependency); } void swapchain::release_images() diff --git a/rendering/vulkan14/vulkan14swapchain.h b/rendering/vulkan14/vulkan14swapchain.h index 89ab00d6..f028d3c9 100644 --- a/rendering/vulkan14/vulkan14swapchain.h +++ b/rendering/vulkan14/vulkan14swapchain.h @@ -44,7 +44,7 @@ class swapchain // the image of the frame Frame (index of the frame in flight) acquire_result acquire(std::uint32_t const Frame); // the image acquired, from undefined to cleared with Colour (linear) and ready to be shown - void record_clear(VkCommandBuffer Commands, glm::vec4 const &Colour) const; + void record_clear(command_stream *Commands, glm::vec4 const &Colour) const; // the image acquired and its view VkImage image() const { diff --git a/rendering/vulkan14/vulkan14terrain.cpp b/rendering/vulkan14/vulkan14terrain.cpp new file mode 100644 index 00000000..cde7f9e5 --- /dev/null +++ b/rendering/vulkan14/vulkan14terrain.cpp @@ -0,0 +1,361 @@ +/* +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/vulkan14/vulkan14terrain.h" +#include "rendering/vulkan14/vulkan14commands.h" +#include "rendering/vulkan14/vulkan14device.h" +#include "utilities/Logs.h" + +namespace vk14 +{ + +namespace +{ + +std::uint32_t mip_count(std::uint32_t const Size) +{ + return static_cast(std::floor(std::log2(std::max(Size, 1u)))) + 1; +} + +} // namespace + +terrain_materials::~terrain_materials() +{ + destroy(); +} + +void terrain_materials::init(resources const &Resources, copy_draw Draw) +{ + m_resources = &Resources; + m_draw = std::move(Draw); +} + +void terrain_materials::destroy() +{ + if (m_resources == nullptr) + { + return; + } + auto const device{m_resources->gpu->handle()}; + for (auto &images : m_images) + { + for (auto *view : images.layers) + { + vkDestroyImageView(device, view, nullptr); + } + images = array_images{}; + } + m_resources = nullptr; +} + +bool terrain_materials::gpu_init() +{ + return (m_resources != nullptr) && bool(m_draw); +} + +vulkan14_texture &terrain_materials::texture(texture_handle const Handle) +{ + return static_cast(m_textures.texture(Handle)); +} + +std::shared_ptr terrain_materials::make_image(VkFormat const Format, std::uint32_t const Width, std::uint32_t const Height, std::uint32_t const Mips, char const *Name) +{ + image_desc desc; + desc.format = Format; + desc.extent = {Width, Height, 1}; + desc.mips = Mips; + desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + auto result{std::make_shared()}; + if (false == result->create(*m_resources->memory, desc, Name)) + { + ErrorLog(std::string("heightmap terrain: no gpu image for ") + Name); + return nullptr; + } + return result; +} + +void terrain_materials::upload(texture_handle const Texture, VkFormat const Format, std::uint32_t const Width, std::uint32_t const Height, void const *Data, VkDeviceSize const Size, bool const Mips, VkSampler const Sampler) +{ + auto &target{texture(Texture)}; + auto map{target.adopted()}; + auto const generate{Mips && m_resources->uploads->can_blit(Format)}; + auto const mips{generate ? mip_count(std::max(Width, Height)) : 1u}; + if ((map == nullptr) || (map->desc().format != Format) || (map->desc().extent.width != Width) || (map->desc().extent.height != Height) || (map->desc().mips != mips)) + { + map = make_image(Format, Width, Height, mips, target.name.c_str()); + if (map == nullptr) + { + return; + } + } + // the frames sent before are done with the content the upload replaces before it's written (the barrier of the upload) + if (m_resources->uploads->upload(map, Data, Size, {{0, {Width, Height, 1}}}, generate)) + { + target.adopt(map, Sampler); + } +} + +void terrain_materials::gpu_white(texture_source &Texture) +{ + std::uint8_t const texel[4]{255, 255, 255, 255}; + auto &target{static_cast(Texture)}; + auto map{make_image(VK_FORMAT_R8G8B8A8_UNORM, 1, 1, 1, target.name.c_str())}; + if ((map != nullptr) && m_resources->uploads->upload(map, texel, sizeof(texel), {{0, {1, 1, 1}}}, false)) + { + target.adopt(map, m_resources->sampling->repeat_nearest); + } +} + +void terrain_materials::gpu_storage(texture_array const &Array, int const Layers, bool const Fresh) +{ + auto const index{static_cast(&Array - arrays().data())}; + auto &images{m_images[index]}; + let_go(images); + image_desc desc; + desc.format = (Array.srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM); + desc.extent = {static_cast(Array.size), static_cast(Array.size), 1}; + desc.mips = mip_count(static_cast(Array.size)); + desc.layers = static_cast(Layers); + desc.view_type = VK_IMAGE_VIEW_TYPE_2D_ARRAY; + desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + auto &target{texture(Array.handle)}; + auto map{std::make_shared()}; + if (false == map->create(*m_resources->memory, desc, target.name)) + { + ErrorLog("heightmap terrain: no gpu image for the layers of \"" + target.name + "\""); + return; + } + auto const device{m_resources->gpu->handle()}; + for (int layer = 0; layer < Layers; ++layer) + { + VkImageViewCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + info.image = map->handle(); + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.format = desc.format; + info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, static_cast(layer), 1}; + VkImageView view{VK_NULL_HANDLE}; + if (vkCreateImageView(device, &info, nullptr, &view) != VK_SUCCESS) + { + ErrorLog("heightmap terrain: no view of a layer of \"" + target.name + "\""); + images.layers.clear(); + break; + } + images.layers.push_back(view); + } + images.map = map; + // cleared by the next frame, before anything reads it + images.clear = true; + target.adopt(map, m_resources->sampling->repeat_aniso); +} + +void terrain_materials::let_go(array_images &Images) +{ + if (Images.layers.empty() && (Images.map == nullptr)) + { + return; + } + // the frames in flight may still read them + m_resources->deletions->push(m_resources->uploads->current_value(), [device = m_resources->gpu->handle(), views = std::move(Images.layers), map = std::move(Images.map)]() { + for (auto *view : views) + { + vkDestroyImageView(device, view, nullptr); + } + }); + Images = array_images{}; +} + +void terrain_materials::gpu_blank(slot const &Slot) +{ + std::uint8_t const blank[4]{0, 0, 0, 0}; + for (auto const handle : Slot.weights) + { + upload(handle, VK_FORMAT_R8G8B8A8_UNORM, 1, 1, blank, sizeof(blank), true, m_resources->sampling->clamp_linear); + } + std::array const zeros{}; + upload(Slot.table, VK_FORMAT_R32G32B32A32_SFLOAT, static_cast(max_layers), 2, zeros.data(), sizeof(zeros), false, m_resources->sampling->clamp_nearest); + // led between the samples, so the edges of the holes run smoothly across the cells + std::uint8_t const none{0}; + upload(Slot.holes, VK_FORMAT_R8_UNORM, 1, 1, &none, 1, false, m_resources->sampling->clamp_linear); +} + +void terrain_materials::gpu_weights(texture_handle const Texture, int const Side, std::uint8_t const *Texels) +{ + auto const side{static_cast(Side)}; + upload(Texture, VK_FORMAT_R8G8B8A8_UNORM, side, side, Texels, static_cast(side) * side * 4, true, m_resources->sampling->clamp_linear); +} + +void terrain_materials::gpu_table(texture_handle const Texture, float const *Table) +{ + upload(Texture, VK_FORMAT_R32G32B32A32_SFLOAT, static_cast(max_layers), 2, Table, max_layers * 2 * 4 * sizeof(float), false, m_resources->sampling->clamp_nearest); +} + +void terrain_materials::gpu_holes(texture_handle const Texture, int const Side, std::uint8_t const *Samples) +{ + auto const side{static_cast(Side)}; + upload(Texture, VK_FORMAT_R8_UNORM, side, side, Samples, static_cast(side) * side, false, m_resources->sampling->clamp_linear); +} + +void terrain_materials::begin_frame(command_stream *const Commands, std::uint64_t const Value) +{ + m_commands = Commands; + m_value = Value; + for (auto &images : m_images) + { + if ((false == images.clear) || (images.map == nullptr)) + { + continue; + } + images.clear = false; + // the layers not copied yet read black, as the storage of opengl starts out on most drivers + transition(Commands, *images.map, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_CLEAR_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT); + VkClearColorValue const zero{}; + VkImageSubresourceRange const range{VK_IMAGE_ASPECT_COLOR_BIT, 0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS}; + Commands->clear_color_image(images.map->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &zero, 1, &range); + transition(Commands, *images.map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); + } +} + +void terrain_materials::gpu_begin_copies() +{ + for (auto &images : m_images) + { + images.drawn = false; + } +} + +void terrain_materials::gpu_copy(texture_array const &Array, int const Layer, texture_source *Source, std::array const &Default) +{ + auto const index{static_cast(&Array - arrays().data())}; + auto &images{m_images[index]}; + if ((m_commands == nullptr) || (images.map == nullptr) || (static_cast(Layer) >= images.layers.size())) + { + return; + } + if (false == images.drawn) + { + images.drawn = true; + transition(m_commands, *images.map, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT); + } + auto const view{images.layers[Layer]}; + auto const format{images.map->desc().format}; + VkExtent2D const extent{images.map->desc().extent.width, images.map->desc().extent.height}; + if (Source != nullptr) + { + auto const *entry{m_resources->images->find(static_cast(*Source).get_id())}; + if (entry != nullptr) + { + m_draw(m_commands, view, format, extent, entry->target->view(), entry->sampler); + return; + } + } + // a map the layer doesn't have; one which failed reads as the texture 0 of opengl + VkRenderingAttachmentInfo attachment{}; + attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; + attachment.imageView = view; + attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + if (Source == nullptr) + { + std::copy(Default.begin(), Default.end(), attachment.clearValue.color.float32); + } + else + { + attachment.clearValue.color = VkClearColorValue{{0.f, 0.f, 0.f, 1.f}}; + } + VkRenderingInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + rendering.renderArea = {{0, 0}, extent}; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &attachment; + m_commands->begin_rendering(&rendering); + m_commands->end_rendering(); +} + +void terrain_materials::gpu_end_copies(bool const Copied) +{ + for (auto &images : m_images) + { + if ((false == images.drawn) || (images.map == nullptr)) + { + continue; + } + images.drawn = false; + if (Copied && (images.map->desc().mips > 1) && m_resources->uploads->can_blit(images.map->desc().format)) + { + make_mipmaps(*images.map); + } + else + { + transition(m_commands, *images.map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); + } + } +} + +// the other levels of every layer made by blits, each from the one before it (glGenerateMipmap of the opengl renderer) +void terrain_materials::make_mipmaps(image &Map) +{ + auto const commands{m_commands}; + auto const layers{Map.desc().layers}; + auto const mips{Map.desc().mips}; + auto const extent{Map.desc().extent}; + auto barrier = [&](std::uint32_t Level, std::uint32_t Count, VkImageLayout From, VkImageLayout To, VkPipelineStageFlags2 SrcStage, VkAccessFlags2 SrcAccess, VkPipelineStageFlags2 DstStage, VkAccessFlags2 DstAccess) { + VkImageMemoryBarrier2 entry{}; + entry.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; + entry.srcStageMask = SrcStage; + entry.srcAccessMask = SrcAccess; + entry.dstStageMask = DstStage; + entry.dstAccessMask = DstAccess; + entry.oldLayout = From; + entry.newLayout = To; + entry.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + entry.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + entry.image = Map.handle(); + entry.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, Level, Count, 0, layers}; + VkDependencyInfo dependency{}; + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency.imageMemoryBarrierCount = 1; + dependency.pImageMemoryBarriers = &entry; + commands->pipeline_barrier2(&dependency); + }; + // the first level drawn, the others written by the blits + barrier(0, 1, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); + barrier(1, mips - 1, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_2_NONE, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT); + for (std::uint32_t level = 1; level < mips; ++level) + { + if (level > 1) + { + barrier(level - 1, 1, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); + } + VkImageBlit2 blit{}; + blit.sType = VK_STRUCTURE_TYPE_IMAGE_BLIT_2; + blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level - 1, 0, layers}; + blit.srcOffsets[1] = {static_cast(std::max(extent.width >> (level - 1), 1u)), static_cast(std::max(extent.height >> (level - 1), 1u)), 1}; + blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, layers}; + blit.dstOffsets[1] = {static_cast(std::max(extent.width >> level, 1u)), static_cast(std::max(extent.height >> level, 1u)), 1}; + VkBlitImageInfo2 info{}; + info.sType = VK_STRUCTURE_TYPE_BLIT_IMAGE_INFO_2; + info.srcImage = Map.handle(); + info.srcImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + info.dstImage = Map.handle(); + info.dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + info.regionCount = 1; + info.pRegions = &blit; + info.filter = VK_FILTER_LINEAR; + commands->blit_image2(&info); + } + barrier(0, mips - 1, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_2_NONE, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); + barrier(mips - 1, 1, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); + Map.state = image_state{VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT}; +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14terrain.h b/rendering/vulkan14/vulkan14terrain.h new file mode 100644 index 00000000..9825a1bb --- /dev/null +++ b/rendering/vulkan14/vulkan14terrain.h @@ -0,0 +1,85 @@ +/* +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/. +*/ + +// the materials of the heightmap terrain in the vulkan renderer (the stage P6 of the plan, opengl46_terrain_materials): the +// textures of gfx::terrain_materials as images of the textures of the bank (vulkan14_texture::adopt()). the weights, the tables +// and the holes of the chunks go through the uploads; the layer maps are drawn into the layers of the arrays by the copy shader +// (Draw, given by the renderer) in the commands of the frame, the mipmaps of the arrays made by blits after them + +#pragma once + +#include "rendering/common/terrainmaterials.h" +#include "rendering/vulkan14/vulkan14texture.h" + +#include + +namespace vk14 +{ + +class terrain_materials : public gfx::terrain_materials +{ + public: + // the copy shader drawn into Target (a layer of an array in the layout of a colour attachment, of Format and Extent) from the + // image Source sampled with Sampler + using copy_draw = std::function; + + terrain_materials(material_manager &Materials, texture_bank &Textures) : gfx::terrain_materials(Materials, Textures) {} + ~terrain_materials() override; + + // called once, before anything is made + void init(resources const &Resources, copy_draw Draw); + // the views go (the device is idle) + void destroy(); + // the commands of the frame and the value of the timeline the gpu reaches when it's done with it: the arrays made since the last + // frame are cleared. before update() + void begin_frame(command_stream *Commands, std::uint64_t Value); + // the frame was recorded: no commands go to it any more + void end_frame() + { + m_commands = nullptr; + } + + protected: + bool gpu_init() override; + void gpu_texture(texture_source &Texture, texture_kind const Kind) override {} + void gpu_white(texture_source &Texture) override; + void gpu_storage(texture_array const &Array, int const Layers, bool const Fresh) override; + void gpu_blank(slot const &Slot) override; + void gpu_weights(texture_handle const Texture, int const Side, std::uint8_t const *Texels) override; + void gpu_table(texture_handle const Texture, float const *Table) override; + void gpu_holes(texture_handle const Texture, int const Side, std::uint8_t const *Samples) override; + void gpu_begin_copies() override; + void gpu_copy(texture_array const &Array, int const Layer, texture_source *Source, std::array const &Default) override; + void gpu_end_copies(bool const Copied) override; + + private: + struct array_images + { + std::shared_ptr map; + std::vector layers; // the first level of each layer, drawn to + bool clear{false}; // made since the last frame, its content not defined yet + bool drawn{false}; // a layer was drawn in the copies being made + }; + + vulkan14_texture &texture(texture_handle const Handle); + // an image of Format, Width x Height, a single layer, with Mips levels, sampled and uploaded to + std::shared_ptr make_image(VkFormat const Format, std::uint32_t const Width, std::uint32_t const Height, std::uint32_t const Mips, char const *Name); + // Data into the image of the texture, made anew of Format when there is none of the size; mipmaps made of the first level when Mips + void upload(texture_handle const Texture, VkFormat const Format, std::uint32_t const Width, std::uint32_t const Height, void const *Data, VkDeviceSize const Size, bool const Mips, VkSampler const Sampler); + void let_go(array_images &Images); + void make_mipmaps(image &Map); + + resources const *m_resources{nullptr}; + copy_draw m_draw; + command_stream *m_commands{nullptr}; + std::uint64_t m_value{0}; + std::array m_images; // of the arrays: diffuse, normal maps, specular/gloss +}; + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14texture.cpp b/rendering/vulkan14/vulkan14texture.cpp index 32e470e3..170d5597 100644 --- a/rendering/vulkan14/vulkan14texture.cpp +++ b/rendering/vulkan14/vulkan14texture.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/vulkan14/vulkan14texture.h" +#include "rendering/vulkan14/vulkan14commands.h" #include "rendering/vulkan14/vulkan14device.h" #include "utilities/Globals.h" #include "utilities/Logs.h" @@ -42,7 +43,7 @@ bool uploader::can_blit(VkFormat Format) const return result; } -void uploader::begin_frame(VkCommandBuffer Commands, std::uint64_t Value) +void uploader::begin_frame(command_stream *Commands, std::uint64_t Value) { m_commands = Commands; m_value = Value; @@ -57,7 +58,7 @@ void uploader::begin_frame(VkCommandBuffer Commands, std::uint64_t Value) bool uploader::end_frame() { - if (m_buffercopies && (m_commands != VK_NULL_HANDLE)) + if (m_buffercopies && (m_commands != nullptr)) { // the draws of the frame read what was copied VkMemoryBarrier2 barrier{}; @@ -70,10 +71,10 @@ bool uploader::end_frame() dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.memoryBarrierCount = 1; dependency.pMemoryBarriers = &barrier; - vkCmdPipelineBarrier2(m_commands, &dependency); + m_commands->pipeline_barrier2(&dependency); } m_buffercopies = false; - m_commands = VK_NULL_HANDLE; + m_commands = nullptr; return m_recorded; } @@ -99,7 +100,7 @@ bool uploader::upload_buffer(std::shared_ptr const &Target, void const * std::memcpy(upload.staging->mapped(), Data, static_cast(Size)); upload.staging->flush(0, Size); m_uploaded += Size; - if (m_commands != VK_NULL_HANDLE) + if (m_commands != nullptr) { record(upload); } @@ -125,7 +126,7 @@ bool uploader::upload(std::shared_ptr const &Image, void const *Data, VkD std::memcpy(upload.staging->mapped(), Data, static_cast(Size)); upload.staging->flush(0, Size); m_uploaded += Size; - if (m_commands != VK_NULL_HANDLE) + if (m_commands != nullptr) { record(upload); } @@ -153,7 +154,7 @@ void uploader::record(job &Job) dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.memoryBarrierCount = 1; dependency.pMemoryBarriers = &barrier; - vkCmdPipelineBarrier2(m_commands, &dependency); + m_commands->pipeline_barrier2(&dependency); m_buffercopies = true; } std::vector regions; @@ -174,7 +175,7 @@ void uploader::record(job &Job) copy.dstBuffer = Job.buffertarget->handle(); copy.regionCount = static_cast(regions.size()); copy.pRegions = regions.data(); - vkCmdCopyBuffer2(m_commands, ©); + m_commands->copy_buffer2(©); m_recorded = true; m_deletions->push(m_value, [staging = Job.staging, target = Job.buffertarget]() {}); return; @@ -204,7 +205,7 @@ void uploader::record(job &Job) copy.dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; copy.regionCount = static_cast(regions.size()); copy.pRegions = regions.data(); - vkCmdCopyBufferToImage2(commands, ©); + commands->copy_buffer_to_image2(©); auto const given{static_cast(Job.levels.size())}; if (Job.mipmaps && (mips > given) && (given > 0)) @@ -227,7 +228,7 @@ void uploader::record(job &Job) dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; dependency.imageMemoryBarrierCount = 1; dependency.pImageMemoryBarriers = &entry; - vkCmdPipelineBarrier2(commands, &dependency); + commands->pipeline_barrier2(&dependency); }; auto const extent{target.desc().extent}; for (std::uint32_t level = given; level < mips; ++level) @@ -249,7 +250,7 @@ void uploader::record(job &Job) info.regionCount = 1; info.pRegions = &blit; info.filter = VK_FILTER_LINEAR; - vkCmdBlitImage2(commands, &info); + commands->blit_image2(&info); } barrier(0, mips - 1, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_2_NONE, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); barrier(mips - 1, 1, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, @@ -628,6 +629,31 @@ void vulkan14_texture::apply_pending() } } +void vulkan14_texture::adopt(std::shared_ptr const &Image, VkSampler const Sampler) +{ + if (m_id != 0) + { + if (m_image != Image) + { + m_resources.deletions->push(m_resources.uploads->current_value(), [old = m_image]() {}); + } + m_resources.images->replace(m_id, Image, Sampler); + } + else + { + m_id = m_resources.images->add(Image, Sampler); + } + m_image = Image; + m_made = true; + if (Image) + { + data_width = Image->desc().extent.width; + data_height = Image->desc().extent.height; + } + is_ready = true; + is_static = true; +} + void vulkan14_texture::upload_rgba(std::uint8_t const *Data, std::size_t Width, std::size_t Height) { // as the opengl renderer: srgb rgba with mipmaps, the image made anew when the size changes diff --git a/rendering/vulkan14/vulkan14texture.h b/rendering/vulkan14/vulkan14texture.h index 16ecacf6..95ae4410 100644 --- a/rendering/vulkan14/vulkan14texture.h +++ b/rendering/vulkan14/vulkan14texture.h @@ -43,7 +43,7 @@ class uploader void init(allocator const &Allocator, deletion_queue &Deletions, VkPhysicalDevice Physical); // the commands of the uploads go to Commands until end_frame(); Value: the value of the timeline the gpu reaches when it's done // with the frame. the uploads waiting from between the frames are recorded first - void begin_frame(VkCommandBuffer Commands, std::uint64_t Value); + void begin_frame(command_stream *Commands, std::uint64_t Value); // whether anything was recorded to the commands of the frame bool end_frame(); // the last value of the timeline the gpu may use the objects with: the frame being made, or the last one sent @@ -82,7 +82,7 @@ class uploader allocator const *m_allocator{nullptr}; deletion_queue *m_deletions{nullptr}; VkPhysicalDevice m_physical{VK_NULL_HANDLE}; - VkCommandBuffer m_commands{VK_NULL_HANDLE}; + command_stream *m_commands{nullptr}; std::uint64_t m_value{0}; bool m_recorded{false}; // buffers were copied to in the frame: the barriers before the first copy and after the last one @@ -176,6 +176,13 @@ struct vulkan14_texture : public texture_source // the content given by update_from_memory() from another thread, uploaded by the thread of the renderer void apply_pending(); + // the image of a texture the renderer fills itself (the textures of the heightmap terrain): sampled with Sampler from now on, + // the image it had before goes once the gpu is done with it + void adopt(std::shared_ptr const &Image, VkSampler Sampler); + std::shared_ptr const &adopted() const + { + return m_image; + } protected: void on_failed() override; diff --git a/rendering/vulkan14/vulkan14weather.cpp b/rendering/vulkan14/vulkan14weather.cpp new file mode 100644 index 00000000..95b73b3b --- /dev/null +++ b/rendering/vulkan14/vulkan14weather.cpp @@ -0,0 +1,665 @@ +/* +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/. +*/ + +// the weather of the vulkan renderer (the stage P2 of the plan, opengl46_weather of the opengl renderer), drawn from the weather of +// the simulation by gfx::weather_state, which it shares with the opengl renderer: the block of the weather of every pass, the +// colours of the fog towards the horizon from the sky-view table of the atmosphere, the noises of the clouds, the shadows of the +// clouds, the clouds of the main view (blended over frames) and the panorama of the clouds for the other views, the fog volume +// of the main view, and the clouds and the fog laid over the sky + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14context.h" +#include "rendering/vulkan14/vulkan14commands.h" +#include "simulation/simulation.h" +#include "simulation/simulationenvironment.h" +#include "utilities/Globals.h" + +namespace +{ + +// sizes of the noises (clouds_noise.comp) +int const SHAPE_NOISE_SIZE{128}; +int const DETAIL_NOISE_SIZE{32}; +int const COVERAGE_NOISE_SIZE{512}; + +VkPipelineStageFlags2 const compute_stage{VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT}; +VkPipelineStageFlags2 const sampled_stage{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT}; +VkAccessFlags2 const sampled_access{VK_ACCESS_2_SHADER_SAMPLED_READ_BIT}; +VkAccessFlags2 const storage_write{VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT}; + +std::uint32_t groups(int const Size) +{ + return static_cast((Size + static_cast(gl::WEATHER_GROUP_SIZE) - 1) / static_cast(gl::WEATHER_GROUP_SIZE)); +} + +// a view of the first level of an image, for a kernel writing it +VkImageView first_level_view(VkDevice Device, vk14::image const &Image, std::string const &Name) +{ + VkImageViewCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + info.image = Image.handle(); + info.viewType = Image.desc().view_type; + info.format = Image.desc().format; + info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, Image.desc().layers}; + VkImageView view{VK_NULL_HANDLE}; + if (!VK14_VERIFY(vkCreateImageView(Device, &info, nullptr, &view))) + { + return VK_NULL_HANDLE; + } + vk14::name_object(Device, VK_OBJECT_TYPE_IMAGE_VIEW, vk14::handle_value(view), Name); + return view; +} + +// the push constants and the blocks of the kernels, laid out as those of the shaders +struct noise_constants +{ + std::int32_t kind; + std::int32_t size; +}; +struct horizon_constants +{ + std::int32_t offset; + std::int32_t pad[3]; + glm::vec4 params; + glm::vec3 white_balance; + float pad1; + glm::vec3 old_color; + float pad2; +}; +struct shadow_constants +{ + std::int32_t size; + std::int32_t pad[3]; + glm::vec3 placement; + std::int32_t steps; +}; +struct march_constants +{ + std::int32_t mode; + std::int32_t pad; + glm::ivec2 size; + glm::mat4 inverse_projection; + glm::mat4 view_to_world; + std::int32_t frame; + std::int32_t pad1[3]; + glm::vec4 quality; + glm::ivec2 rows; +}; +static_assert(offsetof(march_constants, quality) == 160, "the layout of the constants of clouds_march.comp"); +struct temporal_constants +{ + glm::ivec2 size; + glm::ivec2 pad; + glm::mat4 inverse_projection; + glm::mat4 view_to_world; + glm::mat4 previous_projection_view; + glm::vec3 previous_camera; + float blend; +}; +static_assert(sizeof(temporal_constants) == 224, "the layout of the constants of clouds_temporal.comp"); +struct inject_block +{ + glm::ivec3 grid; + std::int32_t pad; + glm::mat4 inverse_projection; + glm::mat4 view_to_world; + glm::vec4 slicing; + glm::mat4 previous_projection_view; + glm::vec3 previous_camera; + float pad1; + glm::vec4 options; + std::int32_t cablamp; + std::int32_t pad2; + glm::vec2 previous_slicing; + glm::vec2 jitter; +}; +static_assert(offsetof(inject_block, cablamp) == 256, "the layout of the block of fog_inject.comp"); +struct integrate_constants +{ + glm::ivec3 grid; + std::int32_t pad; + glm::mat4 inverse_projection; + glm::mat4 view_to_world; + glm::vec4 slicing; +}; + +} // namespace + +bool vulkan14_renderer::init_weather(std::string &Reason) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &weather{scene.weather}; + auto const device{ctx.device.handle()}; + // the noises, mipmapped and repeated + auto mipmapped = [&](std::shared_ptr &Image, bool const Volume, int const Size, char const *Name) { + vk14::image_desc desc; + desc.type = (Volume ? VK_IMAGE_TYPE_3D : VK_IMAGE_TYPE_2D); + desc.view_type = (Volume ? VK_IMAGE_VIEW_TYPE_3D : VK_IMAGE_VIEW_TYPE_2D); + desc.format = VK_FORMAT_R8G8B8A8_UNORM; + desc.extent = {static_cast(Size), static_cast(Size), Volume ? static_cast(Size) : 1u}; + desc.mips = static_cast(std::floor(std::log2(static_cast(Size)))) + 1; + desc.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + Image = std::make_shared(); + if (false == Image->create(ctx.memory, desc, Name)) + { + Image.reset(); + Reason = std::string("no image for ") + Name; + return false; + } + return true; + }; + if ((false == mipmapped(weather.shape_noise, true, SHAPE_NOISE_SIZE, "cloud shape noise")) || (false == mipmapped(weather.detail_noise, true, DETAIL_NOISE_SIZE, "cloud detail noise")) || + (false == mipmapped(weather.coverage_noise, false, COVERAGE_NOISE_SIZE, "cloud coverage noise"))) + { + return false; + } + { + vk14::image_desc desc; + desc.format = VK_FORMAT_R32_SFLOAT; + desc.usage = VK_IMAGE_USAGE_STORAGE_BIT; + weather.depth_dummy = std::make_shared(); + if (false == weather.depth_dummy->create(ctx.memory, desc, "clouds panorama depth")) + { + weather.depth_dummy.reset(); + Reason = "no image for the depth of the panorama"; + return false; + } + } + try + { + auto kernel = [&](vk14::compute_kernel &Kernel, char const *Shader, char const *Name) { + if (false == Kernel.create(device, ctx.pipelines, ctx.shaders.get(Shader), scene.pass_layout, {}, Name)) + { + Reason = std::string("no compute pipeline of ") + Name; + return false; + } + return true; + }; + if ((false == kernel(weather.noise, "clouds_noise.comp", "clouds noise")) || (false == kernel(weather.march, "clouds_march.comp", "clouds march")) || + (false == kernel(weather.temporal, "clouds_temporal.comp", "clouds temporal")) || (false == kernel(weather.shadow, "clouds_shadow.comp", "clouds shadow")) || + (false == kernel(weather.horizon, "fog_horizon.comp", "fog horizon")) || (false == kernel(weather.inject, "fog_inject.comp", "fog inject")) || (false == kernel(weather.integrate, "fog_integrate.comp", "fog integrate"))) + { + return false; + } + weather.sky = std::make_shared(); + weather.sky->vertex = ctx.shaders.get("quad.vert", "#define QUAD_SCENE 1\n"); + weather.sky->fragment = ctx.shaders.get("sky_weather.frag"); + weather.sky->fragment_name = "sky_weather.frag"; + weather.sky->vertices = vk14::vertex_kind::none; + weather.sky->name = "quad.vert + sky_weather.frag"; + } + catch (gfx::shader_exception const &e) + { + Reason = std::string("invalid shader: ") + e.what(); + return false; + } + return true; +} + +void vulkan14_renderer::destroy_weather() +{ + auto &ctx{*m_context}; + auto &weather{ctx.scene.weather}; + auto const device{ctx.device.handle()}; + for (auto *kernel : {&weather.noise, &weather.march, &weather.temporal, &weather.shadow, &weather.horizon, &weather.inject, &weather.integrate}) + { + kernel->destroy(device); + } + for (auto *image : {&weather.shape_noise, &weather.detail_noise, &weather.coverage_noise, &weather.depth_dummy, &weather.shadow_map, &weather.clouds_color, &weather.clouds_depth, &weather.clouds_history[0], &weather.clouds_history[1], &weather.panorama, + &weather.fog_scattering[0], &weather.fog_scattering[1], &weather.fog_integrated}) + { + image->reset(); + } + weather.sky.reset(); +} + +namespace +{ + +// an image of the weather made anew: the old one goes once the gpu is done with the frames which used it (Value) +std::shared_ptr weather_image(vk14::allocator &Memory, vk14::deletion_queue &Deletions, std::uint64_t const Value, std::shared_ptr &Image, VkImageType const Type, VkFormat const Format, glm::ivec3 const &Size, char const *Name) +{ + if (Image) + { + Deletions.push(Value, [old = std::move(Image)]() {}); + } + vk14::image_desc desc; + desc.type = Type; + desc.view_type = (Type == VK_IMAGE_TYPE_3D ? VK_IMAGE_VIEW_TYPE_3D : VK_IMAGE_VIEW_TYPE_2D); + desc.format = Format; + desc.extent = {static_cast(std::max(Size.x, 1)), static_cast(std::max(Size.y, 1)), static_cast(std::max(Size.z, 1))}; + desc.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + Image = std::make_shared(); + if (false == Image->create(Memory, desc, Name)) + { + Image.reset(); + } + return Image; +} + +} // namespace + +// the three noises, once (opengl46_weather::make_noise()) +void vulkan14_renderer::make_cloud_noise() +{ + auto &ctx{*m_context}; + auto &weather{ctx.scene.weather}; + auto const commands{ctx.commands}; + auto const device{ctx.device.handle()}; + ctx.passes.begin_pass("clouds noise", {vk14::use_of(*weather.shape_noise, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write), vk14::use_of(*weather.detail_noise, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write), + vk14::use_of(*weather.coverage_noise, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write)}); + // the kernel writes the first levels + auto const shape{first_level_view(device, *weather.shape_noise, "cloud shape noise level 0")}; + auto const detail{first_level_view(device, *weather.detail_noise, "cloud detail noise level 0")}; + auto const coverage{first_level_view(device, *weather.coverage_noise, "cloud coverage noise level 0")}; + auto const make = [&](int const Kind, VkImageView Volume, int const Size) { + weather.noise.dispatch(commands, {vk14::storage_image(Volume), vk14::storage_image(coverage)}, noise_constants{Kind, Size}, groups(Size), groups(Size), Kind == 2 ? 1u : static_cast(Size)); + vk14::memory_barrier(commands, compute_stage, storage_write, compute_stage, storage_write); + }; + make(0, shape, SHAPE_NOISE_SIZE); + make(1, detail, DETAIL_NOISE_SIZE); + make(2, shape, COVERAGE_NOISE_SIZE); + ctx.deletions.push(ctx.uploads.current_value(), [device, shape, detail, coverage]() { + vkDestroyImageView(device, shape, nullptr); + vkDestroyImageView(device, detail, nullptr); + vkDestroyImageView(device, coverage, nullptr); + }); + // the mipmaps, each level from the one before it + for (auto *image : {weather.shape_noise.get(), weather.detail_noise.get(), weather.coverage_noise.get()}) + { + auto const &desc{image->desc()}; + auto barrier = [&](std::uint32_t Level, VkImageLayout From, VkImageLayout To, VkPipelineStageFlags2 SrcStage, VkAccessFlags2 SrcAccess, VkPipelineStageFlags2 DstStage, VkAccessFlags2 DstAccess) { + VkImageMemoryBarrier2 entry{}; + entry.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; + entry.srcStageMask = SrcStage; + entry.srcAccessMask = SrcAccess; + entry.dstStageMask = DstStage; + entry.dstAccessMask = DstAccess; + entry.oldLayout = From; + entry.newLayout = To; + entry.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + entry.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + entry.image = image->handle(); + entry.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, Level, 1, 0, 1}; + VkDependencyInfo dependency{}; + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency.imageMemoryBarrierCount = 1; + dependency.pImageMemoryBarriers = &entry; + commands->pipeline_barrier2(&dependency); + }; + barrier(0, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, compute_stage, storage_write, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); + for (std::uint32_t level = 1; level < desc.mips; ++level) + { + barrier(level, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_NONE, VK_ACCESS_2_NONE, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT); + auto const extent = [&](std::uint32_t Level) { + return VkOffset3D{std::max(static_cast(desc.extent.width >> Level), 1), std::max(static_cast(desc.extent.height >> Level), 1), std::max(static_cast(desc.extent.depth >> Level), 1)}; + }; + VkImageBlit2 blit{}; + blit.sType = VK_STRUCTURE_TYPE_IMAGE_BLIT_2; + blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level - 1, 0, 1}; + blit.srcOffsets[1] = extent(level - 1); + blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, 1}; + blit.dstOffsets[1] = extent(level); + VkBlitImageInfo2 info{}; + info.sType = VK_STRUCTURE_TYPE_BLIT_IMAGE_INFO_2; + info.srcImage = image->handle(); + info.srcImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + info.dstImage = image->handle(); + info.dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + info.regionCount = 1; + info.pRegions = &blit; + info.filter = VK_FILTER_LINEAR; + commands->blit_image2(&info); + barrier(level, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_BLIT_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); + } + VkImageMemoryBarrier2 entry{}; + entry.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2; + entry.srcStageMask = VK_PIPELINE_STAGE_2_BLIT_BIT; + entry.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT; + entry.dstStageMask = sampled_stage; + entry.dstAccessMask = sampled_access; + entry.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + entry.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + entry.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + entry.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + entry.image = image->handle(); + entry.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, desc.mips, 0, 1}; + VkDependencyInfo dependency{}; + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency.imageMemoryBarrierCount = 1; + dependency.pImageMemoryBarriers = &entry; + commands->pipeline_barrier2(&dependency); + image->state = vk14::image_state{VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access}; + } + ctx.passes.end_pass(); + weather.noise_made = true; +} + +// the block of the weather of the pass to come, from gfx::weather_state (without the colours of the horizon, a block of their own) +void vulkan14_renderer::upload_weather_block() +{ + auto &ctx{*m_context}; + auto constexpr size{offsetof(gl::weather_ubs, fog_horizon)}; + auto const slice{ctx.ring.allocate(size, ctx.device.caps().limits.minUniformBufferOffsetAlignment)}; + if (false == bool(slice)) + { + return; + } + std::memcpy(slice.data, &m_weatherstate.block(), size); + m_draw->weather_offset = static_cast(slice.offset); + m_draw->set_dirty = true; +} + +// what the weather of the simulation is for the frame (opengl46_weather::update()): the block, the colours of the horizon, the +// shadows of the clouds, a band of the panorama. before the passes of the frame +void vulkan14_renderer::update_weather() +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &weather{scene.weather}; + auto const commands{ctx.commands}; + if (false == weather.noise_made) + { + make_cloud_noise(); + } + m_weatherstate.update(m_skylight.atmosphere_weight(), true, -m_sunlight.direction, glm::vec3{m_sunlight.diffuse} * simulation::Environment.light_intensity(), glm::vec3{m_sunlight.ambient}, m_fogcolor, Global.pCamera.Pos); + auto const shadows{m_weatherstate.shadows_wanted()}; + if (shadows) + { + if (weather.shadow_size != Global.gfx_clouds_shadows_size) + { + weather.shadow_size = Global.gfx_clouds_shadows_size; + weather_image(ctx.memory, ctx.deletions, ctx.uploads.current_value(), weather.shadow_map, VK_IMAGE_TYPE_2D, VK_FORMAT_R16_SFLOAT, glm::ivec3{weather.shadow_size, weather.shadow_size, 1}, "cloud shadow map"); + if (weather.shadow_map) + { + // read before it's first drawn (the pass sets hold it) + vk14::barriers(commands, {vk14::use_of(*weather.shadow_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); + } + } + m_weatherstate.place_shadows(weather.shadow_size, Global.pCamera.Pos); + } + upload_weather_block(); + + ctx.passes.begin_pass("weather"); + bind_compute_set(weather.horizon); + // the colours of the horizon: from the atmosphere on the gpu, the old fog colour otherwise + if (m_weatherstate.horizon_on_gpu()) + { + horizon_constants constants{}; + constants.params = glm::vec4{m_skylight.atmosphere_weight(), m_skylight.sky_scale(), 0.5f * m_skylight.overcast(), 64.f}; + constants.white_balance = m_skylight.white_balance(); + constants.old_color = m_fogcolor; + vk14::memory_barrier(commands, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_UNIFORM_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT, compute_stage, storage_write); + weather.horizon.dispatch(commands, {vk14::buffer_descriptor(scene.weather_horizon.handle(), 0, sizeof(glm::vec4) * gl::FOG_HORIZON_SAMPLES)}, constants, 1); + vk14::memory_barrier(commands, compute_stage, storage_write, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_UNIFORM_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_READ_BIT); + } + else + { + vk14::update_buffer(commands, scene.weather_horizon, &m_weatherstate.block().fog_horizon[0], sizeof(glm::vec4) * gl::FOG_HORIZON_SAMPLES, 0); + } + + if (shadows && weather.shadow_map) + { + vk14::barriers(commands, {vk14::use_of(*weather.shadow_map, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write)}); + shadow_constants constants{}; + constants.size = weather.shadow_size; + constants.placement = glm::vec3{m_weatherstate.block().cloud_shadow_params.x, m_weatherstate.block().cloud_shadow_params.y, Global.gfx_clouds_shadows_range}; + constants.steps = 8; + weather.shadow.dispatch(commands, {vk14::storage_image(weather.shadow_map->view())}, constants, groups(weather.shadow_size), groups(weather.shadow_size), 1); + vk14::barriers(commands, {vk14::use_of(*weather.shadow_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); + m_weatherstate.shadows_drawn(true); + upload_weather_block(); + bind_compute_set(weather.march); + } + else + { + m_weatherstate.shadows_drawn(false); + } + + // a band of the panorama of the clouds, all of it the first time + if (m_weatherstate.clouds_drawn()) + { + auto const level{gfx::weather_state::quality(Global.gfx_clouds, Global.gfx_fog_quality)}; + if ((weather.panorama_size != level.panorama) || (false == bool(weather.panorama))) + { + weather.panorama_size = level.panorama; + weather_image(ctx.memory, ctx.deletions, ctx.uploads.current_value(), weather.panorama, VK_IMAGE_TYPE_2D, VK_FORMAT_R16G16B16A16_SFLOAT, glm::ivec3{weather.panorama_size, 1}, "clouds panorama"); + m_weatherstate.panorama_resized(); + } + if (weather.panorama) + { + int first, rows; + m_weatherstate.panorama_band(weather.panorama_size, first, rows); + vk14::barriers(commands, {vk14::use_of(*weather.panorama, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write), vk14::use_of(*weather.depth_dummy, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write)}); + march_constants constants{}; + constants.mode = 1; + constants.size = weather.panorama_size; + constants.frame = m_weatherstate.frame(); + // the panorama is seen blurred: fewer samples + constants.quality = glm::vec4{static_cast(std::max(level.steps / 2, 24)), static_cast(std::max(level.light_steps - 1, 2)), Global.gfx_clouds_distance, 0.f}; + constants.rows = glm::ivec2{first, rows}; + // the depth isn't written in this mode, but something has to be bound + weather.march.dispatch(commands, {vk14::storage_image(weather.panorama->view()), vk14::storage_image(weather.depth_dummy->view())}, constants, groups(weather.panorama_size.x), groups(rows), 1); + vk14::barriers(commands, {vk14::use_of(*weather.panorama, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); + } + } + ctx.passes.end_pass(); +} + +// the clouds and the fog volume of the colour pass of the main view (opengl46_weather::render_view()): Projection and View +// (rotation, world axes to the view) of its camera, Size of the target, Cablamp the light of the cab lamp or -1. the lights, the +// clusters and the block of the scene of the pass are written, the shadow map drawn. outside a rendering +void vulkan14_renderer::render_weather_view(glm::mat4 const &Projection, glm::mat4 const &View, glm::dvec3 const &Camera, glm::ivec2 const &Size, int const Cablamp) +{ + auto &ctx{*m_context}; + auto &weather{ctx.scene.weather}; + auto const commands{ctx.commands}; + auto const level{gfx::weather_state::quality(Global.gfx_clouds, Global.gfx_fog_quality)}; + // the images of the clouds of the main view and of the fog volume, for the size and the quality + auto const cloudsize{glm::max((Size + level.divisor - 1) / level.divisor, glm::ivec2{1})}; + if (m_weatherstate.clouds_drawn() && ((cloudsize != weather.view_size) || (false == bool(weather.clouds_color)))) + { + weather.view_size = cloudsize; + weather_image(ctx.memory, ctx.deletions, ctx.uploads.current_value(), weather.clouds_color, VK_IMAGE_TYPE_2D, VK_FORMAT_R16G16B16A16_SFLOAT, glm::ivec3{cloudsize, 1}, "clouds colour"); + weather_image(ctx.memory, ctx.deletions, ctx.uploads.current_value(), weather.clouds_depth, VK_IMAGE_TYPE_2D, VK_FORMAT_R32_SFLOAT, glm::ivec3{cloudsize, 1}, "clouds depth"); + for (auto &history : weather.clouds_history) + { + weather_image(ctx.memory, ctx.deletions, ctx.uploads.current_value(), history, VK_IMAGE_TYPE_2D, VK_FORMAT_R16G16B16A16_SFLOAT, glm::ivec3{cloudsize, 1}, "clouds history"); + } + m_weatherstate.previous_clouds.valid = false; + } + if ((Global.gfx_fog == 1) && ((level.fog_grid != weather.fog_grid) || (false == bool(weather.fog_integrated)))) + { + weather.fog_grid = level.fog_grid; + for (auto &scattering : weather.fog_scattering) + { + weather_image(ctx.memory, ctx.deletions, ctx.uploads.current_value(), scattering, VK_IMAGE_TYPE_3D, VK_FORMAT_R16G16B16A16_SFLOAT, weather.fog_grid, "fog scattering"); + } + // one slice more: the fog through the whole volume without the clouds, which the sky takes (fog_integrate.comp) + weather_image(ctx.memory, ctx.deletions, ctx.uploads.current_value(), weather.fog_integrated, VK_IMAGE_TYPE_3D, VK_FORMAT_R16G16B16A16_SFLOAT, weather.fog_grid + glm::ivec3{0, 0, 1}, "fog volume"); + if (weather.fog_integrated) + { + // the pass sets hold it + vk14::barriers(commands, {vk14::use_of(*weather.fog_integrated, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); + } + m_weatherstate.previous_fog.valid = false; + } + auto const rotation{glm::mat4{glm::mat3{View}}}; + auto const inverseprojection{glm::inverse(Projection)}; + auto const viewtoworld{glm::transpose(rotation)}; + auto const projectionview{Projection * rotation}; + auto &previousclouds{m_weatherstate.previous_clouds}; + auto &previousfog{m_weatherstate.previous_fog}; + + if (m_weatherstate.clouds_drawn() && weather.clouds_color && weather.clouds_depth && weather.clouds_history[0] && weather.clouds_history[1]) + { + ctx.passes.begin_pass("clouds, fog volume", {vk14::use_of(*weather.clouds_color, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write), vk14::use_of(*weather.clouds_depth, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write)}); + bind_compute_set(weather.march); + march_constants constants{}; + constants.mode = 0; + constants.size = weather.view_size; + constants.inverse_projection = inverseprojection; + constants.view_to_world = viewtoworld; + constants.frame = m_weatherstate.frame(); + constants.quality = glm::vec4{static_cast(level.steps), static_cast(level.light_steps), Global.gfx_clouds_distance, level.detail ? 1.f : 0.f}; + constants.rows = glm::ivec2{0, weather.view_size.y}; + weather.march.dispatch(commands, {vk14::storage_image(weather.clouds_color->view()), vk14::storage_image(weather.clouds_depth->view())}, constants, groups(weather.view_size.x), groups(weather.view_size.y), 1); + + auto const next{1 - weather.clouds_current}; + auto &history{*weather.clouds_history[weather.clouds_current]}; + auto &result{*weather.clouds_history[next]}; + vk14::barriers(commands, {vk14::use_of(*weather.clouds_color, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access), + vk14::use_of(*weather.clouds_depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access), vk14::use_of(history, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access), + vk14::use_of(result, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write)}); + temporal_constants blend{}; + blend.size = weather.view_size; + blend.inverse_projection = inverseprojection; + blend.view_to_world = viewtoworld; + blend.previous_projection_view = previousclouds.projection_view; + blend.previous_camera = glm::vec3{previousclouds.camera - Camera}; + blend.blend = gfx::weather_state::continuity(previousclouds, Camera) ? 0.15f : 1.f; + auto const &linear{ctx.sampling.clamp_linear}; + auto const &nearest{ctx.sampling.clamp_nearest}; + weather.temporal.dispatch(commands, + {vk14::image_descriptor(linear, weather.clouds_color->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL), vk14::image_descriptor(nearest, weather.clouds_depth->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL), + vk14::image_descriptor(linear, history.view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL), vk14::storage_image(result.view())}, + blend, groups(weather.view_size.x), groups(weather.view_size.y), 1); + vk14::barriers(commands, {vk14::use_of(result, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); + weather.clouds_current = next; + previousclouds = {projectionview, Camera, {}, true}; + m_weatherstate.clouds_made(); + } + else + { + ctx.passes.begin_pass("clouds, fog volume"); + previousclouds.valid = false; + } + + if ((Global.gfx_fog == 1) && weather.fog_integrated && weather.fog_scattering[0] && weather.fog_scattering[1]) + { + bind_compute_set(weather.inject); + auto const temporal{Global.gfx_fog_temporal && gfx::weather_state::continuity(previousfog, Camera)}; + auto const jitter{m_weatherstate.fog_jitter(temporal)}; + auto const slicing{gfx::weather_state::fog_slicing(weather.fog_grid.z, jitter)}; + auto const next{1 - weather.fog_current}; + auto &history{*weather.fog_scattering[weather.fog_current]}; + auto &result{*weather.fog_scattering[next]}; + vk14::barriers(commands, {vk14::use_of(result, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write), vk14::use_of(history, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); + inject_block block{}; + block.grid = weather.fog_grid; + block.inverse_projection = inverseprojection; + block.view_to_world = viewtoworld; + block.slicing = slicing; + block.previous_projection_view = previousfog.projection_view; + block.previous_camera = glm::vec3{previousfog.camera - Camera}; + auto const sunshadows{Global.gfx_fog_shadows && Global.gfx_shadowmap_enabled}; + auto const share{temporal ? std::clamp(Global.gfx_fog_temporal_share, 0.02f, 1.f) : 1.f}; + block.options = glm::vec4{share, Global.gfx_fog_lights, sunshadows ? 1.f : 0.f, Global.gfx_fog_shadows ? 1.f : 0.f}; + block.cablamp = Cablamp; + block.previous_slicing = glm::vec2{previousfog.slicing.x, previousfog.slicing.z}; + block.jitter = glm::vec2{jitter.x, jitter.y}; + auto const slice{ctx.ring.allocate(sizeof(block), ctx.device.caps().limits.minUniformBufferOffsetAlignment)}; + if (slice) + { + std::memcpy(slice.data, &block, sizeof(block)); + weather.inject.run(commands, + {vk14::storage_image(result.view()), vk14::image_descriptor(ctx.sampling.clamp_linear, history.view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL), vk14::buffer_descriptor(slice.buffer, slice.offset, sizeof(block))}, + groups(weather.fog_grid.x), groups(weather.fog_grid.y), static_cast(weather.fog_grid.z)); + } + vk14::barriers(commands, {vk14::use_of(result, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access), vk14::use_of(*weather.fog_integrated, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write)}); + integrate_constants integrate{}; + integrate.grid = weather.fog_grid; + integrate.inverse_projection = inverseprojection; + integrate.view_to_world = viewtoworld; + integrate.slicing = slicing; + weather.integrate.dispatch(commands, {vk14::image_descriptor(ctx.sampling.clamp_linear, result.view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL), vk14::storage_image(weather.fog_integrated->view())}, integrate, + groups(weather.fog_grid.x), groups(weather.fog_grid.y), 1); + vk14::barriers(commands, {vk14::use_of(*weather.fog_integrated, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); + weather.fog_current = next; + previousfog = {projectionview, Camera, slicing, true}; + m_weatherstate.fog_made(slicing, weather.fog_grid); + } + else + { + previousfog.valid = false; + } + ctx.passes.end_pass(); +} + +// the clouds and the fog over the sky, in the sky pass (opengl46_weather::draw_sky()). Mainview: the clouds of the main view, the +// panorama otherwise +void vulkan14_renderer::render_sky_weather(glm::mat4 const &Projection, bool const Mainview) +{ + auto &ctx{*m_context}; + auto &weather{ctx.scene.weather}; + auto &draw{*m_draw}; + if (false == bool(weather.sky)) + { + return; + } + int mode{0}; + vk14::image *image{nullptr}; + if (m_weatherstate.clouds_drawn()) + { + if (Mainview && m_weatherstate.clouds_view_valid() && weather.clouds_history[weather.clouds_current]) + { + mode = 1; + image = weather.clouds_history[weather.clouds_current].get(); + } + else if (weather.panorama) + { + mode = 2; + image = weather.panorama.get(); + } + } + // over the sky as it is: (1, source alpha), without the depth + auto const glare{draw.glare}; + draw.glare = true; + if (false == bind_scene_pipeline(*weather.sky, *draw.targets, vk14::blend_mode::under_transmittance, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP)) + { + draw.glare = glare; + return; + } + auto const cull{draw.cullface}; + draw.cullface = false; + apply_draw_state(); + draw.cullface = cull; + draw.glare = glare; + draw.block = push_draw_block(); + if (draw.block == ~0u) + { + return; + } + if (draw.set_dirty) + { + bind_pass_set(); + } + // the clouds pushed as the first texture of the draw, the textures of the walk pushed again for the draws which follow + std::array images; + auto const &scene{ctx.scene}; + images.fill(VkDescriptorImageInfo{ctx.sampling.clamp_linear, scene.white->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}); + if (image != nullptr) + { + // the panorama repeats around the camera + images[0] = VkDescriptorImageInfo{(mode == 2 ? ctx.sampling.panorama : ctx.sampling.clamp_linear), image->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + } + scene.material_set.push(draw.commands, scene.scene_layout, images.data()); + draw.textures_dirty = true; + vk14::draw_constants constants; + constants.draw = draw.block; + auto const inverseprojection{glm::inverse(Projection)}; + for (int column = 0; column < 4; ++column) + { + constants.data[column] = inverseprojection[column]; + } + constants.data[4] = glm::vec4{static_cast(mode), 0.f, 0.f, 0.f}; + push_constants(&constants); + draw.commands->draw(4, 1, 0, 0); +} diff --git a/shaders/vk14/apply_fog.glsl b/shaders/vk14/apply_fog.glsl index be00979e..6f057d25 100644 --- a/shaders/vk14/apply_fog.glsl +++ b/shaders/vk14/apply_fog.glsl @@ -1,20 +1,21 @@ -// fog between the camera and a fragment, the api of shaders/ogl46/apply_fog.glsl. the fog volume and the weather of the opengl -// renderer come with the stage P2 of the plan; until then the fog is analytic: exponential with the distance, of the density of -// the draw (fog_density of the model block, a draw with 0 is not fogged), of the colour fog_color of the light buffer, turned -// towards the colour of the sun around its direction +// fog between the camera and a fragment, see fog_volume.glsl and weather_common.glsl. a draw with a fog_density of 0 is not +// fogged; one with another density than the regular one (the glare of lamps) gets the optical depth scaled by the ratio +#include "fog_volume.glsl" + // rgb: light the fog adds, a: what it lets through vec4 fog_at_fragment() { if (fog_density <= 0.0) return vec4(0.0, 0.0, 0.0, 1.0); - float amount = 1.0 - clamp(exp(-length(f_pos.xyz) * fog_density), 0.0, 1.0); - vec3 color = fog_color; - if ((lights_count >= 1U) && (lights[0].type == LIGHT_DIR)) + vec4 fog = fog_along_view(f_pos.xyz, gl_FragCoord.xy); + float scale = fog_density / max(fog_light.w, 1e-12); + if (abs(scale - 1.0) > 1e-3) { - float sunward = pow(max(dot(normalize(f_pos.xyz), normalize(-lights[0].dir)), 0.0), 30.0); - color = mix(color, lights[0].color, sunward); + float transmittance = exp(log(max(fog.a, 1e-8)) * scale); + fog.rgb *= (1.0 - transmittance) / max(1.0 - fog.a, 1e-4); + fog.a = transmittance; } - return vec4(color * amount, 1.0 - amount); + return fog; } vec3 get_fog_color() diff --git a/shaders/vk14/atmosphere_ambient.comp b/shaders/vk14/atmosphere_ambient.comp new file mode 100644 index 00000000..9398d9aa --- /dev/null +++ b/shaders/vk14/atmosphere_ambient.comp @@ -0,0 +1,130 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// 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 + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" +#include "atmosphere_common.glsl" + +layout(set = COMPUTE_SET, binding = 0) uniform sampler2D atmo_transmittance_lut; +layout(set = COMPUTE_SET, binding = 1) uniform sampler2D atmo_skyview_lut; + +layout(local_size_x = ATMOSPHERE_AMBIENT_GROUP_SIZE) in; + +layout(std430, set = COMPUTE_SET, binding = 2) 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/vk14/atmosphere_common.glsl b/shaders/vk14/atmosphere_common.glsl new file mode 100644 index 00000000..e5cb4177 --- /dev/null +++ b/shaders/vk14/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, set = PASS_SET, binding = ATMOSPHERE_UBO) 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/vk14/atmosphere_integrate.glsl b/shaders/vk14/atmosphere_integrate.glsl new file mode 100644 index 00000000..596ea1ab --- /dev/null +++ b/shaders/vk14/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/vk14/atmosphere_multiscattering.comp b/shaders/vk14/atmosphere_multiscattering.comp new file mode 100644 index 00000000..2120fe19 --- /dev/null +++ b/shaders/vk14/atmosphere_multiscattering.comp @@ -0,0 +1,72 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// 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 + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" +#include "atmosphere_common.glsl" + +layout(set = COMPUTE_SET, binding = 0) uniform sampler2D atmo_transmittance_lut; +#include "atmosphere_integrate.glsl" + +layout(local_size_x = 64) in; + +layout(set = COMPUTE_SET, binding = 1, 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/vk14/atmosphere_skyview.comp b/shaders/vk14/atmosphere_skyview.comp new file mode 100644 index 00000000..66adbfe3 --- /dev/null +++ b/shaders/vk14/atmosphere_skyview.comp @@ -0,0 +1,42 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// 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 + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" +#include "atmosphere_common.glsl" + +layout(set = COMPUTE_SET, binding = 0) uniform sampler2D atmo_transmittance_lut; +layout(set = COMPUTE_SET, binding = 1) uniform sampler2D atmo_multiscattering_lut; +#define ATMO_MULTISCATTERING_LUT +#include "atmosphere_integrate.glsl" + +layout(local_size_x = 8, local_size_y = 8) in; + +layout(set = COMPUTE_SET, binding = 2, 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/vk14/atmosphere_transmittance.comp b/shaders/vk14/atmosphere_transmittance.comp new file mode 100644 index 00000000..95e4d201 --- /dev/null +++ b/shaders/vk14/atmosphere_transmittance.comp @@ -0,0 +1,43 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// 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 + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" +#include "atmosphere_common.glsl" + +layout(local_size_x = 8, local_size_y = 8) in; + +layout(set = COMPUTE_SET, binding = 0, 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/vk14/clouds_common.glsl b/shaders/vk14/clouds_common.glsl new file mode 100644 index 00000000..54cf3f8e --- /dev/null +++ b/shaders/vk14/clouds_common.glsl @@ -0,0 +1,291 @@ +// clouds of the weather (opengl46_weather): density of the layers of weather_common.glsl, made of three tiling noises. +// the shape noise (3d, clouds_noise.comp) gives the lumps of a cloud, the detail noise (3d) eats into their edges, the coverage +// noise (2d) spreads the clouds of a layer over the sky so they take up its coverage. positions are world offsets from the +// camera, in metres, y up; the earth curves away from the camera, so far clouds sink below the horizon +#include "weather_common.glsl" + +layout(set = PASS_SET, binding = CLOUD_SHAPE_TEX) uniform sampler3D cloud_shape_noise; +layout(set = PASS_SET, binding = CLOUD_DETAIL_TEX) uniform sampler3D cloud_detail_noise; +layout(set = PASS_SET, binding = CLOUD_COVERAGE_TEX) uniform sampler2D cloud_coverage_noise; + +const float CLOUD_EARTH_RADIUS = 6360000.0; +// how much of the shape noise is left standing at the edge and in the core of a cloud area (cloud_layer_density) +const float CLOUD_FILL_EDGE = 0.45; +const float CLOUD_FILL_CORE = 0.85; +// how fast the density rises inside the edge of a cloud: above 1 the edges are crisper, the inside keeps its density +const float CLOUD_EDGE_SHARPNESS = 3.0; +// how far the base and the top of the single clouds stand off those of their layer, up or down, as shares of the depth of the layer +// and at most in metres: the clouds of a layer don't all end at one height, some reach a little out of it +// how high the base of a cloud curves up at its edge and how far into the cloud area (cloud_coverage: y) it does, and how much higher +// the base of a billow is between the cells of the shape noise than in their middles; shares of the height of the cloud +const float CLOUD_BASE_ROUND = 0.35; +const float CLOUD_BASE_ROUND_DEPTH = 0.3; +const float CLOUD_BASE_LUMPS = 0.08; +const float CLOUD_BASE_SHIFT = 0.07; +const float CLOUD_BASE_SHIFT_MAX = 100.0; +const float CLOUD_TOP_SHIFT = 0.15; +const float CLOUD_TOP_SHIFT_MAX = 400.0; +// the detail noise: its tile against the tile of the shape noise and how deep it eats into the edges (times the erosion of the +// genus and the tuning of the debug window, cloud_wind.w); the finer octave of the thin edges: its tile against the detail tile, +// how far the coarser octave pushes its lookup about (in its tiles), and the density below which it is used +const float CLOUD_DETAIL_SCALE = 6.0; +const float CLOUD_EROSION = 2.0; +const float CLOUD_FINE_SCALE = 3.7; +const float CLOUD_FINE_PUSH = 0.6; +const float CLOUD_FINE_BELOW = 0.6; + +float cloud_remap(float value, float low, float high, float newlow, float newhigh) +{ + return newlow + (value - low) / max(high - low, 1e-5) * (newhigh - newlow); +} + +// height of a point above the ground reference, the earth curving down with the square of the distance +float cloud_height(vec3 position) +{ + return weather_camera.x + position.y - dot(position.xz, position.xz) * (0.5 / CLOUD_EARTH_RADIUS); +} + +// distances along a ray (unit Direction from the camera) where it is at Height: the height goes as +// camera + up * t - (1 - up^2) / (2 R) * t^2. returns the number of crossings ahead (0-2), sorted into T +int cloud_ray_height(float up, float height, out vec2 t) +{ + t = vec2(-1.0); + float a = (1.0 - up * up) * (0.5 / CLOUD_EARTH_RADIUS); + float b = -up; + float c = height - weather_camera.x; + // a t^2 + b t + c = 0 + if (a < 1e-12) + { + if (abs(b) < 1e-8) + return 0; + float root = -c / b; + t = vec2(root, -1.0); + return root > 0.0 ? 1 : 0; + } + float discriminant = b * b - 4.0 * a * c; + if (discriminant < 0.0) + return 0; + float q = -0.5 * (b + (b >= 0.0 ? 1.0 : -1.0) * sqrt(discriminant)); + float r0 = q / a; + float r1 = (abs(q) > 1e-12) ? c / q : r0; + vec2 roots = vec2(min(r0, r1), max(r0, r1)); + if (roots.y <= 0.0) + return 0; + if (roots.x <= 0.0) + { + t = vec2(roots.y, -1.0); + return 1; + } + t = roots; + return 2; +} + +// the first stretch of a ray within the heights of a layer, x to y; y <= x when it never gets there +vec2 cloud_layer_segment(float up, float base, float top) +{ + vec2 tb, tt; + int nb = cloud_ray_height(up, base, tb); + int nt = cloud_ray_height(up, top, tt); + // the crossings in order; the ray goes in and out of the layer at each + float crossings[4]; + int count = 0; + for (int i = 0; i < nb; i++) + crossings[count++] = tb[i]; + for (int i = 0; i < nt; i++) + crossings[count++] = tt[i]; + for (int i = 1; i < count; i++) + for (int j = i; j > 0 && crossings[j - 1] > crossings[j]; j--) + { + float swap = crossings[j]; + crossings[j] = crossings[j - 1]; + crossings[j - 1] = swap; + } + bool inside = (weather_camera.x >= base) && (weather_camera.x <= top); + float start = 0.0; + for (int i = 0; i < count; i++) + { + if (inside) + return vec2(start, crossings[i]); + inside = true; + start = crossings[i]; + } + return inside ? vec2(start, 1e9) : vec2(0.0, -1.0); +} + +// the position in the noise of a layer: the camera's place in it and the offset from the camera, stretched along the wind +vec2 cloud_noise_plane(cloud_layer_s layer, vec2 offset) +{ + vec2 wind = cloud_wind.xy; + float along = dot(offset, wind); + vec2 across = offset - wind * along; + return across + wind * (along / max(layer.motion.w, 1.0)); +} + +// how far the base (x) and the top (y) of the clouds of a layer may stand off those of the layer, metres +vec2 cloud_layer_shift(cloud_layer_s layer) +{ + float thickness = layer.heights.y - layer.heights.x; + return vec2(min(thickness * CLOUD_BASE_SHIFT, CLOUD_BASE_SHIFT_MAX), min(thickness * CLOUD_TOP_SHIFT, CLOUD_TOP_SHIFT_MAX)); +} + +// the lowest base (x) and the highest top (y) the clouds of a layer may reach +vec2 cloud_layer_bounds(cloud_layer_s layer) +{ + vec2 shift = cloud_layer_shift(layer); + return vec2(layer.heights.x - shift.x, layer.heights.y + shift.y); +} + +// share of the layer filled at a place, x: the soft mask of the coverage, y: how far into a cloud area (0 at its edge). Shift: how +// far the base (x) and the top (y) of the cloud there stand off those of the layer, from the second and the third noise of the +// coverage texture, so the clouds which reach lower don't all reach higher too +vec2 cloud_coverage(cloud_layer_s layer, vec2 offset, out vec2 shift) +{ + vec2 coords = (layer.extra.zw + cloud_noise_plane(layer, offset)) * layer.motion.z; + vec3 noise = textureLod(cloud_coverage_noise, coords + layer.extra.y, 0.0).rgb; + shift = (noise.yz * 2.0 - 1.0) * cloud_layer_shift(layer); + float coverage = layer.shape.x; + float threshold = 1.0 - coverage; + float mask = smoothstep(threshold - 0.06, threshold + 0.06, noise.x); + float depth = clamp((noise.x - threshold) / max(coverage, 0.05), 0.0, 1.0); + return vec2(mask, depth); +} + +vec2 cloud_coverage(cloud_layer_s layer, vec2 offset) +{ + vec2 shift; + return cloud_coverage(layer, offset, shift); +} + +// extinction (1/m) of a volumetric layer at a point; Detail eats the edges with the small noise +float cloud_layer_density(int index, vec3 position, float height, bool detail) +{ + cloud_layer_s layer = cloud_layers[index]; + vec2 bounds = cloud_layer_bounds(layer); + if (height <= bounds.x || height >= bounds.y) + return 0.0; + vec2 shift; + vec2 coverage = cloud_coverage(layer, position.xz, shift); + float bottom = layer.heights.x + shift.x; + float h = (height - bottom) / max(layer.heights.y + shift.y - bottom, 1.0); + if (h <= 0.0 || h >= 1.0) + return 0.0; + // the anvil spreads the top of the storm clouds out of their area + if (layer.profile.w > 0.0) + { + float anvil = layer.profile.w * smoothstep(0.7, 0.92, h) * (1.0 - smoothstep(0.97, 1.0, h)); + coverage.x = max(coverage.x, anvil * smoothstep(0.0, 0.5, coverage.y + 0.35)); + coverage.y = max(coverage.y, anvil * 0.6); + } + if (coverage.x <= 0.0) + return 0.0; + // the tops of the single clouds are of different heights, the larger ones (deeper in their area) the taller + float top = mix(1.0 - layer.profile.z, 1.0, coverage.y); + float fall = 1.0 - smoothstep(top * layer.profile.y, top, h); + if (fall <= 0.0) + return 0.0; + vec2 plane = layer.motion.xy + cloud_noise_plane(layer, position.xz); + vec3 coords = vec3(plane.x, height + layer.extra.y * 4096.0, plane.y) * layer.shape.z; + vec4 shape = textureLod(cloud_shape_noise, coords, 0.0); + // the smaller cells of the worley octaves bite billows into the heaps of the perlin-worley noise; the octaves keep to a narrow + // band (clouds_noise.comp: WORLEY_FBM_LOW, WORLEY_FBM_HIGH), spread over 0-1 first + float worley = clamp((shape.g * 0.625 + shape.b * 0.25 + shape.a * 0.125 - 0.30) / 0.35, 0.0, 1.0); + // the base is not cut flat: it curves up towards the edge of the cloud into its sides, and every billow starts a little higher or + // lower; both as shares of the height of the cloud, so the small ones are rounded too and not cut away. the sheets (little + // erosion) keep a flatter base + float lift = (CLOUD_BASE_ROUND * (1.0 - smoothstep(0.0, CLOUD_BASE_ROUND_DEPTH, coverage.y)) + CLOUD_BASE_LUMPS * (1.0 - worley)) * top * clamp(layer.shape.w * 2.5, 0.0, 1.0); + float gradient = smoothstep(lift, lift + layer.profile.x, h) * fall; + if (gradient <= 0.0) + return 0.0; + float base = clamp(cloud_remap(shape.r, (1.0 - worley) * 0.35, 1.0, 0.0, 1.0), 0.0, 1.0); + // the sheets (little erosion) keep few holes, the heaps are made of lumps + base = mix(1.0, base, clamp(layer.shape.w * 2.5, 0.25, 1.0)); + // inside its area a cloud is not a solid block: the lumps of the shape noise are cut out of it, the more the nearer its edge, + // so it breaks up into single heaps with rounded tops and gaps between them. the sheets keep their base above the cut, and a + // layer closing up (broken to overcast) keeps its lumps whole, so it has no more gaps than its coverage + float fill = coverage.x * mix(mix(CLOUD_FILL_EDGE, CLOUD_FILL_CORE, coverage.y), 1.0, smoothstep(0.5, 1.0, layer.shape.x)); + float density = clamp(cloud_remap(base * gradient, 1.0 - fill, 1.0, 0.0, 1.0), 0.0, 1.0) * coverage.x; + if (detail && density > 0.0 && layer.shape.w > 0.0) + { + vec3 detailcoords = coords * CLOUD_DETAIL_SCALE; + vec4 small = textureLod(cloud_detail_noise, detailcoords, 0.0); + // spread over 0-1 as the worley octaves of the shape; left in their narrow band they would only thin the cloud evenly + float fbm = clamp((small.r * 0.625 + small.g * 0.25 + small.b * 0.125 - 0.30) / 0.35, 0.0, 1.0); + // the thin outer part of a cloud gets a finer octave too, its lookup pushed about by the coarser one, so the edges are frayed + // into small billows and torn bits rather than evenly rounded. the dense inside is never eaten into, so it is skipped there + float fineshare = 1.0 - smoothstep(CLOUD_FINE_BELOW * 0.5, CLOUD_FINE_BELOW, density); + if (fineshare > 0.0) + { + vec3 push = (small.gbr - 0.5) * CLOUD_FINE_PUSH; + vec4 fine = textureLod(cloud_detail_noise, detailcoords * CLOUD_FINE_SCALE + push, 0.0); + float finefbm = clamp((fine.r * 0.625 + fine.g * 0.25 + fine.b * 0.125 - 0.30) / 0.35, 0.0, 1.0); + fbm = clamp(mix(fbm, fbm * 0.65 + finefbm * 0.35, fineshare), 0.0, 1.0); + } + // wispy at the base, billowing at the top + float erosion = mix(fbm, 1.0 - fbm, clamp(h * 4.0, 0.0, 1.0)); + density = clamp(cloud_remap(density, erosion * layer.shape.w * CLOUD_EROSION * cloud_wind.w, 1.0, 0.0, 1.0), 0.0, 1.0); + } + return min(density * CLOUD_EDGE_SHARPNESS, 1.0) * layer.shape.y; +} + +// the same without the detail, cheaper, for the shadows and the fog +float cloud_density_coarse(vec3 position) +{ + float height = cloud_height(position); + float density = 0.0; + for (int i = 0; i < int(weather_camera.w); i++) + { + if (cloud_layers[i].extra.x > 0.5) + continue; + density += cloud_layer_density(i, position, height, false); + } + return density; +} + +// optical depth of a flat layer (cirrus and the like) where a ray crosses its middle, Up the vertical part of the ray there +float cloud_flat_depth(int index, vec3 position) +{ + cloud_layer_s layer = cloud_layers[index]; + vec2 coverage = cloud_coverage(layer, position.xz); + if (coverage.x <= 0.0) + return 0.0; + vec2 plane = layer.motion.xy + cloud_noise_plane(layer, position.xz); + vec2 coords = plane * layer.shape.z; + // strands and ripples out of slices of the noise + vec4 shape = textureLod(cloud_shape_noise, vec3(coords, 0.37 + layer.extra.y), 0.0); + vec4 small = textureLod(cloud_detail_noise, vec3(coords * 4.0, 0.61), 0.0); + float strands = clamp(cloud_remap(shape.r * 0.7 + small.r * 0.3, 0.35 * layer.shape.w + (1.0 - coverage.x) * 0.5, 1.0, 0.0, 1.0), 0.0, 1.0); + return strands * coverage.x * layer.shape.y; +} + +// the shadow of the clouds at a point: what they let through towards the key light. Steps per layer +float cloud_transmittance_to_sun(vec3 position, int steps) +{ + vec3 sun = weather_sun_direction.xyz; + if (sun.y <= 0.01) + return fog_light.y; + float height = cloud_height(position); + float depth = 0.0; + for (int i = 0; i < int(weather_camera.w); i++) + { + cloud_layer_s layer = cloud_layers[i]; + if (layer.extra.x > 0.5) + { + float middle = 0.5 * (layer.heights.x + layer.heights.y); + if (middle > height) + depth += cloud_flat_depth(i, position + sun * ((middle - height) / sun.y)) / sun.y; + continue; + } + vec2 bounds = cloud_layer_bounds(layer); + float enter = max(bounds.x - height, 0.0) / sun.y; + float leave = (bounds.y - height) / sun.y; + if (leave <= 0.0) + continue; + float step = (leave - enter) / float(steps); + for (int s = 0; s < steps; s++) + { + vec3 point = position + sun * (enter + step * (float(s) + 0.5)); + depth += cloud_layer_density(i, point, cloud_height(point), false) * step; + } + } + return exp(-depth); +} diff --git a/shaders/vk14/clouds_march.comp b/shaders/vk14/clouds_march.comp new file mode 100644 index 00000000..01541963 --- /dev/null +++ b/shaders/vk14/clouds_march.comp @@ -0,0 +1,295 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// clouds of the weather ray marched through their layers (clouds_common.glsl), into an image the sky pass lays over the sky +// (sky_weather.frag). mode 0: the view of the main camera at a part of the resolution, smoothed over frames by +// clouds_temporal.comp; mode 1: a panorama around the camera (azimuth across, elevation up), a band of rows per frame, for the +// environment map and the other views. rgb: the light of the clouds with the fog in front of them, a: what they let through +// of the sky behind; the depth image gets the distance of the clouds, for the reprojection + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" +#include "clouds_common.glsl" + +layout(local_size_x = WEATHER_GROUP_SIZE, local_size_y = WEATHER_GROUP_SIZE, local_size_z = 1) in; + +layout(rgba16f, set = COMPUTE_SET, binding = 0) uniform writeonly image2D clouds_color; +layout(r32f, set = COMPUTE_SET, binding = 1) uniform writeonly image2D clouds_depth; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + int mode; + ivec2 size; + // mode 0: clip space to view space of the camera, and view space to world axes + mat4 inverse_projection; + mat4 view_to_world; + int frame; + // x: samples through a layer at most, y: samples towards the sun, z: farthest distance (m), w: 1 with the detail noise + vec4 quality; + // mode 1: first row of the band and the rows in it + ivec2 rows; +}; + +// lowest elevation of the panorama (radians); its rows go with the square of the height, denser at the horizon +const float PANORAMA_LOW = -0.1; +// scales of the light of the clouds against the shading of the surfaces (light_common.glsl: SUN_DIFFUSE_SCALE, AMBIENT_SCALE), +// times the tuning of the debug window (weather_sun_color.w, weather_ambient.w). the sunlight three times that of the sunlit faces, +// set by eye in the simulator: the sunlit sides of real cumulus are several times brighter than the sky around them. the sky light +// about as the shaded faces get it: more of it greys the shaded sides of the clouds into a flat pastel +const float CLOUD_SUN_SCALE = 4.5; +const float CLOUD_AMBIENT_SCALE = 0.6; +// the ground under the clouds lights their bases back with this share of the light falling on it +const float CLOUD_GROUND_ALBEDO = 0.2; +// how fast the light of the sky fades into a cloud, per optical depth of the cloud below a point, or above it past CLOUD_SKY_REACH +const float CLOUD_AMBIENT_FADE = 0.05; +// the part of the cloud over a point looked at for the sky light held back (cloud_sky_depth), metres, how fast the light fades +// per optical depth there (less than the extinction: the light scattered many times gets through further), and the share left +// in the darkest places +const float CLOUD_SKY_REACH = 400.0; +const float CLOUD_SKY_FADE = 0.25; +const float CLOUD_SKY_FLOOR = 0.15; +// how dense a cloud has to be to scatter the sunlight back at full: the thin edges and wisps of the side lit by the sun, seen with the +// sun behind, are darker than its bulk (the "powder" effect: the light takes a few scatterings to turn back towards the view) +const float CLOUD_POWDER = 6.0; +// how many of the nearest steps towards the sun see the detail noise +const int CLOUD_LIGHT_DETAIL_STEPS = 1; +// orders of the scattering the sunlight is followed through (cloud_sunlight): the higher ones reach deep into a thick cloud, so its +// shaded sides and its inside are lit through the cloud too, not only by the sky, and a big cloud close by is not a flat grey +const int CLOUD_SCATTER_OCTAVES = 6; +// how much longer every next step towards the sun is than the one before: 1 + this / steps², so the first step is short where the +// light falls off fastest (about a 30th of the reach at 4 steps) and the steps get close to even when there are many of them +const float CLOUD_LIGHT_GROWTH = 24.0; + +float henyey_greenstein(float cosangle, float g) +{ + // times 4 pi: 1 for an even scattering + float g2 = g * g; + return (1.0 - g2) / pow(max(1.0 + g2 - 2.0 * g * cosangle, 1e-4), 1.5); +} + +float cloud_phase(float cosangle, float scale) +{ + return mix(henyey_greenstein(cosangle, 0.8 * scale), henyey_greenstein(cosangle, -0.25 * scale), 0.3); +} + +// the light of the sky a face looking straight up (Up 1) or down (-1) gets: the uniform ambient light and the directional part of +// the sky light (sky_ssbo, light_common.glsl: sky_ambient_directional) along the vertical. with the physical atmosphere it is bluer +// and brighter from above than from the sides, so the shaded sides of the clouds take the colour of the sky above them +vec3 cloud_sky_light(float up) +{ + vec3 directional = sky_ambient_sh[1].rgb * (0.488603 * up) - sky_ambient_sh[6].rgb * 0.315392 - sky_ambient_sh[8].rgb * 0.546274; + return max(weather_ambient.rgb + directional * 1.3, vec3(0.0)) * (CLOUD_AMBIENT_SCALE * weather_ambient.w); +} + +// light of the sun reaching a point inside a layer through the cloud towards it, with the light scattered many times +// (octaves after Wrenninge), and the phase of the view +float cloud_sunlight(int index, vec3 position, float cosangle, int steps, float powder, bool detail) +{ + vec3 sun = weather_sun_direction.xyz; + cloud_layer_s layer = cloud_layers[index]; + float thickness = layer.heights.y - layer.heights.x; + float reach = min(thickness, 1500.0); + float depth = 0.0; + float travelled = 0.0; + // steps growing geometrically: near the point the shape matters, far only the bulk. with steps growing evenly the first one was a + // tenth of the reach at 4 steps, and the light fell off the smooth sides of the clouds in visible bands + float growth = 1.0 + CLOUD_LIGHT_GROWTH / float(steps * steps); + float step = reach * (growth - 1.0) / (pow(growth, float(steps)) - 1.0); + for (int s = 0; s < steps; s++) + { + vec3 point = position + sun * (travelled + 0.5 * step); + travelled += step; + // the nearest steps with the detail noise: the small billows shade one another, lit on top and grey beneath + depth += cloud_layer_density(index, point, cloud_height(point), detail && (s < CLOUD_LIGHT_DETAIL_STEPS)) * step; + step *= growth; + } + float light = 0.0; + float amount = 1.0; + float fade = 1.0; + float spread = 1.0; + for (int octave = 0; octave < CLOUD_SCATTER_OCTAVES; octave++) + { + light += amount * exp(-depth * fade) * cloud_phase(cosangle, spread) * (octave == 0 ? powder : 1.0); + amount *= 0.5; + fade *= 0.5; + spread *= 0.5; + } + return light; +} + +// optical depth of the cloud over a point up to Reach above it, from Samples of the coarse density: under a lump and deep in a +// thick cloud the sky light is held back, at the tops and the thin edges it is not, so the shaded sides take the form of the cloud +float cloud_sky_depth(int index, vec3 position, float height, float reach, int samples) +{ + float depth = 0.0; + for (int s = 0; s < samples; s++) + { + // up along the vertical: the height grows by the same distance (cloud_height bends only with the horizontal distance) + float up = reach * (float(s) + 0.5) / float(samples); + depth += cloud_layer_density(index, position + vec3(0.0, up, 0.0), height + up, false); + } + return depth * reach / float(samples); +} + +float interleaved_noise(vec2 pixel) +{ + return fract(52.9829189 * fract(dot(pixel, vec2(0.06711056, 0.00583715)))); +} + +void main() +{ + ivec2 texel = ivec2(gl_GlobalInvocationID.xy); + vec3 direction; + if (mode == 0) + { + if (any(greaterThanEqual(texel, size))) + return; + vec2 ndc = (vec2(texel) + 0.5) / vec2(size) * 2.0 - 1.0; + // reverse z: the near plane at 1 + vec4 near = inverse_projection * vec4(ndc, 1.0, 1.0); + direction = normalize(mat3(view_to_world) * normalize(near.xyz / near.w)); + } + else + { + texel.y += rows.x; + if (texel.x >= size.x || texel.y >= min(size.y, rows.x + rows.y)) + return; + vec2 coords = (vec2(texel) + 0.5) / vec2(size); + float elevation = PANORAMA_LOW + (0.5 * WEATHER_PI - PANORAMA_LOW) * coords.y * coords.y; + float azimuth = coords.x * 2.0 * WEATHER_PI; + direction = vec3(cos(elevation) * sin(azimuth), sin(elevation), cos(elevation) * cos(azimuth)); + } + + vec3 sun = weather_sun_direction.xyz; + float cosangle = dot(direction, sun); + float sunup = weather_sun_direction.w; + float maxdistance = quality.z; + int maxsteps = int(quality.x); + int lightsteps = int(quality.y); + int skysteps = (lightsteps >= 4) ? 2 : 1; + bool detail = quality.w > 0.5; + float jitter = fract(interleaved_noise(vec2(texel)) + float(frame) * 0.618034); + + vec3 scattered = vec3(0.0); + float transmittance = 1.0; + float depthsum = 0.0; + float weightsum = 0.0; + int layers = int(weather_camera.w); + + // the stretches of the volumetric layers along the ray, nearest first + vec2 segments[MAX_CLOUD_LAYERS]; + int order[MAX_CLOUD_LAYERS]; + int count = 0; + for (int i = 0; i < layers; i++) + { + if (cloud_layers[i].extra.x > 0.5) + continue; + vec2 bounds = cloud_layer_bounds(cloud_layers[i]); + vec2 segment = cloud_layer_segment(direction.y, bounds.x, bounds.y); + segment.y = min(segment.y, maxdistance); + if (segment.y <= segment.x) + continue; + int at = count++; + segments[at] = segment; + order[at] = i; + for (int j = at; j > 0 && segments[j - 1].x > segments[j].x; j--) + { + vec2 swapsegment = segments[j]; + segments[j] = segments[j - 1]; + segments[j - 1] = swapsegment; + int swaporder = order[j]; + order[j] = order[j - 1]; + order[j - 1] = swaporder; + } + } + + vec3 ambienttop = cloud_sky_light(1.0); + vec3 sunlight = weather_sun_color.rgb * (CLOUD_SUN_SCALE * weather_sun_color.w) * sunup; + // the bases are lit from below by the ground, which the sun lights through the gaps between the clouds + vec3 ambientbottom = (weather_sun_color.rgb * max(weather_sun_direction.y, 0.0) * sunup * fog_light.y + ambienttop) * CLOUD_GROUND_ALBEDO; + // seen against the sun the thin edges glow (the forward scattering); seen with the sun behind, they are dark + float powderview = clamp(1.0 - cosangle, 0.0, 1.0); + for (int k = 0; k < count && transmittance > 0.01; k++) + { + int index = order[k]; + cloud_layer_s layer = cloud_layers[index]; + vec2 segment = segments[k]; + float span = segment.y - segment.x; + float thickness = layer.heights.y - layer.heights.x; + int steps = clamp(int(float(maxsteps) * span / (thickness * 4.0)), 8, maxsteps); + float step = span / float(steps); + for (int s = 0; s < steps; s++) + { + float t = segment.x + (float(s) + jitter) * step; + vec3 position = direction * t; + float height = cloud_height(position); + float density = cloud_layer_density(index, position, height, detail); + if (density <= 0.0) + continue; + float powder = mix(1.0, 1.0 - exp(-density / max(layer.shape.y, 1e-6) * CLOUD_POWDER), powderview); + float sunshare = (sunup > 0.0) ? cloud_sunlight(index, position, cosangle, lightsteps, powder, detail) : 0.0; + // the light of the sky reaches inside through the cloud above, that of the ground through the cloud below: the base of a + // thick cloud is darker than its top, and greyer, lit only by the ground and the sky at its sides + float column = layer.shape.y * layer.shape.x; + float top = cloud_layer_bounds(layer).y; + float reach = clamp(top - height, 0.0, CLOUD_SKY_REACH); + float sky = exp(-CLOUD_SKY_FADE * cloud_sky_depth(index, position, height, reach, skysteps) - CLOUD_AMBIENT_FADE * column * max(top - height - reach, 0.0)); + float below = column * max(height - layer.heights.x, 0.0); + vec3 light = sunlight * sunshare + (ambienttop * mix(CLOUD_SKY_FLOOR, 1.0, sky) + ambientbottom * exp(-CLOUD_AMBIENT_FADE * below)); + float through = exp(-density * step); + vec3 inscattered = light * (1.0 - through); + scattered += transmittance * inscattered; + float weight = transmittance * (1.0 - through); + depthsum += t * weight; + weightsum += weight; + transmittance *= through; + if (transmittance < 0.01) + break; + } + } + + // the thin high layers, past the others + for (int i = 0; i < layers && transmittance > 0.01; i++) + { + cloud_layer_s layer = cloud_layers[i]; + if (layer.extra.x < 0.5) + continue; + vec2 crossing; + if (cloud_ray_height(direction.y, 0.5 * (layer.heights.x + layer.heights.y), crossing) == 0) + continue; + float t = crossing.x; + if (t > maxdistance * 2.0) + continue; + vec3 position = direction * t; + // the slant of the ray through the sheet where it crosses it, the earth curving away + float slope = max(abs(direction.y - (1.0 - direction.y * direction.y) * t / CLOUD_EARTH_RADIUS), 0.03); + float depth = cloud_flat_depth(i, position) / slope; + if (depth <= 0.0) + continue; + float through = exp(-depth); + // ice crystals scatter forwards strongly + vec3 light = sunlight * henyey_greenstein(cosangle, 0.6) * 0.8 + ambienttop; + scattered += transmittance * light * (1.0 - through); + float weight = transmittance * (1.0 - through); + depthsum += t * weight; + weightsum += weight; + transmittance *= through; + } + + // the haze the atmosphere has in the sky already lies between the camera and the clouds too; the rest of the fog goes over + // the clouds and the sky alike when they are laid over it (sky_weather.frag) + float span_distance = (weightsum > 1e-5) ? depthsum / weightsum : maxdistance; + if (weightsum > 1e-5) + { + vec4 fog = fog_analytic_with(direction, 0.0, span_distance, min(fog_haze.x, fog_haze.z), 0.0); + scattered = scattered * fog.a + fog.rgb * (1.0 - transmittance); + } + if (any(isnan(scattered)) || isnan(transmittance)) + { + scattered = vec3(0.0); + transmittance = 1.0; + } + imageStore(clouds_color, texel, vec4(scattered, transmittance)); + if (mode == 0) + imageStore(clouds_depth, texel, vec4(span_distance)); +} diff --git a/shaders/vk14/clouds_noise.comp b/shaders/vk14/clouds_noise.comp new file mode 100644 index 00000000..e71b03db --- /dev/null +++ b/shaders/vk14/clouds_noise.comp @@ -0,0 +1,141 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the tiling noises the clouds and the fog are made of, made once at the start (opengl46_weather::init). +// kind 0, the shape (3d): r perlin-worley (perlin fbm with the worley cells carved in), g b a worley fbm of rising frequency; +// kind 1, the detail (3d): r g b worley fbm of rising frequency; +// kind 2, the coverage (2d): r g b three perlin fbm, spread evenly over 0-1 so a threshold of 1 - c leaves a share c above it + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" +#include "random.glsl" + +layout(local_size_x = WEATHER_GROUP_SIZE, local_size_y = WEATHER_GROUP_SIZE, local_size_z = 1) in; + +layout(rgba8, set = COMPUTE_SET, binding = 0) uniform writeonly image3D noise_volume; +layout(rgba8, set = COMPUTE_SET, binding = 1) uniform writeonly image2D noise_plane; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + int kind; + int size; +}; + +vec3 hash3(ivec3 cell, int period, uint seed) +{ + uvec3 wrapped = uvec3((cell % period + period) % period); + uint h = Hash(uvec3(wrapped.x + seed * 977U, wrapped.y, wrapped.z)); + uint h2 = Hash(h + 0x68bc21ebU); + uint h3 = Hash(h2 + 0x02e5be93U); + return vec3(float(h & 0xffffU), float(h2 & 0xffffU), float(h3 & 0xffffU)) / 65535.0; +} + +// gradient noise repeating every Period cells, -1..1 about +float perlin(vec3 position, int period, uint seed) +{ + ivec3 cell = ivec3(floor(position)); + vec3 local = fract(position); + vec3 fade = local * local * local * (local * (local * 6.0 - 15.0) + 10.0); + float corners[8]; + for (int i = 0; i < 8; i++) + { + ivec3 offset = ivec3(i & 1, (i >> 1) & 1, (i >> 2) & 1); + vec3 gradient = normalize(hash3(cell + offset, period, seed) * 2.0 - 1.0 + 1e-4); + corners[i] = dot(gradient, local - vec3(offset)); + } + float x0 = mix(corners[0], corners[1], fade.x); + float x1 = mix(corners[2], corners[3], fade.x); + float x2 = mix(corners[4], corners[5], fade.x); + float x3 = mix(corners[6], corners[7], fade.x); + return mix(mix(x0, x1, fade.y), mix(x2, x3, fade.y), fade.z) * 1.6; +} + +// inverted distance to the nearest of the points scattered one per cell, 0-1, repeating every Period cells +float worley(vec3 position, int period, uint seed) +{ + ivec3 cell = ivec3(floor(position)); + vec3 local = fract(position); + float nearest = 1.0; + for (int z = -1; z <= 1; z++) + for (int y = -1; y <= 1; y++) + for (int x = -1; x <= 1; x++) + { + ivec3 offset = ivec3(x, y, z); + vec3 point = vec3(offset) + hash3(cell + offset, period, seed); + nearest = min(nearest, length(point - local)); + } + return 1.0 - clamp(nearest, 0.0, 1.0); +} + +float perlin_fbm(vec3 position, int frequency, int octaves, uint seed) +{ + float sum = 0.0; + float amplitude = 0.5; + float total = 0.0; + for (int i = 0; i < octaves; i++) + { + sum += perlin(position * float(frequency), frequency, seed + uint(i)) * amplitude; + total += amplitude; + amplitude *= 0.5; + frequency *= 2; + } + return sum / total; +} + +float worley_fbm(vec3 position, int frequency, uint seed) +{ + return worley(position * float(frequency), frequency, seed) * 0.625 + + worley(position * float(frequency * 2), frequency * 2, seed + 1U) * 0.25 + + worley(position * float(frequency * 4), frequency * 4, seed + 2U) * 0.125; +} + +// octaves of the coverage noise: it only places the clouds and gives their outline. it is the same at every height, so its finer +// octaves would stand as upright grooves down the sides of the clouds; their small shapes come from the 3d noises +const int COVERAGE_OCTAVES = 4; + +// the worley fbm keeps about to this range (5th and 95th percentile 0.35 and 0.61) +const float WORLEY_FBM_LOW = 0.30; +const float WORLEY_FBM_HIGH = 0.65; + +float remap(float value, float low, float high, float newlow, float newhigh) +{ + return newlow + (value - low) / (high - low) * (newhigh - newlow); +} + +// a sum of noises is about normally distributed; this spreads it about evenly over 0-1 +float even(float value, float spread) +{ + return clamp(0.5 + 0.5 * tanh(value / spread * 1.1), 0.0, 1.0); +} + +void main() +{ + ivec3 texel = ivec3(gl_GlobalInvocationID); + if (any(greaterThanEqual(texel.xy, ivec2(size)))) + return; + if (kind == 2) + { + vec3 position = vec3((vec2(texel.xy) + 0.5) / float(size), 0.0); + float first = perlin_fbm(position, 4, COVERAGE_OCTAVES, 11U); + float second = perlin_fbm(position + vec3(0.0, 0.0, 0.5), 8, 5, 23U); + float third = perlin_fbm(position + vec3(0.0, 0.0, 0.25), 8, 5, 29U); + imageStore(noise_plane, texel.xy, vec4(even(first, 0.22), even(second, 0.22), even(third, 0.22), 1.0)); + return; + } + if (texel.z >= size) + return; + vec3 position = (vec3(texel) + 0.5) / float(size); + if (kind == 0) + { + // both spread over 0-1 first: as sums of octaves they keep to a narrow band about the middle, and a cloud made of them + // would be an even block with no lumps. the cells then cut the perlin noise into rounded heaps: whole in their middles, + // only the highest perlin left between them + float low = even(perlin_fbm(position, 4, 4, 3U), 0.22); + float cells = worley_fbm(position, 4, 5U); + float spreadcells = clamp((cells - WORLEY_FBM_LOW) / (WORLEY_FBM_HIGH - WORLEY_FBM_LOW), 0.0, 1.0); + float perlinworley = clamp(remap(low, (1.0 - spreadcells) * 0.6, 1.0, 0.0, 1.0), 0.0, 1.0); + imageStore(noise_volume, texel, vec4(perlinworley, cells, worley_fbm(position, 8, 7U), worley_fbm(position, 16, 9U))); + } + else + imageStore(noise_volume, texel, vec4(worley_fbm(position, 4, 13U), worley_fbm(position, 8, 15U), worley_fbm(position, 16, 17U), 1.0)); +} diff --git a/shaders/vk14/clouds_shadow.comp b/shaders/vk14/clouds_shadow.comp new file mode 100644 index 00000000..f5a75df4 --- /dev/null +++ b/shaders/vk14/clouds_shadow.comp @@ -0,0 +1,34 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the shadow the clouds cast on the ground: for points of the ground reference around the camera, what the clouds let through +// towards the key light. the shading (light_common.glsl) and the fog (fog_inject.comp) look it up along the ray of the sun, +// see cloud_shadow() in weather_common.glsl + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" +#include "clouds_common.glsl" + +layout(local_size_x = WEATHER_GROUP_SIZE, local_size_y = WEATHER_GROUP_SIZE, local_size_z = 1) in; + +layout(r16f, set = COMPUTE_SET, binding = 0) uniform writeonly image2D shadow; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + int size; + // world x and z of the centre of the map relative to the camera, size of the map (metres) + vec3 placement; + // samples through a layer + int steps; +}; + +void main() +{ + ivec2 texel = ivec2(gl_GlobalInvocationID.xy); + if (any(greaterThanEqual(texel, ivec2(size)))) + return; + vec2 offset = placement.xy + ((vec2(texel) + 0.5) / float(size) - 0.5) * placement.z; + // the ground reference, the earth curving away + vec3 ground = vec3(offset.x, -weather_camera.x + dot(offset, offset) * (0.5 / CLOUD_EARTH_RADIUS), offset.y); + imageStore(shadow, texel, vec4(cloud_transmittance_to_sun(ground, steps))); +} diff --git a/shaders/vk14/clouds_temporal.comp b/shaders/vk14/clouds_temporal.comp new file mode 100644 index 00000000..740e81da --- /dev/null +++ b/shaders/vk14/clouds_temporal.comp @@ -0,0 +1,72 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the clouds of the main view smoothed over frames: the image of this frame (clouds_march.comp, ray started at another +// distance every frame) blended into the one of the previous frames, found where the clouds were by the distance of the +// clouds, the rotation and the move of the camera. the history is held to the range of the neighbourhood of this frame, +// so moving clouds and a turning camera don't leave trails + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" + +layout(local_size_x = WEATHER_GROUP_SIZE, local_size_y = WEATHER_GROUP_SIZE, local_size_z = 1) in; + +layout(set = COMPUTE_SET, binding = 0) uniform sampler2D current_color; +layout(set = COMPUTE_SET, binding = 1) uniform sampler2D current_depth; +layout(set = COMPUTE_SET, binding = 2) uniform sampler2D history_color; +layout(rgba16f, set = COMPUTE_SET, binding = 3) uniform writeonly image2D result; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + ivec2 size; + mat4 inverse_projection; + mat4 view_to_world; + // world axes to the clip space of the previous frame, the camera of the previous frame at the origin + mat4 previous_projection_view; + // where the camera was in the previous frame, relative to where it is (world, metres) + vec3 previous_camera; + // share of this frame in the result; 1 when there is no history + float blend; +}; + +void main() +{ + ivec2 texel = ivec2(gl_GlobalInvocationID.xy); + if (any(greaterThanEqual(texel, size))) + return; + vec4 current = texelFetch(current_color, texel, 0); + if (blend >= 1.0) + { + imageStore(result, texel, current); + return; + } + // the range of the neighbourhood + vec4 low = current; + vec4 high = current; + for (int y = -1; y <= 1; y++) + for (int x = -1; x <= 1; x++) + { + vec4 neighbour = texelFetch(current_color, clamp(texel + ivec2(x, y), ivec2(0), size - 1), 0); + low = min(low, neighbour); + high = max(high, neighbour); + } + vec2 ndc = (vec2(texel) + 0.5) / vec2(size) * 2.0 - 1.0; + vec4 near = inverse_projection * vec4(ndc, 1.0, 1.0); + vec3 direction = normalize(mat3(view_to_world) * normalize(near.xyz / near.w)); + float span_distance = texelFetch(current_depth, texel, 0).r; + vec3 position = direction * span_distance - previous_camera; + vec4 clip = previous_projection_view * vec4(position, 1.0); + float share = blend; + vec4 history = current; + if (clip.w > 0.0) + { + vec2 coords = clip.xy / clip.w * 0.5 + 0.5; + if (all(greaterThanEqual(coords, vec2(0.0))) && all(lessThanEqual(coords, vec2(1.0)))) + history = clamp(textureLod(history_color, coords, 0.0), low, high); + else + share = 1.0; + } + else + share = 1.0; + imageStore(result, texel, mix(history, current, share)); +} diff --git a/shaders/vk14/cluster_heatmap.frag b/shaders/vk14/cluster_heatmap.frag new file mode 100644 index 00000000..72f9ae03 --- /dev/null +++ b/shaders/vk14/cluster_heatmap.frag @@ -0,0 +1,48 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +// debug view of the light clusters: tints every pixel of the scene with the colour of the number of lights which +// were assigned to its cluster (see gl/cluster_grid.h), pixels of clusters without lights stay as they are. +// drawn over the output of the viewport, with the layout of the scene (the clusters of the pass set; the depth as the texture of +// a material) + +layout(location = 14) in vec2 f_coords; + +#include "common.glsl" + +#pragma texture(depth_tex, 0, R) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D depth_tex; + +layout(location = 0) out vec4 out_color; + +const float TINT = 0.55; + +// blue (few) through green to red (many) +vec3 ramp(float t) +{ + return clamp(vec3(1.5) - abs(4.0 * t - vec3(3.0, 2.0, 1.0)), 0.0, 1.0); +} + +void main() +{ + // reverse z: 0 is the far plane at infinity, i.e. the sky, which has no cluster + float depth = texelFetch(depth_tex, ivec2(f_coords * vec2(textureSize(depth_tex, 0))), 0).r; + if (depth <= 0.0) + discard; + + // depth is znear / w + float w = cluster_znear / depth; + int slice = clamp(int(floor(log(w) * cluster_slice_scale + cluster_slice_bias)), 0, CLUSTER_GRID_Z - 1); + ivec2 tile = clamp(ivec2(floor(f_coords * vec2(CLUSTER_GRID_X, CLUSTER_GRID_Y))), ivec2(0), ivec2(CLUSTER_GRID_X - 1, CLUSTER_GRID_Y - 1)); + + uint count = cluster_ranges[tile.x + CLUSTER_GRID_X * (tile.y + CLUSTER_GRID_Y * slice)].count; + if (count == 0U) + discard; + + // a full cluster (lights beyond its capacity are dropped) is white; the rest goes by the logarithm of the count + vec3 color = (count >= uint(MAX_LIGHTS_PER_CLUSTER)) + ? vec3(1.0) + : ramp(log2(1.0 + float(count)) / log2(1.0 + float(MAX_LIGHTS_PER_CLUSTER))); + + out_color = vec4(color, TINT); +} diff --git a/shaders/vk14/common.glsl b/shaders/vk14/common.glsl index 7f70e12d..458eb33b 100644 --- a/shaders/vk14/common.glsl +++ b/shaders/vk14/common.glsl @@ -6,10 +6,14 @@ // // the pipeline layout is picked by a define before the include of this file: // (none) the passes of the scene (materials, shadows, picking, lines, sprites, sky, particles): set PASS_SET with -// the blocks of the scene, of the draws, of the instances, the lights, the clusters and the shadow, -// environment and headlight maps; set MATERIAL_SET with the textures of the material -// LAYOUT_POST the post-processing and the copies of whole images: set POST_SET pushed with the images read, the -// parameters in the push constants (param stands for pc.data) +// the blocks of the scene, of the draws, of the instances, the lights, the clusters, the shadow, +// environment and headlight maps, the light of the sky, the weather, the shadows of the lamps and the depth +// of the scene; set MATERIAL_SET with the textures of the material +// LAYOUT_COMPUTE the compute kernels (vk14::compute_kernel): set PASS_SET as above (the block of the scene, the lights, the +// clusters, the light of the sky; the weather and the atmosphere with their includes), set COMPUTE_SET +// pushed with the bindings the kernel declares from 0 on, its parameters in push constants of its own +// LAYOUT_POST the post-processing and the copies of whole images: set POST_SET pushed with the images read and the +// exposure, the parameters in the push constants (param stands for pc.data) // LAYOUT_CLUSTER the compute passes of the light clusters: set 0 pushed at the CL_* bindings // LAYOUT_UI the user interface: none of the above, its own push constants // @@ -40,7 +44,22 @@ #define SHADOW_TEX 7 #define ENV_TEX 8 #define HEADLIGHT_TEX 9 -#define PASS_BINDING_COUNT 10 +#define SKY_SSBO 10 +#define ATMOSPHERE_UBO 11 +#define SKY_VIEW_TEX 12 +#define ENV_GGX_TEX 13 +#define WEATHER_UBO 14 +#define WEATHER_HORIZON_UBO 15 +#define FOG_VOLUME_TEX 16 +#define CLOUD_SHADOW_TEX 17 +#define CLOUD_SHAPE_TEX 18 +#define CLOUD_DETAIL_TEX 19 +#define CLOUD_COVERAGE_TEX 20 +#define LOCAL_SHADOW_TEX 21 +#define LOCAL_SHADOW_LAMPS_TEX 22 +#define HEADLIGHT_SHADOW_TEX 23 +#define SCENE_DEPTH_TEX 24 +#define PASS_BINDING_COUNT 25 #define MATERIAL_TEXTURE_FIRST 0 #define CL_CLUSTER_UBO 0 #define CL_AABB_SSBO 1 @@ -50,6 +69,9 @@ #define CL_LIGHT_SSBO 5 #define POST_SET 0 #define POST_TEXTURE_FIRST 0 +#define POST_TEXTURES 4 +#define POST_EXPOSURE_SSBO 4 +#define COMPUTE_SET 1 #define PUSH_CONSTANTS_SIZE 112 #define SPEC_OUTPUT_GAMMA 0 #define SPEC_SHADOWMAP_ENABLED 1 @@ -69,11 +91,35 @@ #define MAX_LIGHTS_PER_CLUSTER 64 #define CLUSTER_BUILD_GROUP_SIZE 64 #define CLUSTER_CULL_GROUP_SIZE 64 +#define ENVMAP_GGX_LEVELS 7U +#define ENVMAP_GGX_ROUGHNESS_MIN 0.200000 +#define ENVMAP_GGX_GROUP_SIZE 8 +#define EXPOSURE_HISTOGRAM_BINS 256 +#define EXPOSURE_HISTOGRAM_GROUP_SIZE 16 +#define ATMOSPHERE_TRANSMITTANCE_WIDTH 256 +#define ATMOSPHERE_TRANSMITTANCE_HEIGHT 64 +#define ATMOSPHERE_MULTISCATTERING_SIZE 32 +#define ATMOSPHERE_SKYVIEW_WIDTH 192 +#define ATMOSPHERE_SKYVIEW_HEIGHT 108 +#define ATMOSPHERE_AMBIENT_GROUP_SIZE 256 +#define MAX_CLOUD_LAYERS 4 +#define FOG_HORIZON_SAMPLES 32 +#define WEATHER_GROUP_SIZE 8 +#define FOG_VOLUME_BIAS 10.000000 +#define INSTANCE_CULL_GROUP_SIZE 64 +#define INSTANCE_OFFSETS_GROUP_SIZE 1024 +#define INSTANCE_COMMAND_GROUP_SIZE 64 +#define INSTANCE_COUNTER_HEADER 8 +#define INSTANCE_JOB_SLEEPERS 1U +#define HIZ_GROUP_SIZE 8 +#define SHADOW_FIT_GROUP_SIZE 16 +#define LOCAL_SHADOW_NEAR 0.150000 +#define PARTICLE_GROUP_SIZE 64 #endif #include "settings.glsl" -#ifndef LAYOUT_UI +#if !defined(LAYOUT_UI) && !defined(LAYOUT_COMPUTE) // vk14::draw_constants, PUSH_CONSTANTS_SIZE bytes, visible in every stage. draw: the element of model_draws[] the draw reads // (the passes of the scene); flags and data: the parameters of the special shaders, each says what it takes from them layout(push_constant) uniform draw_constants_block @@ -171,8 +217,9 @@ layout(std430, set = 0, binding = CL_LIGHT_SSBO) readonly buffer light_ssbo 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; - // not used by the shaders of this renderer yet (the sky ambient light and the prefiltered environment map come later) + // 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[]; @@ -183,6 +230,16 @@ layout(std430, set = 0, binding = CL_LIGHT_SSBO) readonly buffer light_ssbo // the post-processing reads the parameters the opengl renderer gives it in the model block from the push constants #define param pc.data +// exposure worked out by exposure_average.comp, gl::exposure_data +layout(std430, set = POST_SET, binding = POST_EXPOSURE_SSBO) readonly buffer exposure_ssbo +{ + uint histogram[EXPOSURE_HISTOGRAM_BINS]; + float adapted_luminance; + float exposure; + float average_luminance; + uint valid; +}; + #elif !defined(LAYOUT_UI) // the block of the scene, gl::scene_ubs (every member kept, the layout is the one of the c++ struct) @@ -204,7 +261,8 @@ layout(std140, set = PASS_SET, binding = SCENE_UBO) uniform scene_ubo vec4 shadow_fit; }; -// the model block of a draw, gl::model_ubs (256 bytes), the one of the draw picked by pc.draw. the names below stand for its +#ifndef LAYOUT_COMPUTE +// the model block of a draw, gl::model_ubs (256 bytes), the one of the draw picked by pc.draw (MODEL_DRAW). the names below stand for its // fields, so the shaders read them as the uniform block of the opengl renderer struct model_draw_s { @@ -225,15 +283,32 @@ layout(std430, set = PASS_SET, binding = DRAW_SSBO) readonly buffer model_draw_s model_draw_s model_draws[]; }; -#define modelview model_draws[pc.draw].modelview -#define modelviewnormal model_draws[pc.draw].modelviewnormal -#define param model_draws[pc.draw].param -#define future model_draws[pc.draw].future -#define opacity model_draws[pc.draw].opacity -#define emission model_draws[pc.draw].emission -#define fog_density model_draws[pc.draw].fog_density -#define alpha_mult model_draws[pc.draw].alpha_mult -#define shadow_tone model_draws[pc.draw].shadow_tone +#ifdef MODEL_DRAWS +// programs of the multi draws of the instances drawn from the gpu culling: the blocks of the draws of a multi draw follow one +// another from the one of pc.draw, picked by the number of the draw in it. the fragment stage gets the block from the vertex +// stage (which writes f_model_draw) +#if defined(STAGE_VERTEX) +layout(location = 11) flat out uint f_model_draw; +#define MODEL_DRAW (pc.draw + uint(gl_DrawID)) +#elif defined(STAGE_FRAGMENT) +layout(location = 11) flat in uint f_model_draw; +#define MODEL_DRAW f_model_draw +#else +#define MODEL_DRAW pc.draw +#endif +#else +#define MODEL_DRAW pc.draw +#endif + +#define modelview model_draws[MODEL_DRAW].modelview +#define modelviewnormal model_draws[MODEL_DRAW].modelviewnormal +#define param model_draws[MODEL_DRAW].param +#define future model_draws[MODEL_DRAW].future +#define opacity model_draws[MODEL_DRAW].opacity +#define emission model_draws[MODEL_DRAW].emission +#define fog_density model_draws[MODEL_DRAW].fog_density +#define alpha_mult model_draws[MODEL_DRAW].alpha_mult +#define shadow_tone model_draws[MODEL_DRAW].shadow_tone // per-instance modelview matrices of the instanced draws, element 0 the identity. a draw which isn't instanced is drawn with // firstInstance 0, an instanced one from its base instance on, with modelview set to the submodel-local chain (computed from @@ -242,6 +317,7 @@ layout(std430, set = PASS_SET, binding = INSTANCE_SSBO) readonly buffer instance { mat4 instance_modelview[]; }; +#endif // the light buffer of the pass, gl::light_data layout(std430, set = PASS_SET, binding = LIGHT_SSBO) readonly buffer light_ssbo @@ -255,13 +331,21 @@ layout(std430, set = PASS_SET, binding = LIGHT_SSBO) readonly buffer light_ssbo 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; - // not used by the shaders of this renderer yet (the sky ambient light and the prefiltered environment map come later) + // 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, set = PASS_SET, binding = SKY_SSBO) readonly buffer sky_ssbo +{ + vec4 sky_ambient_sh[9]; +}; + // the grid of the light clusters of the pass, gl::cluster_ubs; the compute passes fill the lists, shading only reads them layout(std140, set = PASS_SET, binding = CLUSTER_UBO) uniform cluster_ubo { diff --git a/shaders/vk14/depthprepass.frag b/shaders/vk14/depthprepass.frag new file mode 100644 index 00000000..864e52d2 --- /dev/null +++ b/shaders/vk14/depthprepass.frag @@ -0,0 +1,10 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +// depth prepass of materials which never discard a fragment (no alpha test) + +layout(location = 2) in vec2 f_coord; + +void main() +{ +} diff --git a/shaders/vk14/depthprepass.vert b/shaders/vk14/depthprepass.vert new file mode 100644 index 00000000..ae25d923 --- /dev/null +++ b/shaders/vk14/depthprepass.vert @@ -0,0 +1,26 @@ +#version 460 core +#define STAGE_VERTEX 1 +#include "common.glsl" +// depth prepass of the instances drawn from the gpu culling (opengl46_renderer::Render_Instances_Indirect()). +// the position is worked out the way vertex.vert of the materials does it, and declared invariant in both, so the colour +// pass which follows gets the same depth for the same surface + +layout(location = 0) in vec3 v_vert; +layout(location = 2) in vec2 v_coord; + + +invariant gl_Position; + +layout(location = 2) out vec2 f_coord; + +void main() +{ + mat4 mv = effective_modelview(); + vec4 position = mv * vec4(v_vert, 1.0); + gl_Position = projection * position; + f_coord = v_coord; +#ifdef MODEL_DRAWS + // which block of the multi draw the fragments read (common.glsl) + f_model_draw = MODEL_DRAW; +#endif +} diff --git a/shaders/vk14/depthprepass_alpha.frag b/shaders/vk14/depthprepass_alpha.frag new file mode 100644 index 00000000..abf0f17c --- /dev/null +++ b/shaders/vk14/depthprepass_alpha.frag @@ -0,0 +1,18 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +// depth prepass of materials whose colour shader tests the alpha of the diffuse texture against the opacity of the +// material (mat_default.frag and the like). the test here is stricter by one step of 8-bit alpha: a fragment which writes +// depth here is never discarded by the colour pass, which would leave a hole with nothing drawn behind it + +layout(location = 2) in vec2 f_coord; + + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +void main() +{ + if (texture(diffuse, f_coord).a < opacity + 1.0 / 255.0) + discard; +} diff --git a/shaders/vk14/envmap_prefilter.comp b/shaders/vk14/envmap_prefilter.comp new file mode 100644 index 00000000..a7ada6d9 --- /dev/null +++ b/shaders/vk14/envmap_prefilter.comp @@ -0,0 +1,107 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// 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 + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" + +layout(local_size_x = ENVMAP_GGX_GROUP_SIZE, local_size_y = ENVMAP_GGX_GROUP_SIZE) in; + +layout(set = COMPUTE_SET, binding = 0, rgba16f) writeonly uniform imageCube prefilter_target; +// the plain map with its box filtered mipmaps, on the unit gl::program binds envmap to +layout(set = COMPUTE_SET, binding = 1) uniform samplerCube envmap; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + // perceptual roughness of the level being written + float roughness; + // size of the level being written, and of the first level of the source + int target_size; + float source_size; + 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/vk14/envmapping.glsl b/shaders/vk14/envmapping.glsl index 2d51dbf4..1239845a 100644 --- a/shaders/vk14/envmapping.glsl +++ b/shaders/vk14/envmapping.glsl @@ -1,4 +1,6 @@ layout(set = PASS_SET, binding = ENV_TEX) uniform samplerCube envmap; +// the same map prefiltered with the GGX lobe (envmap_prefilter.comp), used when env_ggx is 1 +layout(set = PASS_SET, binding = ENV_GGX_TEX) uniform samplerCube envmap_ggx; vec3 envmap_color( vec3 normal ) { @@ -45,10 +47,27 @@ vec3 envmap_color_lod(vec3 fragnormal, float lod) } } -// reflection of the surroundings in a surface of the given perceptual roughness (0 mirror, 1 matte): the plain map, its mipmaps -// (box filtered) standing for the roughness. the prefiltered GGX map of the opengl renderer (env_ggx) comes in a later stage +// 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; - return envmap_color_lod(fragnormal, roughness * MAX_REFLECTION_LOD); + 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; + } } diff --git a/shaders/vk14/exposure_average.comp b/shaders/vk14/exposure_average.comp new file mode 100644 index 00000000..a3c5a666 --- /dev/null +++ b/shaders/vk14/exposure_average.comp @@ -0,0 +1,86 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// 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 + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" + +layout(local_size_x = EXPOSURE_HISTOGRAM_BINS) in; + +layout(std430, set = COMPUTE_SET, binding = 0) buffer exposure_ssbo +{ + uint histogram[EXPOSURE_HISTOGRAM_BINS]; + float adapted_luminance; + float exposure; + float average_luminance; + uint valid; +}; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + // x: log2 of the lowest luminance binned, y: log2 luminance per bin + 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 + vec4 adaptation; + // lowest and highest exposure (multipliers) + 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/vk14/exposure_histogram.comp b/shaders/vk14/exposure_histogram.comp new file mode 100644 index 00000000..2c547b1c --- /dev/null +++ b/shaders/vk14/exposure_histogram.comp @@ -0,0 +1,56 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// 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 + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" + +layout(local_size_x = EXPOSURE_HISTOGRAM_GROUP_SIZE, local_size_y = EXPOSURE_HISTOGRAM_GROUP_SIZE) in; + +layout(set = COMPUTE_SET, binding = 0) uniform sampler2D hdr_image; + +layout(std430, set = COMPUTE_SET, binding = 1) buffer exposure_ssbo +{ + uint histogram[EXPOSURE_HISTOGRAM_BINS]; + float adapted_luminance; + float exposure; + float average_luminance; + uint valid; +}; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + // x: log2 of the lowest luminance binned, y: number of bins per unit of log2 luminance + 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/vk14/fog_horizon.comp b/shaders/vk14/fog_horizon.comp new file mode 100644 index 00000000..2f685472 --- /dev/null +++ b/shaders/vk14/fog_horizon.comp @@ -0,0 +1,49 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// colours of the fog towards the horizon by the angle from the azimuth of the sun, from the sky-view table of the atmosphere, +// written into the weather buffer (weather_common.glsl: fog_horizon). the far fog then has the colour of the sky behind it. +// the atmosphere blends into the old sky as opengl46_sky does it, by its share, white balance and brightness + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" +#include "atmosphere_common.glsl" + +layout(local_size_x = FOG_HORIZON_SAMPLES) in; + +layout(set = PASS_SET, binding = SKY_VIEW_TEX) uniform sampler2D sky_view_lut; + +layout(std430, set = COMPUTE_SET, binding = 0) buffer weather_scratch +{ + vec4 weather_words[]; +}; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + // first vec4 of fog_horizon in the weather buffer + int offset; + // x: share of the atmosphere, y: its brightness, z: desaturation by the overcast, w: brightest the colour gets + vec4 params; + vec3 white_balance; + // the colour of the old fog, which the atmosphere replaces by its share + vec3 old_color; +}; + +void main() +{ + uint index = gl_LocalInvocationID.x; + float angle = float(index) / float(FOG_HORIZON_SAMPLES - 1) * ATMO_PI; + vec3 sun = normalize(atmo_sun_direction.xyz); + vec2 flat_sun = (dot(sun.xz, sun.xz) > 1e-8) ? normalize(sun.xz) : vec2(1.0, 0.0); + vec2 side = vec2(-flat_sun.y, flat_sun.x); + vec2 flat_view = flat_sun * cos(angle) + side * sin(angle); + // a little above the horizon, where the far fog meets the sky + vec3 direction = normalize(vec3(flat_view.x, 0.035, flat_view.y)); + vec3 color = textureLod(sky_view_lut, atmo_skyview_uv_world(direction, sun), 0.0).rgb * white_balance * params.y; + color = min(color, vec3(params.w)); + float luminance = dot(color, vec3(0.2126, 0.7152, 0.0722)); + color = mix(color, vec3(luminance), params.z); + if (any(isnan(color))) + color = old_color; + weather_words[offset + int(index)] = vec4(mix(old_color, color, params.x), 1.0); +} diff --git a/shaders/vk14/fog_inject.comp b/shaders/vk14/fog_inject.comp new file mode 100644 index 00000000..bf21ed6d --- /dev/null +++ b/shaders/vk14/fog_inject.comp @@ -0,0 +1,323 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the fog volume of the main view, first pass: in every cell of a grid laid over the view (slices spaced exponentially in the +// depth), how dense the fog is and how much light it scatters towards the camera. the density: the haze, the patches of the +// ground fog, the low clouds; the light: the colour the fog has towards the horizon (weather_common.glsl), less what the +// shadows of the sun and of the clouds hold back, and the lamps and headlights of the light clusters. blended with the +// previous frame, found where the cell was then; the cells are sampled at another point every frame, each cell going through +// the points from a place of its own, so the volume doesn't flicker as a whole, and the blending smooths what a cell holds over +// its whole size: a beam doesn't swim over the grid of the cells as the camera moves and turns. what the cell takes the lights +// from doesn't depend on the point (the clusters of the middle of the cell) +// rgb: light scattered towards the camera per metre, a: extinction per metre + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" +#include "clouds_common.glsl" + +layout(local_size_x = WEATHER_GROUP_SIZE, local_size_y = WEATHER_GROUP_SIZE, local_size_z = 1) in; + +layout(rgba16f, set = COMPUTE_SET, binding = 0) uniform writeonly image3D fog_scattering; +layout(set = COMPUTE_SET, binding = 1) uniform sampler3D fog_history; + +layout(set = PASS_SET, binding = SHADOW_TEX) uniform sampler2DArrayShadow shadowmap; +layout(set = PASS_SET, binding = HEADLIGHT_TEX) uniform sampler2D headlightmap; +layout(set = PASS_SET, binding = HEADLIGHT_SHADOW_TEX) uniform sampler2DArrayShadow headlight_shadowmap; + +// the parameters of the kernel (a block of the ring of the frame: more of them than the push constants hold) +layout(std140, set = COMPUTE_SET, binding = 2) uniform kernel_block +{ + ivec3 grid; + mat4 inverse_projection; + mat4 view_to_world; + // x: near, y: far, z: slices / log((far + FOG_VOLUME_BIAS) / (near + FOG_VOLUME_BIAS)), w: where in its slice the cell is + // sampled this frame (0-1) + vec4 slicing; + // world axes to the clip space of the previous frame, the camera of the previous frame at the origin + mat4 previous_projection_view; + vec3 previous_camera; + // x: share of this frame (1 without history), y: brightness of the lamps in the fog (0: none), z: 1 when the sun casts shadows + // into the fog, w: 1 when the headlights cast shadows into it + vec4 options; + // the light of the cab lamp, which lights no fog (the air of the cab is clear), or -1 + int cablamp; + // x: near, y: slices / log((far + FOG_VOLUME_BIAS) / (near + FOG_VOLUME_BIAS)) of the previous frame + vec2 previous_slicing; + // where in its cell the cell is sampled this frame across the view (0-1), as slicing.w is along the depth + vec2 jitter; +}; + +// ray of the view space through a point of the view (0-1), one metre of the depth long +vec3 view_ray(vec2 coords) +{ + vec4 near = inverse_projection * vec4(coords * 2.0 - 1.0, 1.0, 1.0); + vec3 ray = near.xyz / near.w; + return ray / -ray.z; +} + +// view depth at a place along the slices: slice number and the part of the slice +float slice_depth(float slice) +{ + return (slicing.x + FOG_VOLUME_BIAS) * exp(slice / slicing.z) - FOG_VOLUME_BIAS; +} + +// where in the cell it is sampled this frame, 0-1 on each axis: the point of the sequence of the frames moved by a value of +// the cell's own (pcg3d hash), the middle without the blending +vec3 cell_offset(ivec3 cell) +{ + if (options.x >= 1.0) + return vec3(0.5); + uvec3 v = uvec3(cell) * 1664525U + 1013904223U; + v.x += v.y * v.z; + v.y += v.z * v.x; + v.z += v.x * v.y; + v ^= v >> 16U; + v.x += v.y * v.z; + v.y += v.z * v.x; + v.z += v.x * v.y; + return fract(vec3(jitter, slicing.w) + vec3(v >> 8U) * (1.0 / 16777216.0)); +} + +float henyey_greenstein(float cosangle, float g) +{ + float g2 = g * g; + return (1.0 - g2) / pow(max(1.0 + g2 - 2.0 * g * cosangle, 1e-4), 1.5); +} + +// share of the sunlight the shadow map holds back at a point of the view space, one comparison +float sun_shadow(vec3 position) +{ + if (SHADOWMAP_ENABLED) + { + float span_distance = dot(position, position); + uint cascade; + for (cascade = 0U; cascade < MAX_CASCADES; cascade++) + if (span_distance <= cascade_end[cascade]) + break; + for (; cascade < MAX_CASCADES; cascade++) + { + vec4 lightpos = lightview[cascade] * vec4(position, 1.0); + vec3 coords = lightpos.xyz / lightpos.w; + if (all(greaterThanEqual(coords.xy, vec2(0.0))) && all(lessThanEqual(coords.xy, vec2(1.0)))) + { + if (coords.z < 0.0) + return 0.0; + return texture(shadowmap, vec4(coords.xy, float(cascade), coords.z + 0.0001 * float(cascade + 1U))); + } + // outside the map: lit, unless the cascades are fitted to the depth of the view (gfx.shadow.sdsm), whose maps cover + // only the area of the surfaces seen; then the first further cascade whose map holds the point + if (shadow_fit.x <= 0.0) + return 0.0; + } + return 0.0; + } + else + { + return 0.0; + } +} + +// light of a lamp or of the beam of headlights at a point of the view space, before the phase +vec3 lamp_light(light_s light, vec3 position) +{ + if (light.type == LIGHT_HEADLIGHTS) + { + vec4 projected = light.headlight_projection * vec4(position, 1.0); + vec3 coords = projected.xyz / projected.w; + if (coords.z < 0.0 || coords.z > 1.0 || any(lessThan(coords.xy, vec2(0.0))) || any(greaterThan(coords.xy, vec2(1.0)))) + return vec3(0.0); + float span_distance = length(light.pos - position); + float atten = 1.0 / (1.0 + light.linear * span_distance + light.quadratic * span_distance * span_distance); + atten *= mix(1.0, 0.0, clamp((coords.z - 0.998) * 500.0, 0.0, 1.0)); + vec3 beam = textureLod(headlightmap, coords.xy, 1.0).rgb * light.headlight_weights.rgb; + float intensity = max(max(beam.r, beam.g), beam.b); + if ((options.w > 0.5) && (light.headlight_weights.w > 0.5) && (intensity > 0.0)) + { + // the shadow map of the beam, as in light_common.glsl without the offset along a normal + uint map = uint(light.headlight_weights.w - 0.5); + vec4 shadowpos = headlight_shadow[map] * vec4(position, 1.0); + if (shadowpos.w > 0.0) + { + vec2 shadowcoords = shadowpos.xy / shadowpos.w; + if (all(greaterThanEqual(shadowcoords, vec2(0.0))) && all(lessThanEqual(shadowcoords, vec2(1.0)))) + { + float texel = headlight_shadow_params[map].x * shadowpos.w; + float reference = shadowpos.z / max(shadowpos.w - texel * 1.5, 1e-3); + intensity *= 1.0 - texture(headlight_shadowmap, vec4(shadowcoords, float(map), reference)); + } + } + } + return light.color * (atten * intensity * light.intensity); + } + vec3 tolight = light.pos - position; + float distance2 = dot(tolight, tolight); + float edge = clamp(1.0 - distance2 * distance2 * (light.quadratic * light.quadratic), 0.0, 1.0); + float atten = edge * edge / max(distance2, 0.01); + if (light.type == LIGHT_SPOT) + atten *= smoothstep(light.out_cutoff, light.in_cutoff, dot(-tolight * inversesqrt(max(distance2, 1e-8)), normalize(light.dir))); + return light.color * (atten * light.intensity); +} + +// light of a lamp or of the beam of headlights scattered towards the camera, the average per metre along a segment of the ray +// of a cell (start, unit direction, length; view space). point samples of a lamp next to the ray change many times over with +// where in the cell they fall (the light falls off with the square of the distance), so the falloff of a lamp is integrated +// along the segment and only the rest of its light (the cone, the edge of its reach, the phase) is sampled, at two points the +// integral spreads by itself (equi-angular sampling, Kulla & Fajardo 2012). next to the lamp the light is spread over the size +// of a cell: the distance of the lamp from the ray grows by the width of the cell there, and the cone of a spot light starts +// that far behind the lamp. the point sampled moves within the cell from frame to frame, and the ray of the cell would pass at +// almost no distance from the lamp in one frame and a cell away in the next, above the tip of the cone and below it. Part: +// where between the two halves of the segment the points are (0-1) +vec3 lamp_segment(light_s lamp, light_s cone, vec3 start, vec3 direction, float span, float width, float part); +vec3 lamp_scattering(light_s lamp, vec3 start, vec3 direction, float span, float cellwidth, float part) +{ + vec3 sum = vec3(0.0); + if (lamp.type == LIGHT_HEADLIGHTS) + { + // the beam falls off slowly, two points spread evenly along the segment + for (int i = 0; i < 2; i++) + { + vec3 position = start + direction * (span * (float(i) + part) * 0.5); + sum += lamp_light(lamp, position) * henyey_greenstein(dot(normalize(position - lamp.pos), -direction), 0.4); + } + return sum * 0.5; + } + float closest = dot(lamp.pos - start, direction); + vec3 nearest = start + direction * closest; + float width = max(cellwidth * max(-nearest.z, 0.0), 1e-3); + light_s cone = lamp; + if (lamp.type == LIGHT_SPOT) + cone.pos -= normalize(lamp.dir) * width; + if (dot(lamp.pos - nearest, lamp.pos - nearest) < 9.0 * width * width) + { + // a lamp within a few cells of the ray: the light of the cell is taken along four rays spread over the cell, so it + // doesn't depend on which side of the lamp the ray of this frame passes, and the points along the rays are staggered, + // eight along the cell, where the cone changes the most (its tip) + vec3 across = normalize(cross(direction, vec3(0.0, 1.0, 0.0) + vec3(1e-4, 0.0, 0.0))); + vec3 up = cross(across, direction); + for (int k = 0; k < 4; k++) + { + vec2 shift = (vec2(float(k & 1), float(k >> 1)) - 0.5) * 0.5 * width; + sum += lamp_segment(lamp, cone, start + across * shift.x + up * shift.y, direction, span, width, fract(part + 0.25 * float(k))); + } + return sum * 0.25; + } + return lamp_segment(lamp, cone, start, direction, span, width, part); +} + +// lamp_scattering() along one ray: Cone the lamp with the tip of its cone moved behind it, Width the width of the cell at the lamp +vec3 lamp_segment(light_s lamp, light_s cone, vec3 start, vec3 direction, float span, float width, float part) +{ + vec3 sum = vec3(0.0); + float closest = dot(lamp.pos - start, direction); + vec3 nearest = start + direction * closest; + float reach = sqrt(dot(lamp.pos - nearest, lamp.pos - nearest) + width * width); + float first = atan(-closest, reach); + float last = atan(span - closest, reach); + for (int i = 0; i < 2; i++) + { + vec3 position = start + direction * (closest + reach * tan(mix(first, last, (float(i) + part) * 0.5))); + vec3 tolight = cone.pos - position; + // the light without its falloff, which the integral holds + vec3 fromlamp = position - lamp.pos; + sum += lamp_light(cone, position) * max(dot(tolight, tolight), 0.01) * henyey_greenstein(dot(fromlamp, -direction) * inversesqrt(max(dot(fromlamp, fromlamp), 1e-8)), 0.4); + } + return sum * (0.5 * (last - first) / (reach * max(span, 1e-4))); +} + +void main() +{ + ivec3 cell = ivec3(gl_GlobalInvocationID); + if (any(greaterThanEqual(cell, grid))) + return; + vec3 offset = cell_offset(cell); + vec2 coords = (vec2(cell.xy) + offset.xy) / vec2(grid.xy); + float depth = slice_depth(float(cell.z) + offset.z); + vec3 ray = view_ray(coords); + vec3 view = ray * depth; + // the segment of the ray through the cell, for the light which changes within the cell + float raylength = length(ray); + vec3 raydirection = ray / raylength; + float front = slice_depth(float(cell.z)); + vec3 start = ray * front; + float span = (slice_depth(float(cell.z) + 1.0) - front) * raylength; + vec3 world = mat3(view_to_world) * view; + float height = weather_camera.x + world.y; + + // density + float haze = fog_haze.x * exp(-fog_haze.y * max(height, 0.0)); + float ground = fog_ground.x * exp(-fog_ground.y * max(height - fog_ground.z, 0.0)); + if (ground > 0.0 && fog_ground.w > 0.0) + { + float patches = textureLod(cloud_shape_noise, (world + fog_noise.xyz) * fog_noise.w, 0.0).g; + ground *= mix(1.0, clamp((patches - 0.3) * 2.6, 0.0, 2.2), fog_ground.w); + } + float clouds = cloud_density_coarse(world); + float extinction = haze + ground + clouds; + + vec4 result = vec4(0.0); + if (extinction > 1e-7) + { + vec3 direction = normalize(world); + // the light of the sky and the sun, held back by the shadows + float sunlit = cloud_shadow(world); + if (options.z > 0.5 && sunlit > 0.0) + { + // two points of the segment, one in each half: a shaft of light no longer comes and goes with the one point sampled + float shadow = sun_shadow(start + raydirection * (span * 0.5 * offset.z)) + sun_shadow(start + raydirection * (span * 0.5 * (offset.z + 1.0))); + sunlit *= 1.0 - 0.5 * shadow; + } + vec3 light = fog_whiten(fog_color_world(direction), (ground + clouds) / extinction) * fog_light_factor(sunlit); + // lamps and headlights of the clusters the segment of the cell goes through, each part of the segment with the lights of + // its own cluster. the lights of the cluster of the point sampled would change with the point, frame to frame, where the + // cell reaches into two clusters which don't hold the same lights (a full cluster leaves the least important out) + if (options.y > 0.0 && lights_clustered != 0U && cluster_valid != 0U) + { + vec2 middle = (vec2(cell.xy) + 0.5) / vec2(grid.xy); + ivec2 tile = clamp(ivec2(middle * vec2(CLUSTER_GRID_X, CLUSTER_GRID_Y)), ivec2(0), ivec2(CLUSTER_GRID_X - 1, CLUSTER_GRID_Y - 1)); + float back = front + span / raylength; + int first = clamp(int(floor(cluster_slice_bias + log(max(front, 1e-4)) * cluster_slice_scale)), 0, CLUSTER_GRID_Z - 1); + int last = clamp(int(floor(cluster_slice_bias + log(max(back, 1e-4)) * cluster_slice_scale)), first, min(first + 3, CLUSTER_GRID_Z - 1)); + float cellwidth = length(view_ray(coords + vec2(1.0 / float(grid.x), 0.0)) - ray); + vec3 lamps = vec3(0.0); + for (int slice = first; slice <= last; slice++) + { + float partfront = (slice == first) ? front : exp((float(slice) - cluster_slice_bias) / cluster_slice_scale); + float partback = (slice == last) ? back : exp((float(slice + 1) - cluster_slice_bias) / cluster_slice_scale); + float partspan = max(partback - partfront, 0.0) * raylength; + if (partspan <= 0.0) + continue; + vec3 partstart = ray * partfront; + cluster_range_s range = cluster_ranges[uint(tile.x + CLUSTER_GRID_X * (tile.y + CLUSTER_GRID_Y * slice))]; + vec3 partlamps = vec3(0.0); + for (uint i = 0U; i < range.count; i++) + { + uint index = cluster_light_indices[range.offset + i]; + if (int(index) == cablamp) + continue; + partlamps += lamp_scattering(lights[index], partstart, raydirection, partspan, cellwidth, offset.z); + } + lamps += partlamps * (partspan / max(span, 1e-4)); + } + light += lamps * options.y; + } + result = vec4(light * fog_haze.w * extinction, extinction); + } + + // the previous frame, where the middle of the cell was then: the cells of the history hold what the cells were on the whole, + // not at the point they were sampled at + float share = options.x; + if (share < 1.0) + { + vec3 centre = mat3(view_to_world) * (view_ray((vec2(cell.xy) + 0.5) / vec2(grid.xy)) * slice_depth(float(cell.z) + 0.5)); + vec4 clip = previous_projection_view * vec4(centre - previous_camera, 1.0); + if (clip.w > 0.0) + { + float previousslice = log(max((clip.w + FOG_VOLUME_BIAS) / (previous_slicing.x + FOG_VOLUME_BIAS), 1e-6)) * previous_slicing.y; + vec3 previous = vec3(clip.xy / clip.w * 0.5 + 0.5, previousslice / float(grid.z)); + if (all(greaterThanEqual(previous, vec3(0.0))) && all(lessThanEqual(previous, vec3(1.0)))) + result = mix(textureLod(fog_history, previous, 0.0), result, share); + } + } + if (any(isnan(result))) + result = vec4(0.0); + imageStore(fog_scattering, cell, result); +} diff --git a/shaders/vk14/fog_integrate.comp b/shaders/vk14/fog_integrate.comp new file mode 100644 index 00000000..2a0f3f83 --- /dev/null +++ b/shaders/vk14/fog_integrate.comp @@ -0,0 +1,91 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the fog volume of the main view, second pass: the light and extinction of the cells (fog_inject.comp) added up along the rays +// of the view, front to back, into what the fog adds and lets through from the camera to the far side of every slice +// (apply_fog.glsl). the light of a cell is integrated over its length as Hillaire (2015) does it, so it doesn't grow with +// the size of the cell. the light of the cells is taken with the light of their neighbours across the view, a 3x3 tent filter +// (four fetches of the linear filter): every cell is sampled at a point of its own in a frame, and what is left of that after +// the blending over frames differs from cell to cell, which the filter evens out. the history keeps the cells unfiltered. +// past the last slice one more: the fog all the way through the volume without the low clouds it holds, for the sky + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" +#include "weather_common.glsl" + +layout(local_size_x = WEATHER_GROUP_SIZE, local_size_y = WEATHER_GROUP_SIZE, local_size_z = 1) in; + +layout(set = COMPUTE_SET, binding = 0) uniform sampler3D fog_scattering; +layout(set = PASS_SET, binding = CLOUD_SHAPE_TEX) uniform sampler3D cloud_shape_noise; +layout(rgba16f, set = COMPUTE_SET, binding = 1) uniform writeonly image3D fog_integrated; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + ivec3 grid; + mat4 inverse_projection; + mat4 view_to_world; + // x: near, y: far, z: slices / log((far + FOG_VOLUME_BIAS) / (near + FOG_VOLUME_BIAS)) + vec4 slicing; +}; + +// extinction (1/m) of the haze and the ground fog at a point, made as fog_inject.comp makes them +float fog_clear_extinction(vec3 world) +{ + float height = weather_camera.x + world.y; + float haze = fog_haze.x * exp(-fog_haze.y * max(height, 0.0)); + float ground = fog_ground.x * exp(-fog_ground.y * max(height - fog_ground.z, 0.0)); + if (ground > 0.0 && fog_ground.w > 0.0) + { + float patches = textureLod(cloud_shape_noise, (world + fog_noise.xyz) * fog_noise.w, 0.0).g; + ground *= mix(1.0, clamp((patches - 0.3) * 2.6, 0.0, 2.2), fog_ground.w); + } + return haze + ground; +} + +void main() +{ + ivec2 column = ivec2(gl_GlobalInvocationID.xy); + if (any(greaterThanEqual(column, grid.xy))) + return; + vec2 coords = (vec2(column) + 0.5) / vec2(grid.xy); + vec4 near = inverse_projection * vec4(coords * 2.0 - 1.0, 1.0, 1.0); + vec3 ray = near.xyz / near.w; + // metres along the ray per metre of the depth + float stretch = length(ray / -ray.z); + + vec3 light = vec3(0.0); + float transmittance = 1.0; + // the same without the clouds: over the sky the ray march draws them (clouds_march.comp), and the volume laid over it would draw + // those within its reach a second time, blurred to the size of the cells and cut into bands by the slices + vec3 clearlight = vec3(0.0); + float cleartransmittance = 1.0; + float front = slicing.x; + for (int slice = 0; slice < grid.z; slice++) + { + float back = (slicing.x + FOG_VOLUME_BIAS) * exp(float(slice + 1) / slicing.z) - FOG_VOLUME_BIAS; + float span = (back - front) * stretch; + front = back; + vec4 cell = texelFetch(fog_scattering, ivec3(column, slice), 0); + float depthcoord = (float(slice) + 0.5) / float(grid.z); + vec2 texel = 1.0 / vec2(grid.xy); + vec4 filtered = 0.25 * (textureLod(fog_scattering, vec3(coords + vec2(-0.5, -0.5) * texel, depthcoord), 0.0) + + textureLod(fog_scattering, vec3(coords + vec2(0.5, -0.5) * texel, depthcoord), 0.0) + + textureLod(fog_scattering, vec3(coords + vec2(-0.5, 0.5) * texel, depthcoord), 0.0) + + textureLod(fog_scattering, vec3(coords + vec2(0.5, 0.5) * texel, depthcoord), 0.0)); + cell.rgb = filtered.rgb; + float extinction = max(cell.a, 1e-7); + float through = exp(-extinction * span); + light += transmittance * cell.rgb * ((1.0 - through) / extinction); + transmittance *= through; + imageStore(fog_integrated, ivec3(column, slice), vec4(light, transmittance)); + // the haze and the ground fog of the cell, at its middle, and their light as that of the cell per extinction, the lamps and the + // shadows with it; what is left of the extinction is the clouds. the light per extinction from the filtered cells, light and + // extinction alike: the filtered light over the extinction of the cell alone would bring in the light of a cloud next to it + float middle = (slicing.x + FOG_VOLUME_BIAS) * exp((float(slice) + 0.5) / slicing.z) - FOG_VOLUME_BIAS; + float clear = min(fog_clear_extinction(mat3(view_to_world) * (ray / -ray.z * middle)), extinction); + float clearthrough = exp(-clear * span); + clearlight += cleartransmittance * (filtered.rgb / max(filtered.a, 1e-7)) * (1.0 - clearthrough); + cleartransmittance *= clearthrough; + } + imageStore(fog_integrated, ivec3(column, grid.z), vec4(clearlight, cleartransmittance)); +} diff --git a/shaders/vk14/fog_volume.glsl b/shaders/vk14/fog_volume.glsl new file mode 100644 index 00000000..dd61ce48 --- /dev/null +++ b/shaders/vk14/fog_volume.glsl @@ -0,0 +1,73 @@ +// fog between the camera and a point of the view space (weather_common.glsl): from the fog volume of the main view +// (fog_inject.comp, fog_integrate.comp) up to its far end and along the ray past it, or along the ray all the way in the +// other passes. Fragcoord: the pixel of the target the point is seen at. the volume holds one slice more than fog_volume_params.w: +// the fog through all of it without the clouds, for the sky +#ifndef FOG_VOLUME_GLSL +#define FOG_VOLUME_GLSL + +#include "weather_common.glsl" + +layout(set = PASS_SET, binding = FOG_VOLUME_TEX) uniform sampler3D fog_volume; + +// the volume at a point of the view (0-1) and a coordinate along the slices, filtered across the view with a cubic B-spline +// (four fetches of the linear filter): a cell is several pixels wide, and the linear filter alone draws the shafts of light and +// the edges of shadows in the fog as steps +vec4 fog_volume_at(vec2 coords, float slice) +{ + vec2 size = vec2(textureSize(fog_volume, 0).xy); + vec2 texel = coords * size - 0.5; + vec2 base = floor(texel); + vec2 f = texel - base; + vec2 f2 = f * f; + vec2 f3 = f2 * f; + vec2 w0 = (1.0 - 3.0 * f + 3.0 * f2 - f3) / 6.0; + vec2 w1 = (4.0 - 6.0 * f2 + 3.0 * f3) / 6.0; + vec2 w2 = (1.0 + 3.0 * f + 3.0 * f2 - 3.0 * f3) / 6.0; + vec2 w3 = f3 / 6.0; + vec2 g0 = w0 + w1; + vec2 g1 = w2 + w3; + vec2 h0 = (base - 0.5 + w1 / g0) / size; + vec2 h1 = (base + 1.5 + w3 / g1) / size; + return g0.y * (g0.x * textureLod(fog_volume, vec3(h0.x, h0.y, slice), 0.0) + g1.x * textureLod(fog_volume, vec3(h1.x, h0.y, slice), 0.0)) + + g1.y * (g0.x * textureLod(fog_volume, vec3(h0.x, h1.y, slice), 0.0) + g1.x * textureLod(fog_volume, vec3(h1.x, h1.y, slice), 0.0)); +} + +// rgb: light the fog adds, a: what it lets through +vec4 fog_along_view(vec3 view, vec2 fragcoord) +{ + float span_distance = length(view); + if (span_distance <= 0.0) + return vec4(0.0, 0.0, 0.0, 1.0); + vec3 direction = mat3(inv_view) * (view / span_distance); + if (fog_volume_params.x < 0.5) + return fog_analytic(direction, 0.0, span_distance, false); + // slices are spaced exponentially in the view depth plus FOG_VOLUME_BIAS; slice k holds the fog up to its far side + float depth = max(-view.z, 1e-4); + float slices = fog_volume_params.w; + float slice = log(max((depth + FOG_VOLUME_BIAS) / (fog_volume_params.y + FOG_VOLUME_BIAS), 1e-6)) * fog_volume_params.z; + vec2 coords = fragcoord * fog_volume_target.xy; + if (slice < 1.0) + return mix(vec4(0.0, 0.0, 0.0, 1.0), fog_volume_at(coords, 0.5 / (slices + 1.0)), max(slice, 0.0)); + if (slice < slices) + return fog_volume_at(coords, (slice - 0.5) / (slices + 1.0)); + vec4 volume = fog_volume_at(coords, (slices - 0.5) / (slices + 1.0)); + vec4 rest = fog_analytic(direction, fog_volume_target.z * span_distance / depth, span_distance, false); + return vec4(volume.rgb + volume.a * rest.rgb, volume.a * rest.a); +} + +// fog in front of the sky in a view space direction: through the fog volume to its far end, then along the ray without the +// haze the atmosphere draws already. the far fog of the volume and the fog over the sky behind it are the same then. the volume +// is taken without the low clouds it holds (its extra slice): the ray march draws them over the sky already +vec4 fog_sky(vec3 viewdirection, vec2 fragcoord) +{ + vec3 view = normalize(viewdirection); + vec3 direction = mat3(inv_view) * view; + if (fog_volume_params.x < 0.5 || view.z > -1e-3) + return fog_analytic(direction, 0.0, 200000.0, true); + float slices = fog_volume_params.w; + vec4 volume = fog_volume_at(fragcoord * fog_volume_target.xy, (slices + 0.5) / (slices + 1.0)); + vec4 rest = fog_analytic(direction, fog_volume_target.z / -view.z, 200000.0, true); + return vec4(volume.rgb + volume.a * rest.rgb, volume.a * rest.a); +} + +#endif diff --git a/shaders/vk14/grass.frag b/shaders/vk14/grass.frag new file mode 100644 index 00000000..b55e0c6a --- /dev/null +++ b/shaders/vk14/grass.frag @@ -0,0 +1,49 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +// grass of the heightmap terrain (opengl46_grass, grass.vert): the blades lit like the rest of the scene + +layout(location = 0) in vec3 f_normal; +layout(location = 3) in vec4 f_pos; +layout(location = 12) in vec3 f_albedo; +layout(location = 4) in vec4 f_clip_pos; +layout(location = 5) in vec4 f_clip_future_pos; + +#include "common.glsl" + +layout(location = 0) out vec4 out_color; +#ifdef NO_MOTION_OUTPUT +// a pass without the velocity target: the writes go nowhere +vec4 out_motion; +#else +layout(location = 1) out vec4 out_motion; +#endif + +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +void main() +{ + vec3 fragnormal = normalize(f_normal); + glossiness = phong_glossiness(6.0); + metalic = 0.0; + vec3 fragcolor = apply_lights(ambient, fragnormal, f_albedo, 0.0, 0.2, shadow_tone); + + vec4 color = vec4(apply_fog(fragcolor), 1.0); + if (POSTFX_ENABLED) + { + out_color = color; + } + else + { + out_color = tonemap(color); + } + if (MOTIONBLUR_ENABLED) + { + vec2 a = (f_clip_future_pos.xy / f_clip_future_pos.w) * 0.5 + 0.5; + vec2 b = (f_clip_pos.xy / f_clip_pos.w) * 0.5 + 0.5; + + out_motion = vec4(a - b, 0.0f, 0.0f); + } +} diff --git a/shaders/vk14/grass.vert b/shaders/vk14/grass.vert new file mode 100644 index 00000000..ec9fdb7d --- /dev/null +++ b/shaders/vk14/grass.vert @@ -0,0 +1,247 @@ +#version 460 core +#define STAGE_VERTEX 1 +// grass of the heightmap terrain (opengl46_grass, vk14::grass_field): a blade made of the numbers of its vertex and its instance. +// +// the instances of a draw stand in a tile of a chunk. a blade takes its place in the tile from a sequence which spreads +// any number of its first members evenly (R2, shifted for each tile), so drawing fewer of them thins the grass out +// evenly; a blade picks its kind by the densities painted there, and gets its height, its turn and its lean from its own +// numbers, so it's the same in every frame. blades of no kind, in the holes, or past the share drawn at their distance are +// collapsed into a point outside the view. the vertices go up the blade in pairs, the last one is its tip. +// the chunk is drawn relative to its corner: x, z in metres from the corner, y the height as it is + +#include "common.glsl" + +// the parameters of the draw (the uniforms of the opengl renderer): those of the tile and of the frame in the push constants, +// the kinds of the chunk in a table (vk14::grass_field) +// x, z: corner of the tile in the chunk, metres; y: side of the tile; w: blades of the tile where its densest kind is painted fully +#define grass_tile pc.data[0] +// x: the share of the densest kind at the densest place of the tile (the blades of the tile are counted for it); y: segments +// of a blade; z, w: number of the tile in the world +#define grass_part pc.data[1] +// the camera relative to the corner of the chunk (y as it is) +#define grass_camera pc.data[2] +// x: distance the grass starts to thin out at, for speed; y: distance the fading starts at (gfx.grass.distance); z: where the +// grass ends (gfx.grass.fadedistance) +#define grass_lod pc.data[3] +// x, y: direction of the wind (x, z of the world); z: its strength, 0..1; w: time, seconds +#define grass_wind pc.data[4] +// the time of the year: x: how dry the grass is, y: how fresh (spring), z: how grown (share of the height) +#define grass_season pc.data[5] + +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D grass_heights; // heights of the grid points, R32F +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D grass_density; // densities of the kinds, RGBA8 +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D grass_holes; // holes cut in the ground, R8 +// the table of the chunk, RGBA32F, 4 x 5: a column for each kind; the rows: colour at the root and at the tip (linear rgb), +// height, its variation, width, lean; the colour dried out (linear rgb) and how much it follows the seasons; in the last row +// the share of the blades of each kind against the densest one of the chunk (x) and the chunk (y: side, metres, cells of its +// grid along a side, grass samples along a side less one) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 3) uniform sampler2D grass_kinds; + +vec4 grass_base[4]; +vec4 grass_tip[4]; +vec4 grass_shape[4]; +vec4 grass_dry[4]; +vec4 grass_share; +vec4 grass_chunk; + +void read_kinds() +{ + for (int i = 0; i < 4; ++i) + { + grass_base[i] = texelFetch(grass_kinds, ivec2(i, 0), 0); + grass_tip[i] = texelFetch(grass_kinds, ivec2(i, 1), 0); + grass_shape[i] = texelFetch(grass_kinds, ivec2(i, 2), 0); + grass_dry[i] = texelFetch(grass_kinds, ivec2(i, 3), 0); + } + grass_share = texelFetch(grass_kinds, ivec2(0, 4), 0); + grass_chunk = texelFetch(grass_kinds, ivec2(1, 4), 0); +} + +layout(location = 0) out vec3 f_normal; +layout(location = 3) out vec4 f_pos; +layout(location = 12) out vec3 f_albedo; +layout(location = 4) out vec4 f_clip_pos; +layout(location = 5) out vec4 f_clip_future_pos; +layout(location = 7) out vec4 UserData; + +uint hash(uint x) +{ + x ^= x >> 16; + x *= 0x7feb352dU; + x ^= x >> 15; + x *= 0x846ca68bU; + x ^= x >> 16; + return x; +} + +float random(inout uint state) +{ + state = hash(state); + return float(state >> 8) * (1.0 / 16777216.0); +} + +// the share of the blades drawn at specified distance for speed: fewer further away, each standing for more of them +float lod_thinning(float distance) +{ + return distance <= grass_lod.x ? 1.0 : pow(grass_lod.x / distance, 1.5); +} + +// the share of the blades drawn: the above, and fewer and fewer over the fading distance, where the grass really gets +// sparser; the same as lod_share() of opengl46grass.cpp +float lod_share(float distance) +{ + return lod_thinning(distance) * (1.0 - smoothstep(grass_lod.y, grass_lod.z, distance)); +} + +// height of the ground at a point of the chunk and its normal, the way the full grid is triangulated +float ground(vec2 point, out vec3 normal) +{ + float cells = grass_chunk.y; + float spacing = grass_chunk.x / cells; + vec2 place = clamp(point / spacing, vec2(0.0), vec2(cells)); + ivec2 cell = min(ivec2(place), ivec2(int(cells) - 1)); + vec2 t = place - vec2(cell); + float h00 = texelFetch(grass_heights, cell, 0).r; + float h10 = texelFetch(grass_heights, cell + ivec2(1, 0), 0).r; + float h01 = texelFetch(grass_heights, cell + ivec2(0, 1), 0).r; + float h11 = texelFetch(grass_heights, cell + ivec2(1, 1), 0).r; + // the quads are cut along the diagonal from (x0, z1) to (x1, z0), as the mesh is made + if (t.x + t.y <= 1.0) + { + normal = normalize(vec3(-(h10 - h00) / spacing, 1.0, -(h01 - h00) / spacing)); + return h00 + t.x * (h10 - h00) + t.y * (h01 - h00); + } + normal = normalize(vec3(-(h11 - h01) / spacing, 1.0, -(h11 - h10) / spacing)); + return h11 + (1.0 - t.x) * (h01 - h11) + (1.0 - t.y) * (h10 - h11); +} + +void collapse() +{ + gl_Position = vec4(2.0, 2.0, 2.0, 1.0); + f_normal = vec3(0.0, 1.0, 0.0); + f_pos = vec4(0.0); + f_albedo = vec3(0.0); + f_clip_pos = gl_Position; + f_clip_future_pos = gl_Position; + UserData = vec4(0.0); +} + +void main() +{ + read_kinds(); + int blade = gl_InstanceIndex; + // the numbers of the blade, drawn in the same order whatever becomes of them + uint state = hash(uint(blade) * 0x9E3779B9U ^ hash(uint(int(grass_part.z)) * 0x85EBCA6BU ^ hash(uint(int(grass_part.w)) + 0x27D4EB2FU))); + uint tilestate = hash(uint(int(grass_part.z)) * 0x85EBCA6BU ^ hash(uint(int(grass_part.w)) + 0x165667B1U)); + vec2 shift = vec2(random(tilestate), random(tilestate)); + vec2 jitter = vec2(random(state), random(state)) - 0.5; + float pick = random(state); + float heightrandom = random(state) * 2.0 - 1.0; + float widthrandom = random(state); + float turnrandom = random(state) * 6.2831853; + float leanrandom = random(state) * 6.2831853; + float leanamount = random(state); + float shade = random(state); + float phase = random(state); + float early = random(state); + + // place: R2 spreads the first blades evenly at any count, a little jitter breaks its lattice + vec2 spot = fract(shift + float(blade) * vec2(0.7548776662466927, 0.5698402909980532)); + vec2 point = grass_tile.xy + fract(spot + jitter * (0.7 / sqrt(grass_tile.w))) * grass_tile.z; + if (any(lessThan(point, vec2(0.0))) || any(greaterThanEqual(point, vec2(grass_chunk.x)))) + { + collapse(); + return; + } + vec2 local = point / grass_chunk.x; + float samples = grass_chunk.z; + vec4 density = texture(grass_density, (local * samples + 0.5) / (samples + 1.0)); + float holeside = float(textureSize(grass_holes, 0).x); + float hole = texture(grass_holes, (local * (holeside - 1.0) + 0.5) / holeside).r; + + // the kind: the blades of the tile are shared among the kinds by their densities here + vec4 cover = density * grass_share / max(grass_part.x, 1e-3); + float covered = cover.x + cover.y + cover.z + cover.w; + if (covered > 1.0) + cover /= covered; + int kind = -1; + float sum = 0.0; + for (int i = 0; i < 4; ++i) + { + sum += cover[i]; + if (pick < sum) + { + kind = i; + break; + } + } + + vec3 groundnormal; + vec3 root = vec3(point.x, ground(point, groundnormal), point.y); + float distance = length(root - grass_camera.xyz); + float share = lod_share(distance); + float rank = (float(blade) + 0.5) / grass_tile.w; + if (kind < 0 || hole > 0.5 || rank >= share || distance > grass_lod.z) + { + collapse(); + return; + } + + vec4 shape = grass_shape[kind]; + float height = shape.x * (1.0 + shape.y * heightrandom); + // the last of the blades drawn at the distance grow in, and the grass fades out at the end of its reach + height *= 1.0 - smoothstep(share * 0.8, share, rank); + // lower in winter, growing in spring + float seasonal = grass_dry[kind].a; + height *= mix(1.0, grass_season.z, seasonal); + // where the paint thins out the blades get shorter + height *= mix(0.5, 1.0, clamp(density[kind] * 1.5, 0.0, 1.0)); + // the blades drawn further away stand for more of them, so they're wider + float width = shape.z * (0.8 + 0.4 * widthrandom) / sqrt(lod_thinning(distance)); + + // the face of the blade turns towards the camera further away, so the blades don't vanish edge on + vec2 facing = vec2(cos(turnrandom), sin(turnrandom)); + vec2 toview = grass_camera.xz - root.xz; + toview = toview / max(length(toview), 1e-3); + facing = mix(facing, toview, smoothstep(grass_lod.x * 0.5, grass_lod.x * 2.0, distance)); + facing = facing / max(length(facing), 1e-3); + vec2 across = vec2(-facing.y, facing.x); + + // the lean of the blade and the wind: a steady push along it, with gusts running across the field, and a flutter + vec2 lean = vec2(cos(leanrandom), sin(leanrandom)) * (shape.w * (0.4 + 0.6 * leanamount)); + float gust = 0.5 + 0.5 * sin(grass_wind.w * 0.9 - dot(root.xz, grass_wind.xy) * 0.12); + float flutter = sin(grass_wind.w * (2.0 + phase) + phase * 6.2831853); + vec2 sway = grass_wind.xy * grass_wind.z * (0.15 + 0.45 * gust + 0.08 * flutter); + vec2 bend = lean + sway; + // a long lean takes some of the height + float bent = min(length(bend), 0.9); + + int segments = int(grass_part.y); + int level = min(gl_VertexIndex / 2, segments); + float side = (gl_VertexIndex >= segments * 2) ? 0.0 : ((gl_VertexIndex & 1) == 0 ? -0.5 : 0.5); + float t = float(level) / float(segments); + float halfwidth = width * (1.0 - pow(t, 1.5)); + vec2 xz = root.xz + across * (side * halfwidth) + bend * (height * t * t); + float y = root.y - 0.02 + height * t * (1.0 - 0.35 * bent * t); + vec3 position = vec3(xz.x, y, xz.y); + + // lit mostly as the ground under it, which keeps a field of blades from shimmering + vec3 normal = normalize(groundnormal * 0.8 + vec3(facing.x, 0.0, facing.y) * (side == 0.0 ? 0.0 : 0.25) + vec3(0.0, 0.3, 0.0)); + // the seasons: some blades dry out (and green up) earlier than others, so a field turns gradually; a dry blade takes the + // dry colour, darker at its root. in spring the green is fresher + float dry = clamp(grass_season.x * seasonal * (0.6 + 0.8 * early), 0.0, 1.0); + vec3 bottom = mix(grass_base[kind].rgb, grass_dry[kind].rgb * 0.45, dry); + vec3 top = mix(grass_tip[kind].rgb, grass_dry[kind].rgb, dry); + top *= mix(vec3(1.0), vec3(1.08, 1.22, 0.85), grass_season.y * seasonal * (1.0 - dry)); + vec3 albedo = mix(bottom, top, t) * (0.85 + 0.3 * shade); + // the lower part of the blades is shaded by the others + albedo *= mix(0.7, 1.0, t); + + f_pos = modelview * vec4(position, 1.0); + f_normal = normalize(modelviewnormal * normal); + f_albedo = albedo; + f_clip_pos = projection * f_pos; + f_clip_future_pos = (projection * future) * f_pos; + gl_Position = f_clip_pos; + UserData = vec4(0.0); +} diff --git a/shaders/vk14/gtao_denoise.comp b/shaders/vk14/gtao_denoise.comp new file mode 100644 index 00000000..d741638f --- /dev/null +++ b/shaders/vk14/gtao_denoise.comp @@ -0,0 +1,58 @@ +#version 460 core +#define STAGE_COMPUTE 1 +#define LAYOUT_COMPUTE 1 +#include "common.glsl" +// smooths the ambient occlusion of gtao_main.comp: the average of the 4x4 pixels around each one, which takes out the +// repeating 4x4 pattern of the slices, with the pixels at a clearly different distance left out so the occlusion does +// not leak across the edges of objects. one invocation per pixel of the half size target + +layout(local_size_x = 8, local_size_y = 8) in; + +layout(set = COMPUTE_SET, binding = 0) uniform sampler2D depth_tex; +layout(set = COMPUTE_SET, binding = 1) uniform sampler2D ao_tex; +layout(set = COMPUTE_SET, binding = 2, r8) writeonly uniform image2D ao_target; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + mat4 inv_projection; +}; + +// relative difference of the distance at which a neighbour stops counting +const float DEPTH_TOLERANCE = 0.05; + +float view_distance(ivec2 texel) +{ + ivec2 full_size = textureSize(depth_tex, 0); + float depth = texelFetch(depth_tex, min(texel * 2, full_size - 1), 0).r; + if (depth <= 0.0) + return 1e30; + vec2 uv = (vec2(texel * 2) + 0.5) / vec2(full_size); + vec4 position = inv_projection * vec4(uv * 2.0 - 1.0, depth, 1.0); + return length(position.xyz / position.w); +} + +void main() +{ + ivec2 texel = ivec2(gl_GlobalInvocationID.xy); + ivec2 size = imageSize(ao_target); + if (texel.x >= size.x || texel.y >= size.y) + return; + + float distance = view_distance(texel); + float sum = 0.0; + float weights = 0.0; + for (int y = -2; y < 2; ++y) + { + for (int x = -2; x < 2; ++x) + { + ivec2 sampletexel = clamp(texel + ivec2(x, y), ivec2(0), size - 1); + float sampledistance = view_distance(sampletexel); + float weight = clamp(1.0 - abs(sampledistance - distance) / (DEPTH_TOLERANCE * distance), 0.0, 1.0); + sum += texelFetch(ao_tex, sampletexel, 0).r * weight; + weights += weight; + } + } + // the pixel itself is in the window with the full weight, so weights >= 1 + imageStore(ao_target, texel, vec4(sum / weights)); +} diff --git a/shaders/vk14/gtao_main.comp b/shaders/vk14/gtao_main.comp new file mode 100644 index 00000000..14f74ef4 --- /dev/null +++ b/shaders/vk14/gtao_main.comp @@ -0,0 +1,160 @@ +#version 460 core +#define STAGE_COMPUTE 1 +#define LAYOUT_COMPUTE 1 +#include "common.glsl" +// ground truth ambient occlusion (Jimenez et al. 2016, "Practical realtime strategies for accurate indirect occlusion"; +// integration as in Intel XeGTAO) at half the resolution of the image. for every pixel, a few slices through the +// hemisphere around the view direction; in each the horizons on both sides are searched in the depth buffer and the +// cosine weighted visible arc between them is integrated against the normal rebuilt from the depth. +// one invocation per pixel of the half size target. the pattern of the slice directions and steps repeats every 4x4 +// pixels; gtao_denoise.comp averages it out + +layout(local_size_x = 8, local_size_y = 8) in; + +// depth of the scene at full resolution: reversed, 1 at the near plane, 0 at infinity (the sky) +layout(set = COMPUTE_SET, binding = 0) uniform sampler2D depth_tex; +layout(set = COMPUTE_SET, binding = 1, r8) writeonly uniform image2D ao_target; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + mat4 inv_projection; + // x: radius in metres, y: projection[0][0] times half the width of the target in pixels + vec2 params; + // the row of the projection which gives the clip w of a view space position + vec4 clipw_row; + // x: number of slices, y: steps on each side of a slice + ivec2 quality; +}; + +const float PI = 3.14159265; +const float HALF_PI = 1.57079633; +// share of the radius over which the weight of an occluder falls to nothing +const float FALLOFF_RANGE = 0.615; + +// depth of the pixel of the half size target: the top left one of the four full size pixels it covers, so the +// application (postfx_ssao_apply.frag) finds the same depth for it +float half_depth(ivec2 texel) +{ + ivec2 full_size = textureSize(depth_tex, 0); + return texelFetch(depth_tex, min(texel * 2, full_size - 1), 0).r; +} + +vec3 view_position(ivec2 texel, float depth) +{ + ivec2 full_size = textureSize(depth_tex, 0); + vec2 uv = (vec2(texel * 2) + 0.5) / vec2(full_size); + vec4 position = inv_projection * vec4(uv * 2.0 - 1.0, depth, 1.0); + return position.xyz / position.w; +} + +// 4x4 ordered pattern, values 0..15 +const float BAYER[16] = float[](0.0, 8.0, 2.0, 10.0, 12.0, 4.0, 14.0, 6.0, 3.0, 11.0, 1.0, 9.0, 15.0, 7.0, 13.0, 5.0); + +void main() +{ + ivec2 texel = ivec2(gl_GlobalInvocationID.xy); + ivec2 size = imageSize(ao_target); + if (texel.x >= size.x || texel.y >= size.y) + return; + + float depth = half_depth(texel); + if (depth <= 0.0) + { + imageStore(ao_target, texel, vec4(1.0)); + return; + } + vec3 position = view_position(texel, depth); + vec3 view = normalize(-position); + + // normal from the neighbours: on each axis the one closer in depth, so edges of objects do not bend it + vec3 left = view_position(texel - ivec2(1, 0), half_depth(texel - ivec2(1, 0))); + vec3 right = view_position(texel + ivec2(1, 0), half_depth(texel + ivec2(1, 0))); + vec3 down = view_position(texel - ivec2(0, 1), half_depth(texel - ivec2(0, 1))); + vec3 up = view_position(texel + ivec2(0, 1), half_depth(texel + ivec2(0, 1))); + vec3 dx = (abs(right.z - position.z) < abs(position.z - left.z)) ? right - position : position - left; + vec3 dy = (abs(up.z - position.z) < abs(position.z - down.z)) ? up - position : position - down; + vec3 normal = normalize(cross(dx, dy)); + if (dot(normal, view) < 0.0) + normal = -normal; + + float radius = params.x; + float clipw = dot(clipw_row, vec4(position, 1.0)); + float screenradius = params.y * radius / max(clipw, 1e-6); + // smaller than a pixel: nothing to find + if (screenradius < 1.0) + { + imageStore(ao_target, texel, vec4(1.0)); + return; + } + screenradius = min(screenradius, 256.0); + + float falloffrange = FALLOFF_RANGE * radius; + float falloffmul = -1.0 / falloffrange; + float falloffadd = (radius - falloffrange) / falloffrange + 1.0; + + int pattern = (texel.x & 3) + 4 * (texel.y & 3); + float slicenoise = (BAYER[pattern] + 0.5) / 16.0; + float stepnoise = fract(float(pattern) * 0.618034 + 0.25); + + int slices = max(quality.x, 1); + int steps = max(quality.y, 1); + float visibility = 0.0; + for (int slice = 0; slice < slices; ++slice) + { + float angle = (float(slice) + slicenoise) * PI / float(slices); + vec2 omega = vec2(cos(angle), sin(angle)); + vec3 direction = vec3(omega, 0.0); + vec3 orthodirection = direction - dot(direction, view) * view; + vec3 axis = normalize(cross(orthodirection, view)); + vec3 projectednormal = normal - axis * dot(normal, axis); + float projectedlength = length(projectednormal); + if (projectedlength < 1e-4) + continue; + float signnormal = sign(dot(orthodirection, projectednormal)); + float cosnormal = clamp(dot(projectednormal, view) / projectedlength, 0.0, 1.0); + float n = signnormal * acos(cosnormal); + + float lowcos0 = cos(n + HALF_PI); + float lowcos1 = cos(n - HALF_PI); + float horizoncos0 = lowcos0; + float horizoncos1 = lowcos1; + + for (int step = 0; step < steps; ++step) + { + // denser near the pixel, where the occluders matter most + float s = (float(step) + stepnoise) / float(steps); + s = s * s; + vec2 offset = omega * max(s * screenradius, float(step) + 1.0); + for (int side = 0; side < 2; ++side) + { + ivec2 sampletexel = texel + ivec2(round(side == 0 ? offset : -offset)); + if (any(lessThan(sampletexel, ivec2(0))) || any(greaterThanEqual(sampletexel, size))) + continue; + float sampledepth = half_depth(sampletexel); + if (sampledepth <= 0.0) + continue; + vec3 delta = view_position(sampletexel, sampledepth) - position; + float distance = length(delta); + if (distance < 1e-5) + continue; + float horizoncos = dot(delta / distance, view); + float weight = clamp(distance * falloffmul + falloffadd, 0.0, 1.0); + if (side == 0) + horizoncos0 = max(horizoncos0, mix(lowcos0, horizoncos, weight)); + else + horizoncos1 = max(horizoncos1, mix(lowcos1, horizoncos, weight)); + } + } + + float h0 = -acos(clamp(horizoncos1, -1.0, 1.0)); + float h1 = acos(clamp(horizoncos0, -1.0, 1.0)); + h0 = n + clamp(h0 - n, -HALF_PI, HALF_PI); + h1 = n + clamp(h1 - n, -HALF_PI, HALF_PI); + float arc0 = (cosnormal + 2.0 * h0 * sin(n) - cos(2.0 * h0 - n)) * 0.25; + float arc1 = (cosnormal + 2.0 * h1 * sin(n) - cos(2.0 * h1 - n)) * 0.25; + visibility += projectedlength * (arc0 + arc1); + } + visibility = clamp(visibility / float(slices), 0.0, 1.0); + imageStore(ao_target, texel, vec4(visibility)); +} diff --git a/shaders/vk14/hiz_build.comp b/shaders/vk14/hiz_build.comp new file mode 100644 index 00000000..1ab9f4b4 --- /dev/null +++ b/shaders/vk14/hiz_build.comp @@ -0,0 +1,46 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// one level of the depth pyramid of the occlusion test of the instances (the hi-z of the renderers): each texel holds the farthest +// depth of the texels of the level below it which it covers. the depth is reversed (1 near, 0 far), so the farthest is +// the smallest. the first level is made from the resolved depth of the scene, the others from the level below. +// a level halves the one below, rounding down; when that one has an odd size, the last row and column of the new level +// take its last row and column as well, so that nothing is left out + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" + +layout(local_size_x = HIZ_GROUP_SIZE, local_size_y = HIZ_GROUP_SIZE) in; + +layout(set = COMPUTE_SET, binding = 0) uniform sampler2D source; +layout(r32f, set = COMPUTE_SET, binding = 1) writeonly uniform image2D target; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + // xy: size of the source level, zw: size of the target level + ivec4 sizes; + // x: level of the source read + ivec4 params; +}; + +void main() +{ + ivec2 texel = ivec2(gl_GlobalInvocationID.xy); + if (any(greaterThanEqual(texel, sizes.zw))) + return; + + ivec2 first = min(2 * texel, sizes.xy - 1); + ivec2 last = min(2 * texel + 1, sizes.xy - 1); + if (texel.x == sizes.z - 1) + last.x = sizes.x - 1; + if (texel.y == sizes.w - 1) + last.y = sizes.y - 1; + + float farthest = 1.0; + for (int y = first.y; y <= last.y; ++y) + { + for (int x = first.x; x <= last.x; ++x) + farthest = min(farthest, texelFetch(source, ivec2(x, y), params.x).r); + } + imageStore(target, texel, vec4(farthest)); +} diff --git a/shaders/vk14/instance_cull.comp b/shaders/vk14/instance_cull.comp new file mode 100644 index 00000000..c6b8e098 --- /dev/null +++ b/shaders/vk14/instance_cull.comp @@ -0,0 +1,291 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// culls the model instances of the buckets queued in a render pass and assigns them their lod bands, with the tests +// gfx::scene_walk::Render_Instanced() makes on the cpu. one invocation per record of a queued bucket. the sleepers of the +// tracks queued come as buckets too, with the tests of gfx::scene_walk::Render_Sleepers(). +// stage 0 counts the instances which pass the tests, per entry (lod band of a group); after instance_cull_offsets.comp +// turned the counts into the first slots of the entries, stage 1 writes the modelview matrices of the instances drawn first +// there, in no particular order, and stage 2 (two phases) those of the instances drawn second after them. +// with the occlusion test on, an instance whose bounding sphere lies whole behind the depth already drawn in the pass +// (the farthest depth of the pyramid over the part of the view it takes, see hiz_build.comp) is left out as well. +// in two phases (the second phase of the culling) stage 1 takes only the instances seen in the previous frame, +// which are drawn, the pyramid is made again with them in it, and stage 2 tests the others against it; it also keeps for +// the next frame which instances it found in view. an instance seen before but hidden behind the depth of the first +// pyramid is hidden behind the second one as well (it only holds nearer depth), so each instance is drawn at most once, +// and everything left out is hidden behind something drawn + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" + +layout(local_size_x = INSTANCE_CULL_GROUP_SIZE) in; + +// scene::basic_cell::instance_record +struct instance_record +{ + vec3 position; // relative to the origin of the bucket + float radius; + float rangesquaredmin; + float rangesquaredmax; + uint flags; + uint visible; // the instance was seen in the previous frame (written by stage 2, 0 when the cpu sends the record) + vec4 rotationscale[3]; +}; + +struct instance_cull_job +{ + vec3 origin; + uint first; + vec3 distanceorigin; + uint count; + uint group; + uint candidatestart; + uint flags; + uint pad; +}; + +struct instance_cull_group +{ + uint lodfirst; + uint lodcount; + uint entryfirst; + uint pad; +}; + +layout(std430, set = COMPUTE_SET, binding = 0) buffer instance_record_ssbo +{ + instance_record records[]; +}; +layout(std430, set = COMPUTE_SET, binding = 1) readonly buffer instance_job_ssbo +{ + instance_cull_job jobs[]; +}; +layout(std430, set = COMPUTE_SET, binding = 2) readonly buffer instance_group_ssbo +{ + instance_cull_group groups[]; +}; +layout(std430, set = COMPUTE_SET, binding = 3) readonly buffer instance_lod_ssbo +{ + float lodbounds[]; +}; +layout(std430, set = COMPUTE_SET, binding = 4) buffer instance_counter_ssbo +{ + uint counters[]; +}; +layout(std430, set = COMPUTE_SET, binding = 5) writeonly buffer instance_output_ssbo +{ + mat4 modelviews[]; +}; + +// the parameters of the kernel (a block of the ring of the frame: more of them than the push constants hold) +layout(std140, set = COMPUTE_SET, binding = 7) uniform kernel_block +{ + // rotation of the view of the pass: the instances are placed relative to its camera + mat4 view; + // planes of the frustum of the pass, relative to its camera + vec4 planes[6]; + // x: scale of the squared distance, y: added to it past the range test, z: range of the scaled distance before the + // addition (reflections), w: squared draw range + vec4 distance_params; + // x: zoom factor + vec4 size_params; + // occlusion test: projection of the pass, x: on, y: levels of the pyramid, zw: size of its first level + mat4 hiz_projection; + ivec4 hiz_params; + // xy: size of the depth the pyramid was made from, pixels + vec4 hiz_screen; + // x: two phases, stage 1 takes only the instances seen in the previous frame + uvec4 phases; +}; +// the parameters which change from stage to stage (push constants). x: jobs, y: records in all jobs, z: entries, w: stage +layout(push_constant) uniform kernel_constants +{ + uvec4 counts; +}; + +// depth pyramid, farthest (smallest, reversed) depth per texel +layout(set = COMPUTE_SET, binding = 6) uniform sampler2D hiz; + +const uint INSTANCE_VISIBLE = 1u; +const uint INSTANCE_INSTANCEABLE = 2u; +// smallest part of the view an instance has to take +const float MIN_SCREEN_SIZE = 0.003; +// header of the counters, see gl::INSTANCE_COUNTER_HEADER +const uint COUNTER_OCCLUDED = 3u; +const uint COUNTER_FIRST = 4u; + +// the job which holds the given record: the last one starting at it or before it +uint find_job(uint candidate) +{ + uint low = 0u; + uint high = counts.x - 1u; + while (low < high) + { + uint middle = (low + high + 1u) / 2u; + if (jobs[middle].candidatestart <= candidate) + low = middle; + else + high = middle - 1u; + } + return low; +} + +// whether a sphere (view space) lies whole behind the depth in the pyramid over the rectangle of the view it takes +bool occluded(vec3 center, float radius) +{ + vec2 low = vec2(1.0); + vec2 high = vec2(-1.0); + float nearest = 0.0; + for (int corner = 0; corner < 8; ++corner) + { + vec3 offset = vec3(((corner & 1) != 0) ? radius : -radius, ((corner & 2) != 0) ? radius : -radius, ((corner & 4) != 0) ? radius : -radius); + vec4 clip = hiz_projection * vec4(center + offset, 1.0); + // reaches the plane of the camera: no rectangle to test + if (clip.w <= 1e-3) + return false; + vec3 ndc = clip.xyz / clip.w; + low = min(low, ndc.xy); + high = max(high, ndc.xy); + nearest = max(nearest, ndc.z); + } + if (any(greaterThan(low, vec2(1.0))) || any(lessThan(high, vec2(-1.0))) || (nearest >= 1.0)) + return false; + + // pixels of the rectangle, then texels of the first level: each covers 2x2 pixels + ivec2 size = hiz_params.zw; + ivec2 pixelfirst = ivec2(clamp((low * 0.5 + 0.5) * hiz_screen.xy, vec2(0.0), hiz_screen.xy - 1.0)); + ivec2 pixellast = ivec2(clamp((high * 0.5 + 0.5) * hiz_screen.xy, vec2(0.0), hiz_screen.xy - 1.0)); + ivec2 first = min(pixelfirst / 2, size - 1); + ivec2 last = min(pixellast / 2, size - 1); + // the level where the rectangle takes at most 2x2 texels + int level = 0; + while ((level < hiz_params.y - 1) && any(greaterThan((last >> level) - (first >> level), ivec2(1)))) + ++level; + ivec2 levelsize = max(size >> level, ivec2(1)); + first = min(first >> level, levelsize - 1); + last = min(last >> level, levelsize - 1); + + float farthest = 1.0; + for (int y = first.y; y <= last.y; ++y) + { + for (int x = first.x; x <= last.x; ++x) + farthest = min(farthest, texelFetch(hiz, ivec2(x, y), level).r); + } + return nearest < farthest; +} + +// the tests of the cpu (Render_Instanced(), for sleepers Render_Sleepers()), all but the occlusion one: the entry of the instance, or false when it's left out. +// Location: relative to the camera of the pass +bool passes(instance_cull_job job, instance_record record, out vec3 location, out uint entry) +{ + location = job.origin + record.position; + entry = 0u; + // instances hidden, or drawn on their own after getting animations or skins from events + if ((record.flags & (INSTANCE_VISIBLE | INSTANCE_INSTANCEABLE)) != (INSTANCE_VISIBLE | INSTANCE_INSTANCEABLE)) + return false; + + for (int plane = 0; plane < 6; ++plane) + { + if (dot(planes[plane].xyz, location) + planes[plane].w <= -record.radius) + return false; + } + + float distancesquared; + if ((job.flags & INSTANCE_JOB_SLEEPERS) != 0u) + { + // the sleepers of a track, as Render_Sleepers() takes them: the lod by the plain distance from the camera of the pass + distancesquared = dot(location, location); + } + else + { + vec3 offset = job.distanceorigin + record.position; + distancesquared = dot(offset, offset) * distance_params.x; + if (distancesquared > distance_params.z) + return false; + distancesquared += distance_params.y; + if ((distancesquared < record.rangesquaredmin) || (distancesquared >= record.rangesquaredmax)) + return false; + if (distancesquared > distance_params.w) + return false; + if (record.radius * record.radius * size_params.x / distancesquared < MIN_SCREEN_SIZE * MIN_SCREEN_SIZE) + return false; + } + + // band: the number of lod bounds the distance reached, like upper_bound over the sorted bounds + instance_cull_group group = groups[job.group]; + uint band = 0u; + for (uint bound = 0u; bound < group.lodcount; ++bound) + { + if (lodbounds[group.lodfirst + bound] <= distancesquared) + band = bound + 1u; + } + entry = group.entryfirst + band; + return true; +} + +void write(uint entry, instance_record record, vec3 location, instance_cull_job job) +{ + uint slot = atomicAdd(counters[INSTANCE_COUNTER_HEADER + counts.z + entry], 1u); + if ((job.flags & INSTANCE_JOB_SLEEPERS) != 0u) + { + // in the order Render_Sleepers() multiplies them (the view moved to the origin of the track, then the transform of the + // sleeper), which gives the very same matrix: the other order rounds differently, enough to shift the edges of sleepers + // sunk in the ballast by a pixel here and there + mat4 originview = view * mat4(vec4(1.0, 0.0, 0.0, 0.0), vec4(0.0, 1.0, 0.0, 0.0), vec4(0.0, 0.0, 1.0, 0.0), vec4(job.origin, 1.0)); + modelviews[slot] = originview * mat4(record.rotationscale[0], record.rotationscale[1], record.rotationscale[2], vec4(record.position, 1.0)); + return; + } + modelviews[slot] = view * mat4(record.rotationscale[0], record.rotationscale[1], record.rotationscale[2], vec4(location, 1.0)); +} + +void main() +{ + // the dispatch can be two dimensional when the records don't fit in the work group count limit of one + uint candidate = (gl_WorkGroupID.y * gl_NumWorkGroups.x + gl_WorkGroupID.x) * INSTANCE_CULL_GROUP_SIZE + gl_LocalInvocationIndex; + if (candidate >= counts.y) + return; + + instance_cull_job job = jobs[find_job(candidate)]; + uint index = job.first + (candidate - job.candidatestart); + instance_record record = records[index]; + vec3 location; + uint entry; + bool passed = passes(job, record, location, entry); + + if (counts.w == 0u) + { + // room in the output for every instance which passes, whatever the occlusion test says + if (passed) + atomicAdd(counters[INSTANCE_COUNTER_HEADER + entry], 1u); + return; + } + + bool seen = (record.visible != 0u); + if (counts.w == 1u) + { + // the ones seen in the previous frame first, the others wait for stage 2 + if ((false == passed) || ((phases.x != 0u) && (false == seen))) + return; + if ((hiz_params.x != 0) && occluded((view * vec4(location, 1.0)).xyz, record.radius)) + { + atomicAdd(counters[COUNTER_OCCLUDED], 1u); + return; + } + write(entry, record, location, job); + if (phases.x != 0u) + atomicAdd(counters[COUNTER_FIRST], 1u); + return; + } + + // stage 2, against the pyramid with the instances drawn first in it + bool inview = passed && (false == occluded((view * vec4(location, 1.0)).xyz, record.radius)); + if (passed && (false == seen)) + { + if (inview) + write(entry, record, location, job); + else + atomicAdd(counters[COUNTER_OCCLUDED], 1u); + } + uint visible = (inview ? 1u : 0u); + if (visible != record.visible) + records[index].visible = visible; +} diff --git a/shaders/vk14/instance_cull_commands.comp b/shaders/vk14/instance_cull_commands.comp new file mode 100644 index 00000000..70e45e07 --- /dev/null +++ b/shaders/vk14/instance_cull_commands.comp @@ -0,0 +1,69 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// fills in the instances of the indirect draw commands of the entries, after instance_cull.comp wrote the modelview +// matrices of the instances drawn. one invocation per command. the commands are kept three times over: the first copy +// draws the instances of stage 1, the second all of them, the third those of stage 2 (two phases), each from its first +// slot in the output on. stage 0 (after stage 1): the instance count of the first copy is how far stage 1 advanced the +// cursor of its entry, the base instance the first slot of the entry. stage 1 (after stage 2): the other two copies take +// the command of the first one, the second with the instances of both stages, the third with the ones past the first copy + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" + +layout(local_size_x = INSTANCE_COMMAND_GROUP_SIZE) in; + +layout(std430, set = COMPUTE_SET, binding = 0) readonly buffer instance_counter_ssbo +{ + uint counters[]; +}; + +// gl::instance_draw_command, eight uints each: count, instance count, first, base vertex (base instance of an array +// draw), base instance, entry, indexed, unused +layout(std430, set = COMPUTE_SET, binding = 1) buffer instance_command_ssbo +{ + uint commands[]; +}; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + // x: commands (of one copy), y: entries, z: stage + uvec4 counts; +}; + +void set_instances(uint field, uint count, uint base) +{ + commands[field + 1u] = count; + if (commands[field + 6u] != 0u) + commands[field + 4u] = base; + else + commands[field + 3u] = base; +} + +void main() +{ + uint command = gl_GlobalInvocationID.x; + if (command >= counts.x) + return; + + uint field = command * 8u; + uint entry = commands[field + 5u]; + uint first = counters[INSTANCE_COUNTER_HEADER + 2u * counts.y + entry]; + uint count = counters[INSTANCE_COUNTER_HEADER + counts.y + entry] - first; + if (counts.z == 0u) + { + set_instances(field, count, first); + return; + } + + uint drawnfirst = commands[field + 1u]; + uint all = (counts.x + command) * 8u; + uint second = (2u * counts.x + command) * 8u; + for (uint value = 0u; value < 8u; ++value) + { + commands[all + value] = commands[field + value]; + commands[second + value] = commands[field + value]; + } + set_instances(all, count, first); + set_instances(second, count - drawnfirst, first + drawnfirst); +} diff --git a/shaders/vk14/instance_cull_offsets.comp b/shaders/vk14/instance_cull_offsets.comp new file mode 100644 index 00000000..c45dea9d --- /dev/null +++ b/shaders/vk14/instance_cull_offsets.comp @@ -0,0 +1,62 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// turns the counts of instances of the entries, written by stage 0 of instance_cull.comp, into the first slots of the +// entries in the output (cursors, which stages 1 and 2 advance as they write, and the same first slots kept for +// instance_cull_commands.comp), and puts the total in the header. +// a single work group: each invocation sums a run of the entries, then the sums are scanned in shared memory + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" + +layout(local_size_x = INSTANCE_OFFSETS_GROUP_SIZE) in; + +layout(std430, set = COMPUTE_SET, binding = 0) buffer instance_counter_ssbo +{ + uint counters[]; +}; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + // x: entries + uvec4 counts; +}; + +shared uint sums[INSTANCE_OFFSETS_GROUP_SIZE]; + +void main() +{ + uint entries = counts.x; + uint invocation = gl_LocalInvocationIndex; + uint run = (entries + INSTANCE_OFFSETS_GROUP_SIZE - 1u) / INSTANCE_OFFSETS_GROUP_SIZE; + uint begin = min(invocation * run, entries); + uint end = min(begin + run, entries); + + uint sum = 0u; + for (uint entry = begin; entry < end; ++entry) + sum += counters[INSTANCE_COUNTER_HEADER + entry]; + sums[invocation] = sum; + memoryBarrierShared(); + barrier(); + + // inclusive scan of the sums + for (uint step = 1u; step < INSTANCE_OFFSETS_GROUP_SIZE; step *= 2u) + { + uint added = (invocation >= step ? sums[invocation - step] : 0u); + memoryBarrierShared(); + barrier(); + sums[invocation] += added; + memoryBarrierShared(); + barrier(); + } + + uint offset = sums[invocation] - sum; + for (uint entry = begin; entry < end; ++entry) + { + counters[INSTANCE_COUNTER_HEADER + entries + entry] = offset; + counters[INSTANCE_COUNTER_HEADER + 2u * entries + entry] = offset; + offset += counters[INSTANCE_COUNTER_HEADER + entry]; + } + if (invocation == INSTANCE_OFFSETS_GROUP_SIZE - 1u) + counters[1] = sums[invocation]; +} diff --git a/shaders/vk14/light_common.glsl b/shaders/vk14/light_common.glsl index 4db128e8..2ac0e89a 100644 --- a/shaders/vk14/light_common.glsl +++ b/shaders/vk14/light_common.glsl @@ -1,13 +1,21 @@ -// stage D of the vulkan renderer: the sun with its cascaded shadows, the environment map, the headlights and the lamps (in the -// clusters or not). the cube shadow maps of the lamps, the shadow maps of the headlights, the contact shadows, the shadows of -// the clouds, the directional sky ambient light and the prefiltered environment map of shaders/ogl46/light_common.glsl come -// with the later stages; the code below is the one of the opengl renderer with those left out, it shades as that one does -// with them off +// the shading of the scene, shaders/ogl46/light_common.glsl with the bindings of the pass set of the vulkan renderer +// (rendering/vulkan14/vulkan14bindings.h): the sun with its cascaded shadows and the shadows of the clouds, the environment map +// with its prefiltered levels, the headlights with their beams and shadow maps, the lamps (in the clusters or not) with their +// cube shadow maps, the contact shadows and the directional ambient light of the sky layout(set = PASS_SET, binding = SHADOW_TEX) uniform sampler2DArrayShadow shadowmap; layout(set = PASS_SET, binding = HEADLIGHT_TEX) uniform sampler2D headlightmap; +// cube shadow maps of the lamps, see opengl46_local_shadows +layout(set = PASS_SET, binding = LOCAL_SHADOW_TEX) uniform samplerCubeArrayShadow local_shadowmap; +// the same for the lamps of scenery models, the lights with linear 1 +layout(set = PASS_SET, binding = LOCAL_SHADOW_LAMPS_TEX) uniform samplerCubeArrayShadow local_shadowmap_lamps; +// shadow maps of the headlights of vehicles, see gl::scene_ubs::headlight_shadow +layout(set = PASS_SET, binding = HEADLIGHT_SHADOW_TEX) uniform sampler2DArrayShadow headlight_shadowmap; +// depth of the opaque scene drawn before the colour, for the contact shadows (gl::scene_ubs::contact_shadow_params) +layout(set = PASS_SET, binding = SCENE_DEPTH_TEX) uniform sampler2D scene_depth; #include "envmapping.glsl" #include "conversion.glsl" +#include "weather_common.glsl" float glossiness = 1.0; float metalic = 0.0; @@ -40,11 +48,80 @@ float length2(vec3 v) return dot(v, v); } -// share of the sun (the moon) held back before the fragment by the shadow map, 0..1 +// how many more lamps and headlights the fragment traces contact shadows to, see add_headlights() +int contact_shadow_budget = 0; + +// contact shadow of a light in direction Light_dir (view space, towards the light), 0..1: whether the depth of the scene drawn +// before the colour (scene_depth) holds a surface between the fragment and a point up to Length metres towards the light. it +// catches what the shadow maps are too coarse for (rails on the sleepers, bolts, edges of panels, where a thing stands on the +// ground), and things too small or near for them. only what is in the depth casts it, and only within the image. +// the ray starts a pixel and a half off the visible side of the surface: the depth there, sampled anywhere within a pixel of +// the projected point, then stays behind the ray on a flat surface, so it doesn't shadow itself. a surface nearer than the +// ray by more than the thickness is taken for something in front of the ray, not something it goes through. +// the shadow fades over the last part of the ray (it doesn't end with an edge) and over the last quarter of the range +float contact_shadow(vec3 light_dir, float raylength, float steps) +{ + float range = contact_shadow_params.y; + vec3 position = f_pos.xyz; + float fade = 1.0 - smoothstep(range * 0.75, range, length(position)); + if ((fade <= 0.0) || (raylength <= 0.0) || (steps < 1.0)) + return 0.0; + vec3 normal = normalize(f_normal); + if (dot(normal, position) > 0.0) + normal = -normal; + if (dot(normal, light_dir) <= 0.0) + return 0.0; + vec2 size = vec2(textureSize(scene_depth, 0)); + // the projection is perspective with the depth n / w (reverse z, infinite far plane): w is the distance along the axis + // of the view, and a depth d is a surface at n / d + float near = projection[3][2]; + float distance = (projection * vec4(position, 1.0)).w; + float pixel = 2.0 * distance / (abs(projection[1][1]) * size.y); + vec4 start = projection * vec4(position + normal * (pixel * 1.5), 1.0); + vec4 ray = projection * vec4(light_dir * raylength, 0.0); + float thickness = 0.3 + pixel * 4.0; + float bias = pixel * 0.5; + // a different start along the ray in neighbouring pixels: the steps give noise instead of bands + float jitter = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))); + for (float i = 0.0; i < steps; i += 1.0) + { + float t = (i + jitter) / steps; + vec4 clip = start + ray * t; + if (clip.w <= near) + break; + vec2 coords = (clip.xy / clip.w) * 0.5 + 0.5; + if (any(lessThan(coords, vec2(0.0))) || any(greaterThan(coords, vec2(1.0)))) + break; + float depth = texelFetch(scene_depth, min(ivec2(coords * size), ivec2(size) - 1), 0).r; + // nothing drawn there (the sky) + if (depth <= 0.0) + continue; + float nearer = clip.w - near / depth; + if ((nearer > bias) && (nearer < thickness)) + return fade * (1.0 - smoothstep(0.6, 1.0, t)); + } + return 0.0; +} + +// contact shadow of a lamp or headlight Distance metres away, whose light reaches the fragment with Strength, for the first +// few lamps of a fragment (contact_shadow_lamps.x) which light it noticeably. the ray goes at most half way to the light, +// so the lamp doesn't shadow with its own fitting +float contact_shadow_lamp(vec3 light_dir, float distance, float strength) +{ + if ((contact_shadow_budget <= 0) || (strength < 0.005)) + return 0.0; + --contact_shadow_budget; + return contact_shadow(light_dir, min(contact_shadow_params.x, distance * 0.5), contact_shadow_params.w); +} + +// share of the sun (the moon) held back before the fragment by the shadow map and the contact shadow, 0..1 float calc_shadow_map(); float calc_shadow() { - return calc_shadow_map(); + float shadow = calc_shadow_map(); + if ((contact_shadow_params.x > 0.0) && (shadow < 1.0)) + shadow = max(shadow, contact_shadow(normalize(-lights[0].dir), contact_shadow_params.x, contact_shadow_params.z)); + return shadow; } float calc_shadow_map() @@ -295,6 +372,52 @@ vec2 calc_dir_light(light_s light, vec3 fragnormal) return calc_light(light_dir, fragnormal); } +// samples of the soft shadows of the lamps and headlights: a disc, rotated per pixel +const vec2 SHADOW_DISC[8] = vec2[](vec2(-0.326, -0.406), vec2(-0.840, -0.074), vec2(-0.696, 0.457), vec2(-0.203, 0.621), + vec2(0.962, -0.195), vec2(0.473, -0.480), vec2(0.519, 0.767), vec2(0.185, -0.893)); + +float shadow_disc_angle() +{ + return 6.2831853 * fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))); +} + +// share of the beam of headlights with a shadow map (Map, headlight_weights.w - 1 of the light) held back before the fragment, +// 0..1. the map was drawn from the projector of the beam: x and y of the projection across it, z / w the depth (near plane over +// the distance along the axis, as the reverse z of the scene), w the distance. the comparison gives 1 where the map holds a +// value at least as large as the reference: a surface closer to the projector +float headlight_beam_shadow(uint map, vec3 fragnormal, vec3 light_dir) +{ + mat4 tomap = headlight_shadow[map]; + vec4 params = headlight_shadow_params[map]; + // a texel of the map is as large as this here. the point is moved along the normal by about a texel, and the reference towards + // the projector by a part of one. only the back faces are in the map, which leaves the lit faces little to shadow themselves with + float texel = params.x * max((tomap * vec4(f_pos.xyz, 1.0)).w, 0.0); + float NdotL = clamp(dot(fragnormal, light_dir), 0.0, 1.0); + vec4 position = tomap * vec4(f_pos.xyz + fragnormal * (texel * (0.5 + 1.5 * (1.0 - NdotL))), 1.0); + if (position.w <= 0.0) + return 0.0; + vec2 coords = position.xy / position.w; + if (any(lessThan(coords, vec2(0.0))) || any(greaterThan(coords, vec2(1.0)))) + return 0.0; + // nearer to the projector than its near plane (the vehicle itself) the reference is past anything in the map + float reference = position.z / max(position.w - texel * 1.5, 1e-3); + float layer = float(map); + float radius = params.y; + if (radius <= 0.0) + return texture(headlight_shadowmap, vec4(coords, layer, reference)); + float angle = shadow_disc_angle(); + vec2 rotation = vec2(cos(angle), sin(angle)); + float spread = radius / float(textureSize(headlight_shadowmap, 0).x); + int count = (radius > 1.5) ? 8 : 4; + float shadow = 0.0; + for (int i = 0; i < count; i++) + { + vec2 offset = vec2(SHADOW_DISC[i].x * rotation.x - SHADOW_DISC[i].y * rotation.y, SHADOW_DISC[i].x * rotation.y + SHADOW_DISC[i].y * rotation.x) * spread; + shadow += texture(headlight_shadowmap, vec4(coords + offset, layer, reference)); + } + return shadow / float(count); +} + // dpdx and dpdy are the change of the view space position of the fragment from a pixel to the next one. the beam texture is // sampled with gradients worked out from them instead of the ones of the hardware: those are undefined where the fragments of // a quad do not go through the same lights, which is what happens at the edges of the clusters @@ -327,6 +450,12 @@ vec2 calc_headlights(light_s light, vec3 fragnormal, vec3 dpdx, vec3 dpdy) vec2 dbeamdy = (dheadlightposdy.xy - beamcoords * dheadlightposdy.w) / headlightpos.w; vec3 lights = textureGrad(headlightmap, beamcoords, dbeamdx, dbeamdy).rgb * light.headlight_weights.rgb; float lightintensity = max(max(lights.r, lights.g), lights.b); + // headlights with a shadow map: what is in the way holds the beam back + if ((light.headlight_weights.w > 0.5) && (lightintensity > 0.0) && (part.x > 0.0)) + lightintensity *= 1.0 - headlight_beam_shadow(uint(light.headlight_weights.w - 0.5), fragnormal, light_dir); + if ((contact_shadow_budget > 0) && (lightintensity > 0.0) && (part.x > 0.0)) + lightintensity *= 1.0 - contact_shadow_lamp(light_dir, distance, + part.x * atten * lightintensity * light.intensity * max(light.color.r, max(light.color.g, light.color.b))); // x the diffuse light, y the light the specular reflection takes (specular_light() brings its own n.l) return vec2(part.x, 1.0) * (atten * lightintensity); } @@ -334,10 +463,59 @@ vec2 calc_headlights(light_s light, vec3 fragnormal, vec3 dpdx, vec3 dpdy) // closest distance (squared) the light of a lamp is computed for: nearer the inverse square would grow without a limit const float LOCAL_LIGHT_MIN_DISTANCE2 = 0.01; +// one comparison with the cube shadow map at the point of the space of the map, moved towards the lamp by Bias (metres). the +// map is in the array of the scenery lamps when Lamps, in the one of the cab lamp otherwise +float local_light_shadow_tap(vec3 position, float layer, float near, float bias, bool lamps) +{ + vec3 axes = abs(position); + float major = max(axes.x, max(axes.y, axes.z)); + float reference = near / max(major - bias, near); + return lamps ? texture(local_shadowmap_lamps, vec4(position, layer), reference) : texture(local_shadowmap, vec4(position, layer), reference); +} + +// share of the light of a lamp with a cube shadow map (headlight_weights.x is its layer) which reaches the fragment, 0..1. +// the map holds n / d of the closest surface, d the distance along the axis of the face, n the near plane: the farther, the +// smaller (as the depth of the scene, reverse z). the comparison gives 1 where the map holds a value at least as large as +// the reference, i.e. a surface at most as far: in shadow +float local_light_shadow(light_s light, vec3 fragnormal) +{ + // the space of the map: origin at the lamp, axes of the model the shadows are cast by + vec3 position = (light.headlight_projection * vec4(f_pos.xyz, 1.0)).xyz; + vec3 normal = normalize(mat3(light.headlight_projection) * fragnormal); + float distance = length(position); + // a texel of the map is as large as this here. the point is moved along the normal by about a texel, which keeps a surface + // from shadowing itself at grazing angles, and the reference is moved towards the lamp by a part of a texel + float texel = light.headlight_weights.z * distance; + vec3 tolamp = -position / max(distance, 1e-4); + float NdotL = clamp(dot(normal, tolamp), 0.0, 1.0); + position += normal * (texel * (0.5 + 1.5 * (1.0 - NdotL))); + float bias = texel * 1.5; + float layer = light.headlight_weights.x; + float radius = light.headlight_weights.w; + bool lamps = (light.linear > 0.5); + if (radius <= 0.0) + return local_light_shadow_tap(position, layer, light.headlight_weights.y, bias, lamps); + // a disc of samples in the plane of the surface, each compared with its own distance: points of the surface itself, which + // samples spread across the direction to the lamp would not be at grazing angles. rotated per pixel to turn the banding into noise + vec3 side = normalize(cross(normal, (abs(normal.y) < 0.99) ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0))); + vec3 up = cross(normal, side); + float angle = shadow_disc_angle(); + vec2 rotation = vec2(cos(angle), sin(angle)); + float spread = texel * radius; + int count = (radius > 1.5) ? 8 : 4; + float shadow = 0.0; + for (int i = 0; i < count; i++) + { + vec2 offset = vec2(SHADOW_DISC[i].x * rotation.x - SHADOW_DISC[i].y * rotation.y, SHADOW_DISC[i].x * rotation.y + SHADOW_DISC[i].y * rotation.x) * spread; + shadow += local_light_shadow_tap(position + side * offset.x + up * offset.y, layer, light.headlight_weights.y, bias, lamps); + } + return shadow / float(count); +} + // light of a lamp (point or spot light: the lamps of models and vehicles, the cab lamp) reaching the fragment, x the diffuse // part and y the light the specular reflection takes (specular_light() brings its own n.l). it falls off with the square of the distance, and smoothly to nothing at the range of the light, // 1 / sqrt(quadratic), past which the culling of the clusters does not take it. a spot light fades from its inner cone to the -// outer one +// outer one. a lamp with a shadow map is held back by what is between it and the fragment vec2 calc_local_light(light_s light, vec3 fragnormal) { vec3 tolight = light.pos - f_pos.xyz; @@ -347,6 +525,11 @@ vec2 calc_local_light(light_s light, vec3 fragnormal) float atten = edge * edge / max(distance2, LOCAL_LIGHT_MIN_DISTANCE2); if (light.type == LIGHT_SPOT) atten *= smoothstep(light.out_cutoff, light.in_cutoff, dot(-light_dir, normalize(light.dir))); + if ((atten > 0.0) && (light.headlight_weights.x >= 0.0) && (dot(fragnormal, light_dir) > 0.0)) + atten *= 1.0 - local_light_shadow(light, fragnormal); + if ((atten > 0.0) && (contact_shadow_budget > 0)) + atten *= 1.0 - contact_shadow_lamp(light_dir, sqrt(distance2), + atten * max(dot(fragnormal, light_dir), 0.0) * light.intensity * max(light.color.r, max(light.color.g, light.color.b))); if (atten <= 0.0) return vec2(0.0); return vec2(max(dot(fragnormal, light_dir), 0.0), 1.0) * atten; @@ -384,6 +567,7 @@ void add_local_lights(vec3 fragnormal, inout vec3 diffuse, inout vec3 specular) // worked out before the loops, which the fragments of a quad do not leave together vec3 dpdx = dFdx(f_pos.xyz); vec3 dpdy = dFdy(f_pos.xyz); + contact_shadow_budget = (contact_shadow_params.x > 0.0) ? int(contact_shadow_lamps.x) : 0; if (lights_clustered != 0U) { @@ -452,6 +636,31 @@ 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)); +} + // lights the surface: the ambient light, the sun (the moon), the headlights and lamps and the reflection of the surroundings. // the units are the ones of the ambient light, the irradiance over pi: a white lambertian surface lit by 1 of it shows 1. // Texturecolor is the albedo (the base colour of a metal), Specularity the specular level of a dielectric (0.5 is the @@ -471,6 +680,8 @@ vec3 shade_surface(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float ref } else { + // 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); #ifndef NO_WEATHERING float weathered = (param[2].w > 0.0 ? clamp(param[2].w * UserData.x, 0.0, 1.0) : 0.0); float tinted = (param[2].w < 0.0 ? clamp(-param[2].w * UserData.x, 0.0, 1.0) : 0.0); @@ -541,8 +752,8 @@ vec3 shade_surface(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float ref // panels, vehicle bodies and terrain reads as a clear edge rather // than a soft Lambertian ramp. Tunable via SUN_NDOTL_SHARPNESS. float sun_NdotL = pow(NdotL, SUN_NDOTL_SHARPNESS); - // the shadows of the clouds come with the weather (stage P2): nothing holds the sun back yet - float cloudsun = 1.0; + // the clouds hold back the sun (the moon at night) where their shadow falls + float cloudsun = cloud_shadow(mat3(inv_view) * f_pos.xyz); // Sun HDR scale -> SUN_DIFFUSE_SCALE, the same for the diffuse and the specular light vec3 sunlight = lights[0].color * (SUN_DIFFUSE_SCALE * lights[0].intensity * cloudsun); diff --git a/shaders/vk14/mat_terrain.frag b/shaders/vk14/mat_terrain.frag new file mode 100644 index 00000000..c5fbb7f0 --- /dev/null +++ b/shaders/vk14/mat_terrain.frag @@ -0,0 +1,4 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +#include "mat_terrain.glsl" diff --git a/shaders/vk14/mat_terrain.glsl b/shaders/vk14/mat_terrain.glsl new file mode 100644 index 00000000..88cd0c18 --- /dev/null +++ b/shaders/vk14/mat_terrain.glsl @@ -0,0 +1,158 @@ +layout(location = 0) in vec3 f_normal; +layout(location = 2) in vec2 f_coord; +layout(location = 3) in vec4 f_pos; +layout(location = 8) in mat3 f_tbn; + +layout(location = 4) in vec4 f_clip_pos; +layout(location = 5) in vec4 f_clip_future_pos; + +#include "common.glsl" + +layout(location = 0) out vec4 out_color; +#ifdef NO_MOTION_OUTPUT +// a pass without the velocity target: the writes go nowhere +vec4 out_motion; +#else +layout(location = 1) out vec4 out_motion; +#endif + +// heightmap terrain: up to 8 materials (layers) painted over a chunk, blended by their weights. +// the textures of all layers of the terrain are kept in texture arrays; the table of the chunk tells which layers it has: +// row 0, per layer: index in the arrays, 1 / metres the textures repeat at, has normal map, has specular/gloss map +// row 1, per layer: reflection, cosine and sine of the angle the textures are turned by +// f_coord holds metres from the corner of the pack of chunks, along x and -z + +#pragma param(color, 0, 0, 4, diffuse) +#pragma param(diffuse, 1, 0, 1, diffuse) +#pragma param(specular, 1, 1, 1, specular) +#pragma param(reflection, 1, 2, 1, one) +#pragma param(glossiness, 1, 3, 1, glossiness) +// corner of the chunk (x, z) in texture coordinates, 1 / size of the chunk, paint samples along a side less one +#pragma param(placement, 2, 0, 4, zero) + +#pragma texture(opaque, 0, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D opaque; +#pragma texture(weights0, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D weights0; +#pragma texture(weights1, 2, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D weights1; +#pragma texture(layerdiffuse, 3, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 3) uniform sampler2DArray layerdiffuse; +#pragma texture(layernormal, 4, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 4) uniform sampler2DArray layernormal; +#pragma texture(layerspecgloss, 5, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 5) uniform sampler2DArray layerspecgloss; +#pragma texture(layertable, 6, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 6) uniform sampler2D layertable; + +#define NORMALMAP +// param[2] holds the placement of the chunk, not the weathering +#define NO_WEATHERING +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +void main() +{ + // place within the chunk, 0..1 along x and z, and the paint samples around it + vec2 local = clamp(vec2(f_coord.x - param[2].x, -f_coord.y - param[2].y) * param[2].z, 0.0, 1.0); +#if TERRAIN_HOLES + // the openings cut in the ground (mat_terrain_holes.frag): the mask led between its samples, cut over the half. + // discarded at the end, the neighbours of the fragment still need it for their derivatives + float holeside = float(textureSize(holes, 0).x); + bool cut = (texture(holes, (local * (holeside - 1.0) + 0.5) / holeside).r > 0.5); +#endif + float samples = param[2].w; + vec2 paintcoord = (local * samples + 0.5) / (samples + 1.0); + vec4 w0 = texture(weights0, paintcoord); + vec4 w1 = texture(weights1, paintcoord); + float weights[8] = float[8](w0.r, w0.g, w0.b, w0.a, w1.r, w1.g, w1.b, w1.a); + float painted = dot(w0, vec4(1.0)) + dot(w1, vec4(1.0)); + + // the layers are sampled only where they're painted, so the gradients are taken here, where every pixel gets them + vec2 coorddx = dFdx(f_coord); + vec2 coorddy = dFdy(f_coord); + + vec3 albedo = vec3(0.0); + vec3 normal = vec3(0.0); + float reflectivity = 0.0; + float specularity = 0.0; + float gloss = 0.0; + float metal = 0.0; + float total = 0.0; + for (int i = 0; i < 8; ++i) + { + // a chunk without weights shows its first layer + float weight = (painted < 0.004 ? (i == 0 ? 1.0 : 0.0) : weights[i]); + if (weight < 0.004) + continue; + vec4 info = texelFetch(layertable, ivec2(i, 0), 0); + vec4 extra = texelFetch(layertable, ivec2(i, 1), 0); + // turned counterclockwise seen from above; f_coord runs along x and -z, so that's clockwise in it + vec2 turn = (extra.y == 0.0 && extra.z == 0.0) ? vec2(1.0, 0.0) : extra.yz; + mat2 rotation = mat2(turn.x, -turn.y, turn.y, turn.x); + vec3 coord = vec3(rotation * f_coord * info.y, info.x); + vec2 dx = rotation * coorddx * info.y; + vec2 dy = rotation * coorddy * info.y; + albedo += textureGrad(layerdiffuse, coord, dx, dy).rgb * weight; + vec3 layernormal_ts = vec3(0.0, 0.0, 1.0); + float layerreflection = extra.x; + if (info.z > 0.5) + { + vec4 normalmap = textureGrad(layernormal, coord, dx, dy); + layernormal_ts.xy = normalmap.rg * 2.0 - 1.0; + layernormal_ts.z = sqrt(1.0 - clamp(dot(layernormal_ts.xy, layernormal_ts.xy), 0.0, 1.0)); + // the normal map leans along the turned texture; turned back to the axes of the terrain + layernormal_ts.xy = layernormal_ts.xy * rotation; + layerreflection *= normalmap.a; + } + normal += layernormal_ts * weight; + if (info.w > 0.5) + { + vec4 specgloss = textureGrad(layerspecgloss, coord, dx, dy); + specularity += specgloss.r * weight; + gloss += specgloss.g * weight; + metal += specgloss.b * weight; + } + else + { + // a layer without a specgloss map: a dielectric of the glossiness: of the material (a blinn-phong exponent) + specularity += 0.5 * weight; + gloss += (1.0 - phong_roughness(abs(param[1].w))) * weight; + } + reflectivity += layerreflection * weight; + total += weight; + } + total = max(total, 0.004); + albedo /= total; + specularity /= total; + metal /= total; + reflectivity = param[1].z * reflectivity / total; + glossiness = (gloss / total) * abs(param[1].w); + metalic = metal; + + vec3 fragnormal = normalize(f_tbn * normalize(normal)); + vec3 fragcolor = ambient; + fragcolor = apply_lights(fragcolor, fragnormal, albedo, reflectivity, specularity, shadow_tone); + +#if TERRAIN_HOLES + if (cut) + discard; +#endif + vec4 color = vec4(apply_fog(fragcolor), alpha_mult); + if (POSTFX_ENABLED) + { + out_color = color; + } + else + { + out_color = tonemap(color); + } + if (MOTIONBLUR_ENABLED) + { + vec2 a = (f_clip_future_pos.xy / f_clip_future_pos.w) * 0.5 + 0.5; + vec2 b = (f_clip_pos.xy / f_clip_pos.w) * 0.5 + 0.5; + + out_motion = vec4(a - b, 0.0f, 0.0f); + } +} diff --git a/shaders/vk14/mat_terrain_holes.frag b/shaders/vk14/mat_terrain_holes.frag new file mode 100644 index 00000000..836513ac --- /dev/null +++ b/shaders/vk14/mat_terrain_holes.frag @@ -0,0 +1,12 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +// heightmap terrain with holes cut in it, for the portals of tunnels and the like (opengl46_terrain_materials): the cells cut +// out entirely are left out of the mesh, the rest of the openings is cut here. the mask of the holes covers the chunk, 255 +// where the ground is cut; the fragments where the value led between its samples is over the half are discarded +#define TERRAIN_HOLES 1 + +#pragma texture(holes, 7, R) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 7) uniform sampler2D holes; + +#include "mat_terrain.glsl" diff --git a/shaders/vk14/particle_common.glsl b/shaders/vk14/particle_common.glsl new file mode 100644 index 00000000..46ae94e0 --- /dev/null +++ b/shaders/vk14/particle_common.glsl @@ -0,0 +1,96 @@ +// storage of the particles of the gpu (gfx::gpu_particles, gl::particle_* in gl/ubo.h) for the compute passes; a kernel which +// draws defines PARTICLE_COMMAND_BINDING and PARTICLE_OUTPUT_BINDING, the bindings of its pushed set the command and the draws +// are at + +#ifndef PARTICLE_COMMON_GLSL +#define PARTICLE_COMMON_GLSL + +// a particle of smoke; it's dead (or its place was never used) when its opacity is 0 or less +struct smoke_particle_s +{ + vec4 position; // xyz: metres from the origin of the particles (world axes), w: rotation, radians + vec4 velocity; // xyz: m/s, w: size of the billboard, metres + vec4 state; // x: opacity, y: age, seconds +}; + +// a source of smoke as the last update of the simulation left it (smoke_source::gpu_state) +struct smoke_source_s +{ + uvec4 pool; // x: first place of the source in the storage, y: number of its places, z: place of the first particle born in the update, w: particles born + vec4 location; // xyz: emitter at the update, from the origin + vec4 previous; // xyz: emitter at the update before, from the origin + vec4 launch; // xy: inclination, radians, zw: speed, m/s + vec4 initial; // xy: size, zw: opacity + vec4 sizechange; // x: per second, y: min, z: max, w: per metre the particle goes through the air (spread:) + vec4 opacitychange; // x: per second, y: min, z: max + vec4 motion; // x: rise, y: damping (per second, of the vertical velocity), z: least vertical velocity + vec4 settling; // x: velocity the particles settle at, m/s upwards, y: drag, per second, z: 1 when they settle (in place of motion), w: seconds they take to show up (fade_in:) + vec4 color; // rgb: colour of the smoke, lit by the lights of the pass + vec4 oldcolor; // rgb: colour lit as the cpu does it, without the lights (gfx.smoke.lighting off) +}; + +// what a draw of a particle reads, as attributes of an instance +struct particle_draw_s +{ + vec4 place; // xyz: metres from the camera (world axes), w: size (smoke) or width (rain, snow) + vec4 shape; // x: rotation (smoke); xyz: from the head of the streak to its tail (rain, snow) + vec4 color; // rgb: lit colour, a: opacity +}; + +#ifdef PARTICLE_OUTPUT_BINDING +// the indirect draw command of the particles; the compute passes count the instances +layout(std430, set = COMPUTE_SET, binding = PARTICLE_COMMAND_BINDING) buffer particle_command_ssbo +{ + uint particle_draw_vertices; + uint particle_draw_instances; + uint particle_draw_first; + uint particle_draw_base; +}; + +layout(std430, set = COMPUTE_SET, binding = PARTICLE_OUTPUT_BINDING) writeonly buffer particle_output_ssbo +{ + particle_draw_s particle_draws[]; +}; +#endif + +// pcg hash +uint particle_hash(uint value) +{ + uint state = value * 747796405U + 2891336453U; + uint word = ((state >> ((state >> 28U) + 4U)) ^ state) * 277803737U; + return (word >> 22U) ^ word; +} + +// next of a sequence of random numbers 0-1 +float particle_random(inout uint state) +{ + state = particle_hash(state); + return float(state >> 8U) * (1.0 / 16777216.0); +} + +// whether a sphere (centre from the camera along the world axes) is within the planes of the view +bool particle_visible(vec4 planes[5], vec3 centre, float radius) +{ + for (int i = 0; i < 5; i++) + if (dot(planes[i].xyz, centre) + planes[i].w < -radius) + return false; + return true; +} + +#ifdef PARTICLE_OUTPUT_BINDING +// adds a particle to the draw; Capacity: the places of the storage of the draws +void particle_emit(vec3 place, float size, vec3 shape, vec3 color, float opacity, uint capacity) +{ + uint index = atomicAdd(particle_draw_instances, 1U); + if (index >= capacity) + { + atomicAdd(particle_draw_instances, 0xFFFFFFFFU); + return; + } + particle_draws[index].place = vec4(place, size); + particle_draws[index].shape = vec4(shape, 0.0); + particle_draws[index].color = vec4(color, opacity); +} +#endif + +#endif diff --git a/shaders/vk14/particle_light.glsl b/shaders/vk14/particle_light.glsl new file mode 100644 index 00000000..88a9d815 --- /dev/null +++ b/shaders/vk14/particle_light.glsl @@ -0,0 +1,147 @@ +// light of the particles of the gpu (gfx::gpu_particles: smoke, rain, snow) at a point of the view space of the pass, as a +// particle takes it as a whole, at its middle: the ambient light, the sun (the moon at night) held back by the shadow map and +// by the clouds, and the lamps and headlights of the light cluster the point is in (the first lights of lights[] without the +// clusters). each light is scattered towards the camera by the phase of the particle, Forward: 0 evenly, towards 1 mostly +// onwards. the scale is the one of the shading of the surfaces (light_common.glsl): a white particle takes as much light as +// a white face turned half way to it. for the compute passes; needs and + +#ifndef PARTICLE_LIGHT_GLSL +#define PARTICLE_LIGHT_GLSL + +layout(set = PASS_SET, binding = SHADOW_TEX) uniform sampler2DArrayShadow shadowmap; +layout(set = PASS_SET, binding = HEADLIGHT_TEX) uniform sampler2D headlightmap; +layout(set = PASS_SET, binding = HEADLIGHT_SHADOW_TEX) uniform sampler2DArrayShadow headlight_shadowmap; + +// AMBIENT_SCALE and SUN_DIFFUSE_SCALE of light_common.glsl +const float PARTICLE_AMBIENT_SCALE = 0.65; +const float PARTICLE_SUN_SCALE = 1.5; + +// Henyey-Greenstein, 1 for every angle when G is 0 +float particle_phase(float cosangle, float g) +{ + float g2 = g * g; + return (1.0 - g2) / pow(max(1.0 + g2 - 2.0 * g * cosangle, 1e-4), 1.5); +} + +// share of the sunlight the shadow map holds back at a point of the view space, one comparison (as fog_inject.comp) +float particle_sun_shadow(vec3 position) +{ + if (SHADOWMAP_ENABLED) + { + float span_distance = dot(position, position); + uint cascade; + for (cascade = 0U; cascade < MAX_CASCADES; cascade++) + if (span_distance <= cascade_end[cascade]) + break; + for (; cascade < MAX_CASCADES; cascade++) + { + vec4 lightpos = lightview[cascade] * vec4(position, 1.0); + vec3 coords = lightpos.xyz / lightpos.w; + if (all(greaterThanEqual(coords.xy, vec2(0.0))) && all(lessThanEqual(coords.xy, vec2(1.0)))) + { + if (coords.z < 0.0) + return 0.0; + return texture(shadowmap, vec4(coords.xy, float(cascade), coords.z + 0.0001 * float(cascade + 1U))); + } + // outside the map: lit, unless the cascades are fitted to the depth of the view (gfx.shadow.sdsm), whose maps cover only + // the area of the surfaces seen; then the first further cascade whose map holds the point + if (shadow_fit.x <= 0.0) + return 0.0; + } + return 0.0; + } + else + { + return 0.0; + } +} + +// light of a lamp or of the beam of headlights reaching a point of the view space (as fog_inject.comp; the beams with their +// shadow maps, the lamps without) +vec3 particle_lamp(light_s light, vec3 position) +{ + if (light.type == LIGHT_HEADLIGHTS) + { + vec4 projected = light.headlight_projection * vec4(position, 1.0); + vec3 coords = projected.xyz / projected.w; + if (coords.z < 0.0 || coords.z > 1.0 || any(lessThan(coords.xy, vec2(0.0))) || any(greaterThan(coords.xy, vec2(1.0)))) + return vec3(0.0); + float span_distance = length(light.pos - position); + float atten = 1.0 / (1.0 + light.linear * span_distance + light.quadratic * span_distance * span_distance); + atten *= mix(1.0, 0.0, clamp((coords.z - 0.998) * 500.0, 0.0, 1.0)); + vec3 beam = textureLod(headlightmap, coords.xy, 1.0).rgb * light.headlight_weights.rgb; + float intensity = max(max(beam.r, beam.g), beam.b); + if ((light.headlight_weights.w > 0.5) && (intensity > 0.0)) + { + uint map = uint(light.headlight_weights.w - 0.5); + vec4 shadowpos = headlight_shadow[map] * vec4(position, 1.0); + if (shadowpos.w > 0.0) + { + vec2 shadowcoords = shadowpos.xy / shadowpos.w; + if (all(greaterThanEqual(shadowcoords, vec2(0.0))) && all(lessThanEqual(shadowcoords, vec2(1.0)))) + { + float texel = headlight_shadow_params[map].x * shadowpos.w; + float reference = shadowpos.z / max(shadowpos.w - texel * 1.5, 1e-3); + intensity *= 1.0 - texture(headlight_shadowmap, vec4(shadowcoords, float(map), reference)); + } + } + } + return light.color * (atten * intensity * light.intensity); + } + vec3 tolight = light.pos - position; + float distance2 = dot(tolight, tolight); + float edge = clamp(1.0 - distance2 * distance2 * (light.quadratic * light.quadratic), 0.0, 1.0); + float atten = edge * edge / max(distance2, 0.01); + if (light.type == LIGHT_SPOT) + atten *= smoothstep(light.out_cutoff, light.in_cutoff, dot(-tolight * inversesqrt(max(distance2, 1e-8)), normalize(light.dir))); + return light.color * (atten * light.intensity); +} + +// the light cluster of a point of the view space in front of the camera, as fragment_cluster() of light_common.glsl finds it for +// the fragment drawn there +uint particle_cluster(vec3 position) +{ + vec4 clip = cluster_projection * vec4(position, 1.0); + vec2 screen = clip.xy / clip.w * 0.5 + 0.5; + ivec2 tile = clamp(ivec2(screen * vec2(CLUSTER_GRID_X, CLUSTER_GRID_Y)), ivec2(0), ivec2(CLUSTER_GRID_X - 1, CLUSTER_GRID_Y - 1)); + int slice = clamp(int(floor(cluster_slice_bias + log(max(clip.w, 1e-4)) * cluster_slice_scale)), 0, CLUSTER_GRID_Z - 1); + return uint(tile.x + CLUSTER_GRID_X * (tile.y + CLUSTER_GRID_Y * slice)); +} + +// Position: of the particle in the view space; World: its offset from the camera along the world axes +vec3 particle_light(vec3 position, vec3 world, float forward) +{ + vec3 tocamera = -normalize(position); + vec3 light = ambient * PARTICLE_AMBIENT_SCALE; + if (lights_count > 0U) + { + float sunlit = cloud_shadow(world) * (1.0 - particle_sun_shadow(position)); + if (sunlit > 0.0) + light += lights[0].color * (0.5 * PARTICLE_SUN_SCALE * lights[0].intensity * sunlit * particle_phase(dot(normalize(lights[0].dir), tocamera), forward)); + } + vec3 lamps = vec3(0.0); + if (lights_clustered != 0U) + { + if ((cluster_valid != 0U) && (position.z < 0.0)) + { + cluster_range_s range = cluster_ranges[particle_cluster(position)]; + for (uint i = 0U; i < range.count; i++) + { + light_s lamp = lights[cluster_light_indices[range.offset + i]]; + vec3 fromlamp = position - lamp.pos; + lamps += particle_lamp(lamp, position) * particle_phase(dot(fromlamp, tocamera) * inversesqrt(max(dot(fromlamp, fromlamp), 1e-8)), forward); + } + } + } + else + { + for (uint i = 1U; i < lights_count; i++) + { + vec3 fromlamp = position - lights[i].pos; + lamps += particle_lamp(lights[i], position) * particle_phase(dot(fromlamp, tocamera) * inversesqrt(max(dot(fromlamp, fromlamp), 1e-8)), forward); + } + } + return light + lamps * 0.5; +} + +#endif diff --git a/shaders/vk14/particle_soft.glsl b/shaders/vk14/particle_soft.glsl new file mode 100644 index 00000000..e332e601 --- /dev/null +++ b/shaders/vk14/particle_soft.glsl @@ -0,0 +1,24 @@ +// soft particles (gfx::gpu_particles): a particle fades where it nears what is behind it, instead of being cut by it. +// the resolved depth of the scene drawn so far (SCENE_DEPTH_TEX of the pass set), reversed with the far plane at infinity: the distance +// along the axis of the view is the near plane over the depth, and the sky, which writes no depth, stays at 0 + +#ifndef PARTICLE_SOFT_GLSL +#define PARTICLE_SOFT_GLSL + +layout(set = PASS_SET, binding = SCENE_DEPTH_TEX) uniform sampler2D particle_scene_depth; + +// share of the particle left at the fragment, 0-1: Range (metres) is how far in front of the scene it fades out over, 0 for none. +// Fragdistance: of the fragment along the axis of the view +float particle_soft_fade(float range, float fragdistance) +{ + if (range <= 0.0) + return 1.0; + float depth = texelFetch(particle_scene_depth, ivec2(gl_FragCoord.xy), 0).r; + if (depth <= 0.0) + return 1.0; + float scene = projection[3][2] / depth; + float share = clamp((scene - fragdistance) / range, 0.0, 1.0); + return share * share * (3.0 - 2.0 * share); +} + +#endif diff --git a/shaders/vk14/particles_precipitation.comp b/shaders/vk14/particles_precipitation.comp new file mode 100644 index 00000000..6935ae46 --- /dev/null +++ b/shaders/vk14/particles_precipitation.comp @@ -0,0 +1,84 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// rain and snow of the gpu (gfx::gpu_particles), in every colour pass which draws them: a thread per drop or flake. the +// particles fill three boxes around the camera, a small dense one and two larger ones, each repeated along the world axes: +// a particle keeps its place in the box, moved by how far the wind took them all and how far it fell, so nothing is kept +// from frame to frame. what the camera sees is lit (particle_light.glsl) and drawn as a streak from where it is to where +// it was a moment ago, seen from the moving camera (particles_precipitation.vert) + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" +#include "weather_common.glsl" +#include "particle_light.glsl" +#define PARTICLE_COMMAND_BINDING 0 +#define PARTICLE_OUTPUT_BINDING 1 +#include "particle_common.glsl" + +layout(local_size_x = PARTICLE_GROUP_SIZE) in; + +// the parameters of the kernel (a block of the ring of the frame: more of them than the push constants hold) +layout(std140, set = COMPUTE_SET, binding = 2) uniform kernel_block +{ + // x, y: first particles of the second and of the third box, z: particles, w: 1 for snow, 0 for rain + uvec4 counts; + // xyz: how far the wind took the particles less where the camera is, in each box (metres, 0 to the size of the box); w: how + // far they fell in it, 0 to 16 sizes of the box + vec4 offsets[3]; + // xyz: size of each box, metres; w unused + vec4 sizes; + // xyz: velocity of the wind less the one of the camera, m/s; w: speed of the fall + vec4 velocity; + // x: seconds (for the flutter of the flakes), y: how long a streak lasts, seconds, z: opacity, w: places for the draws + vec4 timing; + // the world axes around the camera to the view space of the pass + mat4 view; + // y: how much a particle scatters the light onwards, z: brightness + vec4 options; + vec4 planes[5]; +}; + +void main() +{ + uint index = gl_GlobalInvocationID.x; + if (index >= counts.z) + return; + uint box = (index < counts.x) ? 0U : (index < counts.y) ? 1U : 2U; + float size = sizes[box]; + bool snow = counts.w != 0U; + + uint state = particle_hash(index * 3U + 0x9E3779B9U); + vec3 home = vec3(particle_random(state), particle_random(state), particle_random(state)) * size; + // the speed of the fall is (16 + k) / 16 of the common one, so a whole number of sizes of the box (16 of them) of the common + // fall is a whole number of them for every particle, and the fall can be kept within 16 sizes + float pace = (16.0 + floor(particle_random(state) * 7.0) - 3.0) / 16.0; + float width = snow ? mix(0.004, 0.012, particle_random(state)) : mix(0.0012, 0.0025, particle_random(state)); + vec3 move = vec3(velocity.xyz) + vec3(0.0, -velocity.w * pace, 0.0); + vec3 place = home + offsets[box].xyz - vec3(0.0, offsets[box].w * pace, 0.0); + if (snow) + { + // the flakes flutter about their way + float amplitude = mix(0.1, 0.35, particle_random(state)); + vec2 frequency = mix(vec2(1.5), vec2(5.0), vec2(particle_random(state), particle_random(state))); + vec2 phase = vec2(particle_random(state), particle_random(state)) * 6.2831853; + vec2 angle = frequency * timing.x + phase; + place.xz += amplitude * sin(angle); + move.xz += amplitude * frequency * cos(angle); + } + // within the box around the camera + vec3 world = mod(place, size) - 0.5 * size; + float opacity = timing.z; + // fading at the walls of the box, where the particles wrap around, and right at the camera + vec3 walls = abs(world) / (0.5 * size); + opacity *= 1.0 - smoothstep(0.7, 1.0, max(max(walls.x, walls.y), walls.z)); + opacity *= smoothstep(0.15, 0.6, length(world)); + if (opacity <= 0.002) + return; + vec3 tail = -move * timing.y; + float streak = length(tail); + if (streak > 3.0) + tail *= 3.0 / streak; + if (false == particle_visible(planes, world + 0.5 * tail, 0.5 * length(tail) + width)) + return; + vec3 color = particle_light((view * vec4(world, 1.0)).xyz, world, options.y); + particle_emit(world, width, tail, color * options.z, opacity, uint(timing.w)); +} diff --git a/shaders/vk14/particles_precipitation.frag b/shaders/vk14/particles_precipitation.frag new file mode 100644 index 00000000..3f552f44 --- /dev/null +++ b/shaders/vk14/particles_precipitation.frag @@ -0,0 +1,54 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +// rain and snow of the gpu (gfx::gpu_particles, particles_precipitation.vert): a streak rounded at its ends, fainter towards +// its tail, in the colour the compute pass lit it with, faded in front of the scene, fogged + +layout(location = 3) in vec4 f_pos; +layout(location = 13) in vec4 f_color; +layout(location = 17) in vec4 f_streak; +layout(location = 16) in float f_soft; + +layout(location = 4) in vec4 f_clip_pos; +layout(location = 5) in vec4 f_clip_future_pos; + +#include "common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" +#include "particle_soft.glsl" + +layout(location = 0) out vec4 out_color; +#ifdef NO_MOTION_OUTPUT +// a pass without the velocity target: the writes go nowhere +vec4 out_motion; +#else +layout(location = 1) out vec4 out_motion; +#endif + +void main() +{ + // distance from the axis of the streak, in its radii + float along = clamp(f_streak.y, 0.0, f_streak.w); + float offaxis = length(vec2(f_streak.x, f_streak.y - along)) / max(f_streak.z, 1e-6); + float alpha = f_color.a * (1.0 - smoothstep(0.35, 1.0, offaxis)); + alpha *= mix(1.0, 0.4, along / max(f_streak.w, 1e-6)); + alpha *= particle_soft_fade(f_soft, -f_pos.z); + if (alpha <= 0.0) + discard; + vec4 color = vec4(apply_fog(f_color.rgb), alpha); + if (POSTFX_ENABLED) + { + out_color = color; + } + else + { + out_color = tonemap(color); + } + if (MOTIONBLUR_ENABLED) + { + vec2 a = (f_clip_future_pos.xy / f_clip_future_pos.w) * 0.5 + 0.5; + vec2 b = (f_clip_pos.xy / f_clip_pos.w) * 0.5 + 0.5; + + out_motion = vec4(a - b, 0.0f, alpha * alpha_mult); + } +} diff --git a/shaders/vk14/particles_precipitation.vert b/shaders/vk14/particles_precipitation.vert new file mode 100644 index 00000000..fe85537f --- /dev/null +++ b/shaders/vk14/particles_precipitation.vert @@ -0,0 +1,61 @@ +#version 460 core +#define STAGE_VERTEX 1 +#include "common.glsl" +// rain and snow of the gpu (gfx::gpu_particles): a streak per drop or flake the compute pass added to the draw, from where +// it is to where it was a moment ago as the moving camera saw it, turned to face the camera, rounded at its ends. a particle +// thinner than a pixel and a half is drawn that wide and as much fainter, so the distant ones don't flicker + +layout(location = 0) in vec4 v_place; // xyz: the head, metres from the camera (world axes), w: width +layout(location = 1) in vec4 v_shape; // xyz: from the head to the tail +layout(location = 2) in vec4 v_color; // rgb: lit colour, a: opacity + +layout(location = 3) out vec4 f_pos; +layout(location = 13) out vec4 f_color; +// x: across the streak, y: along it from the head, metres; z: its radius, w: its length +layout(location = 17) out vec4 f_streak; +layout(location = 16) out float f_soft; + +layout(location = 4) out vec4 f_clip_pos; +layout(location = 5) out vec4 f_clip_future_pos; + +#include "common.glsl" + +// x: metres it fades out over in front of the scene, 0: none +#define particle_soft pc.data[0] +// x: width of a pixel one metre in front of the camera, metres +#define particle_pixel pc.data[1] + +void main() +{ + vec3 head = (modelview * vec4(v_place.xyz, 1.0)).xyz; + vec3 axis = mat3(modelview) * v_shape.xyz; + float span = length(axis); + vec3 direction = (span > 1e-5) ? axis / span : vec3(0.0, -1.0, 0.0); + vec3 middle = head + 0.5 * axis; + + float width = v_place.w; + float alpha = v_color.a; + float least = 1.5 * particle_pixel.x * max(-middle.z, 1e-3); + if (width < least) + { + alpha *= width / least; + width = least; + } + float radius = 0.5 * width; + vec3 side = cross(direction, normalize(-middle)); + float sidelength = length(side); + side = (sidelength > 1e-4) ? side / sidelength : vec3(1.0, 0.0, 0.0); + + // a triangle strip of four corners: across, along + vec2 corner = vec2(float(gl_VertexIndex & 1), float(gl_VertexIndex >> 1)); + float across = (corner.x * 2.0 - 1.0) * radius; + float along = mix(-radius, span + radius, corner.y); + f_pos = vec4(head + direction * along + side * across, 1.0); + f_clip_pos = projection * f_pos; + f_clip_future_pos = (projection * future) * f_pos; + + gl_Position = f_clip_pos; + f_streak = vec4(across, along, radius, span); + f_color = vec4(v_color.rgb, alpha); + f_soft = particle_soft.x; +} diff --git a/shaders/vk14/particles_smoke.frag b/shaders/vk14/particles_smoke.frag new file mode 100644 index 00000000..5e019670 --- /dev/null +++ b/shaders/vk14/particles_smoke.frag @@ -0,0 +1,50 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +// the smoke of the gpu (gfx::gpu_particles, particles_smoke.vert): the texture of the smoke in the colour the compute pass +// lit the particle with, faded in front of the scene, fogged + +layout(location = 13) in vec4 f_color; +layout(location = 2) in vec2 f_coord; +layout(location = 3) in vec4 f_pos; +layout(location = 16) in float f_soft; + +layout(location = 4) in vec4 f_clip_pos; +layout(location = 5) in vec4 f_clip_future_pos; + +#pragma texture(tex1, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D tex1; + +#include "common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" +#include "particle_soft.glsl" + +layout(location = 0) out vec4 out_color; +#ifdef NO_MOTION_OUTPUT +// a pass without the velocity target: the writes go nowhere +vec4 out_motion; +#else +layout(location = 1) out vec4 out_motion; +#endif + +void main() +{ + vec4 tex_color = texture(tex1, f_coord) * f_color; + tex_color.a *= particle_soft_fade(f_soft, -f_pos.z); + if (POSTFX_ENABLED) + { + out_color = vec4(apply_fog(tex_color.rgb), tex_color.a); + } + else + { + out_color = tonemap(vec4(apply_fog(tex_color.rgb), tex_color.a)); + } + if (MOTIONBLUR_ENABLED) + { + vec2 a = (f_clip_future_pos.xy / f_clip_future_pos.w) * 0.5 + 0.5; + vec2 b = (f_clip_pos.xy / f_clip_pos.w) * 0.5 + 0.5; + + out_motion = vec4(a - b, 0.0f, tex_color.a * alpha_mult); + } +} diff --git a/shaders/vk14/particles_smoke.vert b/shaders/vk14/particles_smoke.vert new file mode 100644 index 00000000..b760e111 --- /dev/null +++ b/shaders/vk14/particles_smoke.vert @@ -0,0 +1,40 @@ +#version 460 core +#define STAGE_VERTEX 1 +#include "common.glsl" +// the smoke of the gpu (gfx::gpu_particles): a billboard per particle the compute pass added to the draw, turned about the +// axis of the view by the rotation of the particle (as opengl46_particles builds them on the cpu) + +layout(location = 0) in vec4 v_place; // xyz: metres from the camera (world axes), w: size +layout(location = 1) in vec4 v_shape; // x: rotation, radians +layout(location = 2) in vec4 v_color; // rgb: lit colour, a: opacity + +layout(location = 3) out vec4 f_pos; +layout(location = 13) out vec4 f_color; +layout(location = 2) out vec2 f_coord; +// metres in front of the scene the particle fades out over, see particle_soft.glsl +layout(location = 16) out float f_soft; + +layout(location = 4) out vec4 f_clip_pos; +layout(location = 5) out vec4 f_clip_future_pos; + +#include "common.glsl" + +// x: share of the size of the particle it fades out over in front of the scene (gfx.particles.soft), 0: none +#define particle_soft pc.data[0] + +void main() +{ + // a triangle strip of four corners + vec2 corner = vec2(float(gl_VertexIndex & 1), float(gl_VertexIndex >> 1)) - 0.5; + float c = cos(v_shape.x); + float s = sin(v_shape.x); + vec2 offset = vec2(c * corner.x - s * corner.y, s * corner.x + c * corner.y) * v_place.w; + f_pos = modelview * vec4(v_place.xyz, 1.0) + vec4(offset, 0.0, 0.0); + f_clip_pos = projection * f_pos; + f_clip_future_pos = (projection * future) * f_pos; + + gl_Position = f_clip_pos; + f_coord = corner + 0.5; + f_color = v_color; + f_soft = particle_soft.x * 0.5 * v_place.w; +} diff --git a/shaders/vk14/particles_smoke_prepare.comp b/shaders/vk14/particles_smoke_prepare.comp new file mode 100644 index 00000000..81c0d423 --- /dev/null +++ b/shaders/vk14/particles_smoke_prepare.comp @@ -0,0 +1,65 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the smoke of the gpu (gfx::gpu_particles), in every colour pass which draws it: a thread per place of the storage. the +// living particles the view sees are lit (particle_light.glsl) and added to the draw (particles_smoke.vert) + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" +#include "weather_common.glsl" +#include "particle_light.glsl" +#define PARTICLE_COMMAND_BINDING 3 +#define PARTICLE_OUTPUT_BINDING 4 +#include "particle_common.glsl" + +layout(local_size_x = PARTICLE_GROUP_SIZE) in; + +layout(std430, set = COMPUTE_SET, binding = 0) readonly buffer particle_ssbo +{ + smoke_particle_s particles[]; +}; +layout(std430, set = COMPUTE_SET, binding = 1) readonly buffer particle_source_ssbo +{ + smoke_source_s sources[]; +}; +layout(std430, set = COMPUTE_SET, binding = 2) readonly buffer particle_owner_ssbo +{ + uint owners[]; +}; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + uint places; + // xyz: the origin of the particles less the camera, metres along the world axes + vec3 origin; + // the world axes around the camera to the view space of the pass + mat4 view; + // x: 1 when the smoke is lit by the lights of the pass, y: how much it scatters the light onwards, z: places for the draws + vec4 options; + // planes of the view (world axes around the camera): a point is inside when dot(xyz, point) + w >= 0 + vec4 planes[5]; +}; + +void main() +{ + uint place = gl_GlobalInvocationID.x; + if (place >= places) + return; + smoke_particle_s particle = particles[place]; + if (particle.state.x <= 0.0) + return; + vec3 world = particle.position.xyz + origin; + float size = particle.velocity.w; + // the corners of the billboard turned anyhow + if (false == particle_visible(planes, world, size * 0.7072)) + return; + smoke_source_s source = sources[owners[place]]; + vec3 color = source.oldcolor.rgb; + if (options.x > 0.5) + color = source.color.rgb * particle_light((view * vec4(world, 1.0)).xyz, world, options.y); + float opacity = clamp(particle.state.x, 0.0, 1.0); + // showing up after the birth (smoke_source::fade_in()) + if (source.settling.w > 0.0) + opacity *= smoothstep(0.0, source.settling.w, particle.state.y); + particle_emit(world, size, vec3(particle.position.w, 0.0, 0.0), color, opacity, uint(options.z)); +} diff --git a/shaders/vk14/particles_smoke_simulate.comp b/shaders/vk14/particles_smoke_simulate.comp new file mode 100644 index 00000000..88c98c6e --- /dev/null +++ b/shaders/vk14/particles_smoke_simulate.comp @@ -0,0 +1,102 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the smoke of the gpu (gfx::gpu_particles), once per update of the simulation: a thread per place of the storage. the +// particles the sources had born in the update take the places after the ones of the last update (each source keeps its +// places as a ring, the oldest give way), spread along the way the emitter went since the update before; the living ones +// change as smoke_source::update() changes them on the cpu + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" +#include "particle_common.glsl" + +layout(local_size_x = PARTICLE_GROUP_SIZE) in; + +layout(std430, set = COMPUTE_SET, binding = 0) buffer particle_ssbo +{ + smoke_particle_s particles[]; +}; +layout(std430, set = COMPUTE_SET, binding = 1) readonly buffer particle_source_ssbo +{ + smoke_source_s sources[]; +}; +// the source of every place +layout(std430, set = COMPUTE_SET, binding = 2) readonly buffer particle_owner_ssbo +{ + uint owners[]; +}; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + uint places; + // x: seconds of the update, yzw: added to the positions, as the origin moved + vec4 step; + // xyz: the wind, m/s; w unused + vec4 wind; + // x: number of the update, for the random numbers + uint seed; +}; + +// smoke_source::update( Particle ); false when the particle is dead +bool advance(inout smoke_particle_s particle, smoke_source_s source, float seconds) +{ + particle.state.x = clamp(particle.state.x + source.opacitychange.x * seconds, source.opacitychange.y, source.opacitychange.z); + if (particle.state.x <= 0.0) + return false; + particle.velocity.w = clamp(particle.velocity.w + source.sizechange.x * seconds, source.sizechange.y, source.sizechange.z); + // the jet widens with the way it goes + if (source.sizechange.w > 0.0) + particle.velocity.w = min(particle.velocity.w + source.sizechange.w * length(particle.velocity.xyz) * seconds, source.sizechange.z); + if (source.settling.z > 0.5) + { + // the drag of the air takes the particle towards the velocity it settles at + particle.velocity.xyz += (vec3(0.0, source.settling.x, 0.0) - particle.velocity.xyz) * (1.0 - exp(-source.settling.y * seconds)); + } + else + { + // crude smoke dispersion + particle.velocity.y += source.motion.x * particle.velocity.y * seconds; + particle.velocity.y -= source.motion.y * particle.velocity.y * seconds; + particle.velocity.y = max(source.motion.z, particle.velocity.y); + } + particle.position.xyz += particle.velocity.xyz * seconds; + particle.position.xyz += 0.1 * particle.state.y * wind.xyz * seconds; + particle.state.y += seconds; + return true; +} + +void main() +{ + uint place = gl_GlobalInvocationID.x; + if (place >= places) + return; + smoke_source_s source = sources[owners[place]]; + smoke_particle_s particle = particles[place]; + particle.position.xyz += step.yzw; + + uint size = source.pool.y; + uint order = (place - source.pool.x + size - source.pool.z) % size; + if (order < source.pool.w) + { + // born in the update: smoke_source::particle_emitter::initialize() + uint state = particle_hash(place ^ particle_hash(seed)); + float polar = mix(source.launch.x, source.launch.y, particle_random(state)); + float azimuth = radians(mix(-180.0, 180.0, particle_random(state))); + vec3 direction = vec3(-sin(polar) * sin(azimuth), cos(polar), sin(polar) * cos(azimuth)); + particle.velocity.xyz = direction * mix(source.launch.z, source.launch.w, particle_random(state)); + particle.position.w = radians(mix(0.0, 360.0, particle_random(state))); + particle.velocity.w = mix(source.initial.x, source.initial.y, particle_random(state)); + particle.state = vec4(mix(source.initial.z, source.initial.w, particle_random(state)), 0.0, 0.0, 0.0); + // the births spread over the update: where the emitter was then, and the time since + float share = (float(order) + 0.5) / float(source.pool.w); + particle.position.xyz = mix(source.previous.xyz, source.location.xyz, share); + if (false == advance(particle, source, step.x * (1.0 - share))) + particle.state.x = 0.0; + } + else if (particle.state.x > 0.0) + { + if (false == advance(particle, source, step.x)) + particle.state.x = 0.0; + } + particles[place] = particle; +} diff --git a/shaders/vk14/postfx_bloom_down.frag b/shaders/vk14/postfx_bloom_down.frag new file mode 100644 index 00000000..0a03c820 --- /dev/null +++ b/shaders/vk14/postfx_bloom_down.frag @@ -0,0 +1,80 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#define LAYOUT_POST 1 +#include "common.glsl" +// 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 + +layout(location = 14) in vec2 f_coords; +layout(location = 0) out vec4 out_color; + +#pragma texture(source_tex, 0, RGB) +layout(set = POST_SET, binding = POST_TEXTURE_FIRST + 0) uniform sampler2D source_tex; + + +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/vk14/postfx_bloom_up.frag b/shaders/vk14/postfx_bloom_up.frag new file mode 100644 index 00000000..9e8b25b6 --- /dev/null +++ b/shaders/vk14/postfx_bloom_up.frag @@ -0,0 +1,33 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#define LAYOUT_POST 1 +#include "common.glsl" +// 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 + +layout(location = 14) in vec2 f_coords; +layout(location = 0) out vec4 out_color; + +#pragma texture(lower_tex, 0, RGB) +layout(set = POST_SET, binding = POST_TEXTURE_FIRST + 0) uniform sampler2D lower_tex; + +#pragma texture(current_tex, 1, RGB) +layout(set = POST_SET, binding = POST_TEXTURE_FIRST + 1) 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/vk14/postfx_ssao_apply.frag b/shaders/vk14/postfx_ssao_apply.frag index 12b3f48c..bbf26ebe 100644 --- a/shaders/vk14/postfx_ssao_apply.frag +++ b/shaders/vk14/postfx_ssao_apply.frag @@ -1,6 +1,5 @@ #version 460 core #define STAGE_FRAGMENT 1 -#define LAYOUT_POST 1 #include "common.glsl" // applies the ambient occlusion of the half size target to the image at full size, as a multiplier: drawn with the // blending dst * src over the image (opengl46_ao::apply). the four nearest pixels of the occlusion are weighed by how @@ -8,22 +7,21 @@ // edges of objects. each pixel of the occlusion stands for the top left one of the four full size pixels it covers // params.x: 0 ground truth ambient occlusion, 1 the older ssao (its own fixed curve) // params.y: strength, a power of the occlusion (1 = as computed); params.z: albedo assumed by the multiple bounce term. -// the fog in front of a surface hides its occlusion as it hides the surface: the analytic fog of apply_fog.glsl, the fog -// volume comes with the stage P2. -// the parameters are in the push constants (loose uniforms in the opengl renderer): pc.data[0..3] the columns of the inverse -// of the projection (inv_projection), pc.data[4] params, pc.data[5].x the density of the fog of the scene (fog_density of the -// regular draws, 0: no fog), pc.data[5].yzw unused +// the fog in front of a surface hides its occlusion as it hides the surface (fog_volume.glsl): drawn in the layout of the passes of +// the scene, with the pass set of the colour pass and the block of its weather. the parameters are in the push constants (loose +// uniforms in the opengl renderer): pc.data[0..3] the columns of the inverse of the projection, pc.data[4] params layout(location = 14) in vec2 f_coords; +#include "fog_volume.glsl" layout(location = 0) out vec4 out_color; -layout(set = POST_SET, binding = POST_TEXTURE_FIRST + 0) uniform sampler2D depth_tex; -layout(set = POST_SET, binding = POST_TEXTURE_FIRST + 1) uniform sampler2D ao_tex; +// the textures of the draw +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D depth_tex; +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D ao_tex; #define inv_projection mat4(pc.data[0], pc.data[1], pc.data[2], pc.data[3]) #define params pc.data[4] -#define scene_fog_density pc.data[5].x vec3 view_position(ivec2 pixel, float depth) { @@ -93,7 +91,6 @@ void main() ao = multi_bounce(ao, params.z); } // what the fog lets through of the surface is what is left to occlude - float transmittance = (scene_fog_density > 0.0) ? clamp(exp(-distance * scene_fog_density), 0.0, 1.0) : 1.0; - ao = mix(1.0, ao, transmittance); + ao = mix(1.0, ao, fog_along_view(view_position(pixel, depth), gl_FragCoord.xy).a); out_color = vec4(vec3(ao), 1.0); } diff --git a/shaders/vk14/postfx_tonemapping.frag b/shaders/vk14/postfx_tonemapping.frag index a77ad64e..deef2f56 100644 --- a/shaders/vk14/postfx_tonemapping.frag +++ b/shaders/vk14/postfx_tonemapping.frag @@ -13,16 +13,13 @@ layout(set = POST_SET, binding = POST_TEXTURE_FIRST + 0) uniform sampler2D tex1; #pragma texture(bloom_tex, 1, RGB) layout(set = POST_SET, binding = POST_TEXTURE_FIRST + 1) uniform sampler2D bloom_tex; -// the automatic exposure (exposure_ssbo of the opengl renderer, exposure_average.comp) comes in a later stage: the fixed -// exposure multiplier alone -const float exposure = 0.0; #include "tonemapping.glsl" #include "dither.glsl" // param is pc.data of the push constants (the model block in the opengl renderer). // 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 (there is none yet, so it has no effect), param[0].z: fixed exposure multiplier, param[0].w: scale of +// exposure is applied (exposure_ssbo of common.glsl, worked out by exposure_average.comp), param[0].z: fixed exposure multiplier, param[0].w: scale of // the bloom texture (it holds the sum of the levels of the chain), param[1].x: 1 when this is the last pass before the // window, which takes the dither void main() diff --git a/shaders/vk14/quad.vert b/shaders/vk14/quad.vert index 3501f409..d277ec03 100644 --- a/shaders/vk14/quad.vert +++ b/shaders/vk14/quad.vert @@ -1,6 +1,9 @@ #version 460 core #define STAGE_VERTEX 1 +// the full screen quad of the post-processing, or with QUAD_SCENE of a pass of the scene (the layout of the scene) +#ifndef QUAD_SCENE #define LAYOUT_POST 1 +#endif #include "common.glsl" const vec2 vert[4] = vec2[] ( diff --git a/shaders/vk14/shadow_fit_bounds.comp b/shaders/vk14/shadow_fit_bounds.comp new file mode 100644 index 00000000..e3832d03 --- /dev/null +++ b/shaders/vk14/shadow_fit_bounds.comp @@ -0,0 +1,133 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the cascades of the sun shadows fitted to the depth of the main view (gfx::shadow_fit), second pass: the distances +// from the nearest to the farthest pixel which isn't the sky (shadow_fit_depth.comp) split between the cascades, and for each +// cascade the box of the pixels it takes, in the space of the light. the cpu makes the maps of the cascades of a frame or +// two later cover the boxes: the camera has moved since, so a cascade takes the pixels a little past its distances as well, +// and the shading goes to the next cascade with a point outside the map of its own (light_common.glsl) + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" + +layout(local_size_x = SHADOW_FIT_GROUP_SIZE, local_size_y = SHADOW_FIT_GROUP_SIZE) in; + +layout(set = COMPUTE_SET, binding = 0) uniform sampler2D scene_depth; + +layout(std430, set = COMPUTE_SET, binding = 1) buffer shadow_fit_ssbo +{ + uint measurement; + uint samples; + // positive floats, as bits + uint depthmin; + uint depthmax; + float splits[4]; + uint counts[4]; + // per cascade the lowest x, y, z, unused, then the highest x, y, z, unused, as shadow_fit_order() gives them + uint bounds[MAX_CASCADES * 8U]; +}; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + // the inverse of the projection of the view + mat4 inv_projection; + // rotation from the view space to the space of the light: x and y across the maps, z along the light + mat4 view_to_light; + // x: the farthest distance the cascades may reach (the one of the fixed cascades), y: the nearest one the first may start + // from, z: the least one the last reaches, w: the share of equal ratios in the splits, the rest in equal lengths + vec4 split_params; + // how far past its distances a cascade takes the pixels, x + y * distance metres + vec4 margin_params; +}; + +shared uint shared_bounds[MAX_CASCADES * 8U]; +shared uint shared_counts[MAX_CASCADES]; + +// a float as a uint which orders the same way, also the negative ones +uint shadow_fit_order(float value) +{ + uint bits = floatBitsToUint(value); + return ((bits & 0x80000000U) != 0U) ? ~bits : (bits | 0x80000000U); +} + +// the distances the cascades go from and to. both ends are moved out to the next step of 1/8 of a power of two, so that +// the cascades change only now and then with the pixels coming and going +void shadow_fit_splits(out float result[MAX_CASCADES + 1U]) +{ + float rangemax = split_params.x; + float far = clamp(uintBitsToFloat(depthmax), split_params.z, rangemax); + far = min(exp2(ceil(log2(far) * 8.0) / 8.0), rangemax); + float near = clamp(uintBitsToFloat(depthmin), split_params.y, far * 0.25); + near = exp2(floor(log2(near) * 8.0) / 8.0); + result[0] = near; + result[MAX_CASCADES] = far; + for (uint cascade = 1U; cascade < MAX_CASCADES; ++cascade) + { + float share = float(cascade) / float(MAX_CASCADES); + result[cascade] = mix(near + (far - near) * share, near * pow(far / near, share), split_params.w); + } +} + +void main() +{ + uint index = gl_LocalInvocationIndex; + if (index < MAX_CASCADES * 8U) + shared_bounds[index] = ((index & 4U) == 0U) ? 0xFFFFFFFFU : 0U; + if (index < MAX_CASCADES) + shared_counts[index] = 0U; + barrier(); + + float cascadesplits[MAX_CASCADES + 1U]; + shadow_fit_splits(cascadesplits); + + ivec2 pixel = ivec2(gl_GlobalInvocationID.xy); + ivec2 size = textureSize(scene_depth, 0); + if ((samples > 0U) && all(lessThan(pixel, size))) + { + float depth = texelFetch(scene_depth, pixel, 0).r; + if (depth > 0.0) + { + vec2 ndc = (vec2(pixel) + 0.5) / vec2(size) * 2.0 - 1.0; + vec4 position = inv_projection * vec4(ndc, depth, 1.0); + vec3 view = position.xyz / position.w; + float distance = length(view); + vec3 light = mat3(view_to_light) * view; + uvec3 ordered = uvec3(shadow_fit_order(light.x), shadow_fit_order(light.y), shadow_fit_order(light.z)); + for (uint cascade = 0U; cascade < MAX_CASCADES; ++cascade) + { + // the end of a cascade has the padding of opengl46_renderer::Render_pass(), MAX_CASCADES - cascade metres + float low = (cascade == 0U) ? 0.0 : cascadesplits[cascade] - (margin_params.x + margin_params.y * cascadesplits[cascade]); + float high = cascadesplits[cascade + 1U] + float(MAX_CASCADES - cascade) + margin_params.x + margin_params.y * cascadesplits[cascade + 1U]; + if ((distance < low) || (distance > high)) + continue; + uint first = cascade * 8U; + atomicAdd(shared_counts[cascade], 1U); + atomicMin(shared_bounds[first + 0U], ordered.x); + atomicMin(shared_bounds[first + 1U], ordered.y); + atomicMin(shared_bounds[first + 2U], ordered.z); + atomicMax(shared_bounds[first + 4U], ordered.x); + atomicMax(shared_bounds[first + 5U], ordered.y); + atomicMax(shared_bounds[first + 6U], ordered.z); + } + } + } + barrier(); + + if ((index < MAX_CASCADES * 8U) && ((index & 3U) != 3U)) + { + if ((index & 4U) == 0U) + { + if (shared_bounds[index] != 0xFFFFFFFFU) + atomicMin(bounds[index], shared_bounds[index]); + } + else if (shared_bounds[index] != 0U) + atomicMax(bounds[index], shared_bounds[index]); + } + if ((index < MAX_CASCADES) && (shared_counts[index] > 0U)) + atomicAdd(counts[index], shared_counts[index]); + if (all(equal(gl_GlobalInvocationID.xy, uvec2(0U)))) + { + for (uint split = 0U; split <= MAX_CASCADES; ++split) + splits[split] = cascadesplits[split]; + } +} diff --git a/shaders/vk14/shadow_fit_depth.comp b/shaders/vk14/shadow_fit_depth.comp new file mode 100644 index 00000000..03a50ff1 --- /dev/null +++ b/shaders/vk14/shadow_fit_depth.comp @@ -0,0 +1,72 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the cascades of the sun shadows fitted to the depth of the main view (gfx::shadow_fit), first pass: how many pixels of +// the resolved depth of the view aren't the sky, and the nearest and the farthest of their distances from the camera. +// the depth is reversed with the far plane at infinity: the sky, which writes no depth, stays at 0. +// shadow_fit_bounds.comp splits the distances between the cascades + +#define LAYOUT_COMPUTE 1 +#include "common.glsl" + +layout(local_size_x = SHADOW_FIT_GROUP_SIZE, local_size_y = SHADOW_FIT_GROUP_SIZE) in; + +layout(set = COMPUTE_SET, binding = 0) uniform sampler2D scene_depth; + +layout(std430, set = COMPUTE_SET, binding = 1) buffer shadow_fit_ssbo +{ + uint measurement; + uint samples; + // positive floats, as bits + uint depthmin; + uint depthmax; + float splits[4]; + uint counts[4]; + uint bounds[MAX_CASCADES * 8U]; +}; + +// the parameters of the kernel (push constants) +layout(push_constant) uniform kernel_constants +{ + // the inverse of the projection of the view + mat4 inv_projection; +}; + +shared uint shared_samples; +shared uint shared_min; +shared uint shared_max; + +void main() +{ + if (gl_LocalInvocationIndex == 0U) + { + shared_samples = 0U; + // infinity + shared_min = 0x7F800000U; + shared_max = 0U; + } + barrier(); + + ivec2 pixel = ivec2(gl_GlobalInvocationID.xy); + ivec2 size = textureSize(scene_depth, 0); + if (all(lessThan(pixel, size))) + { + float depth = texelFetch(scene_depth, pixel, 0).r; + if (depth > 0.0) + { + vec2 ndc = (vec2(pixel) + 0.5) / vec2(size) * 2.0 - 1.0; + vec4 position = inv_projection * vec4(ndc, depth, 1.0); + uint distance = floatBitsToUint(length(position.xyz / position.w)); + atomicAdd(shared_samples, 1U); + atomicMin(shared_min, distance); + atomicMax(shared_max, distance); + } + } + barrier(); + + if ((gl_LocalInvocationIndex == 0U) && (shared_samples > 0U)) + { + atomicAdd(samples, shared_samples); + atomicMin(depthmin, shared_min); + atomicMax(depthmax, shared_max); + } +} diff --git a/shaders/vk14/shadowcube.vert b/shaders/vk14/shadowcube.vert new file mode 100644 index 00000000..30f6f27a --- /dev/null +++ b/shaders/vk14/shadowcube.vert @@ -0,0 +1,31 @@ +#version 460 core +#extension GL_EXT_multiview : require +#define STAGE_VERTEX 1 +// draws the geometry into the six faces of a cube shadow map of a lamp at once, a view of the multiview rendering per face (the +// geometry shader shadowcube.geom of the opengl renderer). the modelview takes the geometry to the space of the map, with the lamp at +// the origin. the faces go in the order of the faces of a cube map (+x, -x, +y, -y, +z, -z), each looks the way glm::lookAt() with +// the usual up vectors of cube maps sets it, with a 90 degree projection of infinite reach and reversed depth like the one of the +// scene: the depth is near / d, d the distance along the axis of the face. the near plane of the map, LOCAL_SHADOW_NEAR: closer +// surfaces (the lamp itself) cast no shadows +#include "common.glsl" +layout(location = 0) in vec3 v_vert; +layout(location = 2) in vec2 v_coord; + +layout(location = 2) out vec2 f_coord; + +// right, up and forward axes of the faces +const vec3 face_right[6] = vec3[](vec3(0.0, 0.0, -1.0), vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0), vec3(1.0, 0.0, 0.0), vec3(1.0, 0.0, 0.0), vec3(-1.0, 0.0, 0.0)); +const vec3 face_up[6] = vec3[](vec3(0.0, -1.0, 0.0), vec3(0.0, -1.0, 0.0), vec3(0.0, 0.0, 1.0), vec3(0.0, 0.0, -1.0), vec3(0.0, -1.0, 0.0), vec3(0.0, -1.0, 0.0)); +const vec3 face_forward[6] = vec3[](vec3(1.0, 0.0, 0.0), vec3(-1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), vec3(0.0, -1.0, 0.0), vec3(0.0, 0.0, 1.0), vec3(0.0, 0.0, -1.0)); + +void main() +{ + vec3 position = (effective_modelview() * vec4(v_vert, 1.0f)).xyz; + int face = int(gl_ViewIndex); + gl_Position = vec4(dot(position, face_right[face]), dot(position, face_up[face]), LOCAL_SHADOW_NEAR, dot(position, face_forward[face])); + f_coord = v_coord; +#ifdef MODEL_DRAWS + // which block of the multi draw the fragments read (common.glsl) + f_model_draw = MODEL_DRAW; +#endif +} diff --git a/shaders/vk14/simpleuv.vert b/shaders/vk14/simpleuv.vert index bd5eebe3..ec59d7da 100644 --- a/shaders/vk14/simpleuv.vert +++ b/shaders/vk14/simpleuv.vert @@ -11,4 +11,8 @@ void main() { gl_Position = (projection * effective_modelview()) * vec4(v_vert, 1.0f); f_coord = v_coord; +#ifdef MODEL_DRAWS + // which block of the multi draw the fragments read (common.glsl) + f_model_draw = MODEL_DRAW; +#endif } diff --git a/shaders/vk14/sky_atmosphere.frag b/shaders/vk14/sky_atmosphere.frag new file mode 100644 index 00000000..01db35f3 --- /dev/null +++ b/shaders/vk14/sky_atmosphere.frag @@ -0,0 +1,63 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +// 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 + +layout(location = 13) in vec3 f_color; +layout(location = 3) in vec4 f_pos; + +#include "atmosphere_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" +#include "sky_shoulder.glsl" + +layout(set = PASS_SET, binding = SKY_VIEW_TEX) uniform sampler2D sky_view_lut; + +layout(location = 0) out vec4 out_color; +#ifdef NO_MOTION_OUTPUT +// a pass without the velocity target: the writes go nowhere +vec4 out_motion; +#else +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)); + } + if (MOTIONBLUR_ENABLED) + { + out_motion = vec4(0.0f); + } +} diff --git a/shaders/vk14/sky_weather.frag b/shaders/vk14/sky_weather.frag new file mode 100644 index 00000000..18da77c4 --- /dev/null +++ b/shaders/vk14/sky_weather.frag @@ -0,0 +1,64 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +// laid over the sky after the dome, the stars, the sun and the moon (opengl46_weather::draw_sky): the clouds (clouds_march.comp) +// and the fog in front of them, which hides the sky and the clouds towards the horizon. blended with (1, source alpha): the colour is +// what the clouds and the fog add, the alpha what they let through of the sky behind. +// mode 0: no clouds, 1: the clouds of the main view (an image over the screen), 2: the panorama of the clouds around the camera + +layout(location = 14) in vec2 f_coords; + +#include "fog_volume.glsl" +#include "tonemapping.glsl" + +// the clouds of the view or the panorama, the first texture of the draw +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D clouds; + +// the clip space to the view space of the camera of the pass (pc.data[0-3]), the view space to world axes (the rotation of the +// block of the scene of the sky pass), the mode (pc.data[4].x) +#define inverse_projection mat4(pc.data[0], pc.data[1], pc.data[2], pc.data[3]) +#define view_to_world inv_view +#define mode int(pc.data[4].x) + +layout(location = 0) out vec4 out_color; +#ifdef NO_MOTION_OUTPUT +// a pass without the velocity target: the writes go nowhere +vec4 out_motion; +#else +layout(location = 1) out vec4 out_motion; +#endif + +// as in clouds_march.comp +const float PANORAMA_LOW = -0.1; + +void main() +{ + vec4 near = inverse_projection * vec4(f_coords * 2.0 - 1.0, 1.0, 1.0); + vec3 direction = normalize(mat3(view_to_world) * normalize(near.xyz / near.w)); + vec4 cloud = vec4(0.0, 0.0, 0.0, 1.0); + if (mode == 1) + cloud = textureLod(clouds, f_coords, 0.0); + else if (mode == 2) + { + float elevation = asin(clamp(direction.y, -1.0, 1.0)); + float v = sqrt(clamp((elevation - PANORAMA_LOW) / (0.5 * WEATHER_PI - PANORAMA_LOW), 0.0, 1.0)); + float u = atan(direction.x, direction.z) / (2.0 * WEATHER_PI); + cloud = textureLod(clouds, vec2(fract(u), v), 0.0); + } + // the sky and the clouds are far: the fog all the way, without the haze the atmosphere has already (the clouds got it) + vec4 fog = fog_sky(near.xyz / near.w, gl_FragCoord.xy); + vec3 color = fog.rgb + fog.a * cloud.rgb; + float through = fog.a * cloud.a; + if (POSTFX_ENABLED) + { + out_color = vec4(color, through); + } + else + { + out_color = vec4(tonemap(vec4(color, 1.0)).rgb, through); + } + if (MOTIONBLUR_ENABLED) + { + out_motion = vec4(0.0); + } +} diff --git a/shaders/vk14/stars.frag b/shaders/vk14/stars.frag new file mode 100644 index 00000000..6d3bb625 --- /dev/null +++ b/shaders/vk14/stars.frag @@ -0,0 +1,35 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +// a star: a soft point added to the sky + +layout(location = 13) in vec3 f_color; + +#include "tonemapping.glsl" + +layout(location = 0) out vec4 out_color; +#ifdef NO_MOTION_OUTPUT +// a pass without the velocity target: the writes go nowhere +vec4 out_motion; +#else +layout(location = 1) out vec4 out_motion; +#endif + +void main() +{ + vec2 offset = gl_PointCoord - 0.5; + float spot = exp(-dot(offset, offset) * 14.0); + vec3 color = f_color * spot; + if (POSTFX_ENABLED) + { + out_color = vec4(color, 0.0); + } + else + { + out_color = vec4(tonemap(vec4(color, 1.0)).rgb, 0.0); + } + if (MOTIONBLUR_ENABLED) + { + out_motion = vec4(0.0); + } +} diff --git a/shaders/vk14/stars.vert b/shaders/vk14/stars.vert new file mode 100644 index 00000000..11bd463e --- /dev/null +++ b/shaders/vk14/stars.vert @@ -0,0 +1,43 @@ +#version 460 core +#define STAGE_VERTEX 1 +#include "common.glsl" +// the stars of the catalogue (environment/starcatalog.h) as points: turned from the equatorial frame to the sky of the observer +// by the sidereal time and the latitude, dimmed through the air towards the horizon and twinkling there + +layout(location = 0) in vec4 v_star; // xyz: direction in the equatorial frame (z to the north pole), w: visual magnitude +layout(location = 1) in vec4 v_color; // rgb: colour of the star, w: a random phase of the twinkling + +// equatorial frame to world axes (x west, y up, z north), and world axes to the view: the columns of their rotations in +// pc.data[0-2] and pc.data[3-5] +#define equatorial_to_world mat3(pc.data[0].xyz, pc.data[1].xyz, pc.data[2].xyz) +#define world_to_view mat3(pc.data[3].xyz, pc.data[4].xyz, pc.data[5].xyz) +// x: brightness of a star of magnitude 0, y: seconds, z: size of the points in pixels (param[0] of the draw block) +#define params param[0] + +layout(location = 13) out vec3 f_color; + +void main() +{ + vec3 world = mat3(equatorial_to_world) * v_star.xyz; + float elevation = world.y; + if (elevation < -0.01) + { + // below the horizon: out of the view + gl_Position = vec4(2.0, 2.0, 2.0, 1.0); + f_color = vec3(0.0); + return; + } + // air mass after Kasten and Young, 0.25 magnitude of extinction per air mass + float altitude = max(degrees(asin(clamp(elevation, 0.0, 1.0))), 0.0); + float airmass = 1.0 / (max(elevation, 0.0) + 0.50572 * pow(altitude + 6.07995, -1.6364)); + float magnitude = v_star.w + 0.25 * airmass; + // low stars twinkle + float twinkle = 1.0 + 0.35 * clamp((airmass - 1.0) * 0.25, 0.0, 1.0) * sin(params.y * (7.0 + 5.0 * v_color.w) + v_color.w * 40.0); + float flux = pow(10.0, -0.4 * magnitude) * params.x * twinkle; + f_color = v_color.rgb * flux; + vec3 view = mat3(world_to_view) * world; + gl_Position = projection * vec4(view, 1.0); + // at the far end of the depth (reverse z) + gl_Position.z = 0.0; + gl_PointSize = params.z * clamp(1.0 + 0.4 * log(1.0 + flux / max(params.x * 0.01, 1e-6)), 1.0, 2.5); +} diff --git a/shaders/vk14/vertex.vert b/shaders/vk14/vertex.vert index 2119390e..ddf02369 100644 --- a/shaders/vk14/vertex.vert +++ b/shaders/vk14/vertex.vert @@ -52,4 +52,8 @@ void main() // TangentViewPos = TBN * vec3(0.0, 0.0, 0.0); TangentFragPos = TBN * f_pos.xyz; UserData = v_userdata; +#ifdef MODEL_DRAWS + // which block of the multi draw the fragments read (common.glsl) + f_model_draw = MODEL_DRAW; +#endif } diff --git a/shaders/vk14/weather_common.glsl b/shaders/vk14/weather_common.glsl new file mode 100644 index 00000000..a61583a2 --- /dev/null +++ b/shaders/vk14/weather_common.glsl @@ -0,0 +1,172 @@ +// weather of the vulkan renderer (the stage P2), shaders/ogl46/weather_common.glsl with the bindings of the pass set: the block +// of the weather of the pass (gl::weather_ubs without the colours of the horizon, which are a block of their own, written by +// fog_horizon.comp or by the cpu): fog, clouds and their shadows. +// world axes around the camera: the positions here are world space offsets from the camera, in metres, y up; heights are +// above the ground reference of the weather. the fog is a haze whose density falls exponentially with the height and a fog +// lying on the ground, both of a constant density below the reference (in the valleys), lit with the colour the atmosphere +// has at the horizon in the direction of the view (fog_horizon), less the part of the sunlight held back by shadows +#ifndef WEATHER_COMMON_GLSL +#define WEATHER_COMMON_GLSL + +struct cloud_layer_s +{ + // x: base, y: top (metres above the ground reference), z: 1 / (top - base), w: genus (environment/weather.h) + vec4 heights; + // x: coverage 0-1, y: extinction at density 1 (1/m), or the optical depth of a flat layer, z: 1 / (size of the shape noise + // tile), w: erosion by the detail noise + vec4 shape; + // x: share of the depth over which the density rises from the base, y: share from which it falls towards the top, + // z: variation of the height of the tops, w: spread of the top (anvil) + vec4 profile; + // xy: position of the camera in the shape noise (metres, world x and z, wrapped to the tile), z: 1 / (size of the coverage + // noise tile), w: stretch along the wind + vec4 motion; + // x: 1 for a flat layer, y: offset of the layer in the noise (seed), zw: position of the camera in the coverage noise + vec4 extra; +}; + +layout(std140, set = PASS_SET, binding = WEATHER_UBO) uniform weather_ubo +{ + // x: extinction of the haze at the ground reference (1/m), y: 1 / its scale height, z: extinction the sky is not fogged + // with (the atmosphere has that haze already), w: albedo of the fog + vec4 fog_haze; + // x: extinction of the ground fog (1/m), y: 1 / scale height of its top, z: height its density starts to fall at, w: patchiness + vec4 fog_ground; + // xyz: position of the camera in the noise of the fog (metres, moved by the wind), w: 1 / size of the patches + vec4 fog_noise; + // x: share of the light of the fog which comes from the sun (and is held back by shadows), y: transmittance of the clouds + // for the sun on average, z: how much the dense ground fog is whitened, w: fog density of a regular draw (fog_density + // of a draw over it scales the optical depth) + vec4 fog_light; + // x: 1 when the fog volume holds the fog of the pass, y: near, z: slices / log(far / near), w: slices + vec4 fog_volume_params; + // xy: 1 / size of the target of the pass (pixels), z: far end of the volume (view depth), w: share of the old sun glow + vec4 fog_volume_target; + // x: height of the camera above the ground reference, y: seconds of simulation, z: 1 when the clouds are drawn, w: layers + vec4 weather_camera; + // xyz: world space direction towards the key light (the sun, or the moon at night), w: 1 when it is above the horizon + vec4 weather_sun_direction; + // rgb: colour of the key light as the shading gets it, without the shadow of the clouds; w: scale of the sunlight of the clouds + vec4 weather_sun_color; + // rgb: ambient light of the sky above; w: scale of the sky light of the clouds + vec4 weather_ambient; + // xy: centre of the cloud shadow map (world x and z relative to the camera), z: 1 / its size (metres), w: 1 when it holds shadows + vec4 cloud_shadow_params; + // xy: world x and z of the direction the wind at the clouds blows to (unit), z: its speed, m/s, w: scale of the erosion of the + // edges of the clouds by the detail noise + vec4 cloud_wind; + cloud_layer_s cloud_layers[MAX_CLOUD_LAYERS]; +}; + +layout(std140, set = PASS_SET, binding = WEATHER_HORIZON_UBO) uniform weather_horizon_ubo +{ + // colour of the fog towards the horizon by the angle from the azimuth of the sun, 0 to 180 degrees, in the units of the + // scene. filled on the gpu from the sky-view table of the atmosphere + vec4 fog_horizon[FOG_HORIZON_SAMPLES]; +}; + +layout(set = PASS_SET, binding = CLOUD_SHADOW_TEX) uniform sampler2D cloud_shadow_map; + +const float WEATHER_PI = 3.14159265359; + +// integral of the density of an exponential layer from the reference up to Height, the density being constant below Shift and +// falling with Rate (1 / scale height) above it +float fog_column(float height, float extinction, float rate, float shift) +{ + float above = height - shift; + if (above <= 0.0) + return extinction * height; + return extinction * (shift + (1.0 - exp(-rate * above)) / rate); +} + +// optical depth of the fog along a ray from the height Origin (above the reference), direction with the vertical part Up, +// between the distances Near and Far, for the haze and the ground fog of the given extinctions at the reference: x of the +// haze, y of the ground fog +vec2 fog_optical_depth(float origin, float up, float near, float far, float haze, float ground) +{ + float span = far - near; + if (span <= 0.0) + return vec2(0.0); + float h0 = origin + up * near; + float h1 = origin + up * far; + if (abs(up) * span < 0.5) + { + // nearly level: the density in the middle + float h = 0.5 * (h0 + h1); + return vec2(haze * exp(-fog_haze.y * max(h, 0.0)), ground * exp(-fog_ground.y * max(h - fog_ground.z, 0.0))) * span; + } + float hazedepth = fog_column(h1, haze, fog_haze.y, 0.0) - fog_column(h0, haze, fog_haze.y, 0.0); + float grounddepth = fog_column(h1, ground, fog_ground.y, fog_ground.z) - fog_column(h0, ground, fog_ground.y, fog_ground.z); + return max(vec2(hazedepth, grounddepth) / up, vec2(0.0)); +} + +// light of the fog in a world space direction: the colour of the horizon the atmosphere has that way, the old sun glow where +// the old sky is used. the dense ground fog is whiter than the air +vec3 fog_color_world(vec3 direction) +{ + vec3 sun = weather_sun_direction.xyz; + // the light of the haze goes with the angle from the sun, not with the azimuth alone: the table is taken at the point of the + // horizon as far from the sun as the view is. level views get the colour of their azimuth as before; a view high up over the + // sun no longer gets the glow of the horizon under it (a column of light standing over a low sun) + float sunflat = length(sun.xz); + float cosangle = 1.0; + if (sunflat > 1e-4) + cosangle = clamp(dot(normalize(direction), sun) / sunflat, -1.0, 1.0); + float position = acos(clamp(cosangle, -1.0, 1.0)) / WEATHER_PI * float(FOG_HORIZON_SAMPLES - 1); + int index = int(floor(position)); + vec3 color = mix(fog_horizon[clamp(index, 0, FOG_HORIZON_SAMPLES - 1)].rgb, fog_horizon[clamp(index + 1, 0, FOG_HORIZON_SAMPLES - 1)].rgb, fract(position)); + if (fog_volume_target.w > 0.0) + color += weather_sun_color.rgb * pow(max(dot(direction, sun), 0.0), 30.0) * fog_volume_target.w; + return color; +} + +// the light the fog takes from the sun, 0-1: the share the shadows may hold back is scaled by Sunlit +float fog_light_factor(float sunlit) +{ + return (1.0 - fog_light.x) + fog_light.x * sunlit; +} + +// the colour of the fog whitened by the share Ground of the dense ground fog in it +vec3 fog_whiten(vec3 color, float ground) +{ + float luminance = dot(color, vec3(0.2126, 0.7152, 0.0722)); + return mix(color, vec3(luminance), clamp(ground, 0.0, 1.0) * fog_light.z); +} + +// fog along a ray of the view between two distances, of the haze and the ground fog of the given extinctions: rgb the light it +// adds, a what it lets through +vec4 fog_analytic_with(vec3 direction, float near, float far, float haze, float ground) +{ + vec2 depth = fog_optical_depth(weather_camera.x, direction.y, near, far, haze, ground); + float total = depth.x + depth.y; + float transmittance = exp(-total); + vec3 light = fog_whiten(fog_color_world(direction), depth.y / max(total, 1e-6)) * fog_light_factor(fog_light.y) * fog_haze.w; + return vec4(light * (1.0 - transmittance), transmittance); +} + +// fog along a ray of the view between two distances. Sky leaves out the haze the atmosphere draws in the sky already +vec4 fog_analytic(vec3 direction, float near, float far, bool sky) +{ + return fog_analytic_with(direction, near, far, sky ? max(fog_haze.x - fog_haze.z, 0.0) : fog_haze.x, fog_ground.x); +} + +// share of the key light the clouds let through to a point, World its offset from the camera +float cloud_shadow(vec3 world) +{ + if (cloud_shadow_params.w < 0.5) + return 1.0; + vec3 sun = weather_sun_direction.xyz; + if (sun.y < 0.02) + return fog_light.y; + // the map holds the shadow on the ground reference: down the ray of the sun to it + float height = weather_camera.x + world.y; + vec2 ground = world.xz - sun.xz * (height / sun.y); + vec2 coords = (ground - cloud_shadow_params.xy) * cloud_shadow_params.z + 0.5; + // towards the edges of the map the shadow fades into the average + vec2 edge = abs(coords - 0.5) * 2.0; + float fade = smoothstep(0.8, 1.0, max(edge.x, edge.y)); + float shadow = textureLod(cloud_shadow_map, coords, 0.0).r; + return mix(shadow, fog_light.y, fade); +} + +#endif diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index a5596974..f2c2ba67 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1769,6 +1769,19 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token Parser >> gfx_vulkan_framesinflight; gfx_vulkan_framesinflight = std::clamp(gfx_vulkan_framesinflight, 2, 3); } + else if (Token == "gfx.vulkan.renderthread") + { + Parser.getTokens(1); + Parser >> gfx_vulkan_renderthread; + } + else if (Token == "gfx.threads") + { + // 0: the cores less two + Parser.getTokens(1); + Parser >> gfx_threads; + gfx_threads = std::clamp(gfx_threads, 0, 64); + } + else if (ConfigParse_gfx_sky(Parser, Token)) { // parsed @@ -2190,6 +2203,8 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.vulkan.device", gfx_vulkan_device ); export_as_text( Output, "gfx.vulkan.validation", gfx_vulkan_validation ); export_as_text( Output, "gfx.vulkan.framesinflight", gfx_vulkan_framesinflight ); + export_as_text( Output, "gfx.vulkan.renderthread", gfx_vulkan_renderthread ); + export_as_text( Output, "gfx.threads", gfx_threads ); export_as_text( Output, "gfx.shadow.angle.min", gfx_shadow_angle_min ); export_as_text( Output, "gfx.shadow.rank.cutoff", gfx_shadow_rank_cutoff ); export_as_text( Output, "gfx.shadow.sdsm", gfx_shadow_sdsm ); diff --git a/utilities/Globals.h b/utilities/Globals.h index b8fecfbb..91820c8b 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -375,6 +375,11 @@ struct global_settings { int gfx_vulkan_validation { 0 }; // (vulkan) frames recorded while the gpu works on the earlier ones, 2 or 3 int gfx_vulkan_framesinflight { 2 }; + // (vulkan) the command buffers of the frame recorded, submitted and presented by a thread of its own while the simulation goes on + // with the next frame + bool gfx_vulkan_renderthread { true }; + // threads recording the command buffers of the vulkan renderer; 0: the cores less two, 1: one thread records them all + int gfx_threads { 0 }; bool vr = false; std::string vr_backend;