From 9b7d02e5fbe9a3127d0891bc71c5d609e24e9ca5 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Sat, 24 Oct 2026 20:10:24 +0200 Subject: [PATCH] =?UTF-8?q?Vulkan:=20scene=20rendering=20(D1=E2=80=93D7)?= =?UTF-8?q?=20on=20a=20scene=20walk=20shared=20with=20GL=20(N6a,=20N6b)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 6 +- ci_shadervalidator/main.cpp | 10 +- model/material.cpp | 6 +- .../locallights.cpp} | 28 +- .../locallights.h} | 17 +- rendering/common/scenewalk.cpp | 3062 +++++++++++++++ rendering/common/scenewalk.h | 346 ++ rendering/opengl46/opengl46localshadows.cpp | 8 +- rendering/opengl46/opengl46localshadows.h | 8 +- rendering/opengl46/opengl46renderer.cpp | 3434 ++--------------- rendering/opengl46/opengl46renderer.h | 238 +- rendering/vulkan14/vulkan14bindings.h | 58 +- rendering/vulkan14/vulkan14context.h | 322 ++ rendering/vulkan14/vulkan14descriptors.cpp | 7 +- rendering/vulkan14/vulkan14frame.cpp | 2839 ++++++++++++++ rendering/vulkan14/vulkan14geometry.cpp | 280 ++ rendering/vulkan14/vulkan14geometry.h | 99 + rendering/vulkan14/vulkan14memory.cpp | 12 +- rendering/vulkan14/vulkan14memory.h | 15 +- rendering/vulkan14/vulkan14renderer.cpp | 763 ++-- rendering/vulkan14/vulkan14renderer.h | 226 +- rendering/vulkan14/vulkan14scene.cpp | 1405 +++++++ rendering/vulkan14/vulkan14shaders.cpp | 77 + rendering/vulkan14/vulkan14shaders.h | 5 +- rendering/vulkan14/vulkan14texture.cpp | 90 + rendering/vulkan14/vulkan14texture.h | 12 +- shaders.7z | Bin 74299 -> 0 bytes shaders/README.md | 22 + shaders/compile_spirv.py | 17 +- shaders/vk14/alphashadowmap.frag | 13 + shaders/vk14/apply_fog.glsl | 40 + shaders/vk14/billboard.frag | 34 + shaders/vk14/celestial.frag | 93 + shaders/vk14/celestial.vert | 34 + shaders/vk14/cluster_build.comp | 59 + shaders/vk14/cluster_cull.comp | 157 + shaders/vk14/color.frag | 35 + shaders/vk14/common.glsl | 287 +- shaders/vk14/conversion.glsl | 108 + shaders/vk14/copy.frag | 15 + shaders/vk14/dither.glsl | 26 + shaders/vk14/envmapping.glsl | 54 + shaders/vk14/freespot.frag | 38 + shaders/vk14/freespot.vert | 21 + shaders/vk14/imgui.frag | 1 + shaders/vk14/imgui.vert | 1 + shaders/vk14/light_common.glsl | 594 +++ shaders/vk14/mat_colored.frag | 4 + shaders/vk14/mat_colored.glsl | 60 + shaders/vk14/mat_default.frag | 4 + shaders/vk14/mat_default.glsl | 69 + shaders/vk14/mat_default_0.frag | 4 + shaders/vk14/mat_default_1.frag | 4 + shaders/vk14/mat_default_2.frag | 4 + shaders/vk14/mat_default_detail.frag | 77 + shaders/vk14/mat_default_specgloss.frag | 77 + shaders/vk14/mat_detail_normalmap.frag | 89 + .../vk14/mat_detail_normalmap_specgloss.frag | 96 + shaders/vk14/mat_detail_parallax.frag | 152 + .../vk14/mat_detail_parallax_specgloss.frag | 158 + shaders/vk14/mat_invalid.frag | 29 + shaders/vk14/mat_normalmap.frag | 76 + shaders/vk14/mat_normalmap_specgloss.frag | 81 + shaders/vk14/mat_parallax.frag | 137 + shaders/vk14/mat_parallax_specgloss.frag | 142 + shaders/vk14/mat_rain_windscreen.frag | 216 ++ shaders/vk14/mat_reflmap.frag | 4 + shaders/vk14/mat_reflmap.glsl | 72 + shaders/vk14/mat_reflmap_specgloss.frag | 79 + shaders/vk14/mat_shadowlessnormalmap.frag | 75 + .../mat_shadowlessnormalmap_specgloss.frag | 80 + shaders/vk14/mat_stars.frag | 38 + shaders/vk14/mat_sunlessnormalmap.frag | 75 + .../vk14/mat_sunlessnormalmap_specgloss.frag | 80 + shaders/vk14/mat_trackblend.frag | 13 + shaders/vk14/mat_trackblend_normalmap.frag | 22 + shaders/vk14/mat_trackblend_parallax.frag | 25 + shaders/vk14/mat_trackblend_specgloss.frag | 21 + shaders/vk14/mat_water.frag | 98 + shaders/vk14/mat_water_specgloss.frag | 103 + shaders/vk14/pick.frag | 10 + shaders/vk14/pick_surface.frag | 11 + shaders/vk14/postfx_chromaticaberration.frag | 64 + shaders/vk14/postfx_motionblur.frag | 46 + shaders/vk14/postfx_ssao.frag | 78 + shaders/vk14/postfx_ssao_apply.frag | 99 + shaders/vk14/postfx_ssao_blur.frag | 18 + shaders/vk14/postfx_tonemapping.frag | 40 + shaders/vk14/precipitation.frag | 42 + shaders/vk14/precipitation.vert | 20 + shaders/vk14/present.frag | 17 + shaders/vk14/quad.vert | 27 + shaders/vk14/random.glsl | 39 + shaders/vk14/settings.glsl | 25 + shaders/vk14/shadowmap.frag | 8 + shaders/vk14/simpleuv.vert | 14 + shaders/vk14/sky_shoulder.glsl | 16 + shaders/vk14/smoke.frag | 43 + shaders/vk14/smoke.vert | 25 + shaders/vk14/tonemapping.glsl | 154 + shaders/vk14/trackblend.glsl | 205 + shaders/vk14/traction.frag | 39 + shaders/vk14/traction.vert | 20 + shaders/vk14/vbocolor.vert | 19 + shaders/vk14/vertex.vert | 55 + shaders/vk14/vertexonly.vert | 10 + world/Traction.h | 5 + 107 files changed, 14511 insertions(+), 3924 deletions(-) rename rendering/{opengl46/opengl46locallights.cpp => common/locallights.cpp} (92%) rename rendering/{opengl46/opengl46locallights.h => common/locallights.h} (94%) create mode 100644 rendering/common/scenewalk.cpp create mode 100644 rendering/common/scenewalk.h create mode 100644 rendering/vulkan14/vulkan14context.h create mode 100644 rendering/vulkan14/vulkan14frame.cpp create mode 100644 rendering/vulkan14/vulkan14geometry.cpp create mode 100644 rendering/vulkan14/vulkan14geometry.h create mode 100644 rendering/vulkan14/vulkan14scene.cpp delete mode 100644 shaders.7z create mode 100644 shaders/vk14/alphashadowmap.frag create mode 100644 shaders/vk14/apply_fog.glsl create mode 100644 shaders/vk14/billboard.frag create mode 100644 shaders/vk14/celestial.frag create mode 100644 shaders/vk14/celestial.vert create mode 100644 shaders/vk14/cluster_build.comp create mode 100644 shaders/vk14/cluster_cull.comp create mode 100644 shaders/vk14/color.frag create mode 100644 shaders/vk14/conversion.glsl create mode 100644 shaders/vk14/copy.frag create mode 100644 shaders/vk14/dither.glsl create mode 100644 shaders/vk14/envmapping.glsl create mode 100644 shaders/vk14/freespot.frag create mode 100644 shaders/vk14/freespot.vert create mode 100644 shaders/vk14/light_common.glsl create mode 100644 shaders/vk14/mat_colored.frag create mode 100644 shaders/vk14/mat_colored.glsl create mode 100644 shaders/vk14/mat_default.frag create mode 100644 shaders/vk14/mat_default.glsl create mode 100644 shaders/vk14/mat_default_0.frag create mode 100644 shaders/vk14/mat_default_1.frag create mode 100644 shaders/vk14/mat_default_2.frag create mode 100644 shaders/vk14/mat_default_detail.frag create mode 100644 shaders/vk14/mat_default_specgloss.frag create mode 100644 shaders/vk14/mat_detail_normalmap.frag create mode 100644 shaders/vk14/mat_detail_normalmap_specgloss.frag create mode 100644 shaders/vk14/mat_detail_parallax.frag create mode 100644 shaders/vk14/mat_detail_parallax_specgloss.frag create mode 100644 shaders/vk14/mat_invalid.frag create mode 100644 shaders/vk14/mat_normalmap.frag create mode 100644 shaders/vk14/mat_normalmap_specgloss.frag create mode 100644 shaders/vk14/mat_parallax.frag create mode 100644 shaders/vk14/mat_parallax_specgloss.frag create mode 100644 shaders/vk14/mat_rain_windscreen.frag create mode 100644 shaders/vk14/mat_reflmap.frag create mode 100644 shaders/vk14/mat_reflmap.glsl create mode 100644 shaders/vk14/mat_reflmap_specgloss.frag create mode 100644 shaders/vk14/mat_shadowlessnormalmap.frag create mode 100644 shaders/vk14/mat_shadowlessnormalmap_specgloss.frag create mode 100644 shaders/vk14/mat_stars.frag create mode 100644 shaders/vk14/mat_sunlessnormalmap.frag create mode 100644 shaders/vk14/mat_sunlessnormalmap_specgloss.frag create mode 100644 shaders/vk14/mat_trackblend.frag create mode 100644 shaders/vk14/mat_trackblend_normalmap.frag create mode 100644 shaders/vk14/mat_trackblend_parallax.frag create mode 100644 shaders/vk14/mat_trackblend_specgloss.frag create mode 100644 shaders/vk14/mat_water.frag create mode 100644 shaders/vk14/mat_water_specgloss.frag create mode 100644 shaders/vk14/pick.frag create mode 100644 shaders/vk14/pick_surface.frag create mode 100644 shaders/vk14/postfx_chromaticaberration.frag create mode 100644 shaders/vk14/postfx_motionblur.frag create mode 100644 shaders/vk14/postfx_ssao.frag create mode 100644 shaders/vk14/postfx_ssao_apply.frag create mode 100644 shaders/vk14/postfx_ssao_blur.frag create mode 100644 shaders/vk14/postfx_tonemapping.frag create mode 100644 shaders/vk14/precipitation.frag create mode 100644 shaders/vk14/precipitation.vert create mode 100644 shaders/vk14/present.frag create mode 100644 shaders/vk14/quad.vert create mode 100644 shaders/vk14/random.glsl create mode 100644 shaders/vk14/settings.glsl create mode 100644 shaders/vk14/shadowmap.frag create mode 100644 shaders/vk14/simpleuv.vert create mode 100644 shaders/vk14/sky_shoulder.glsl create mode 100644 shaders/vk14/smoke.frag create mode 100644 shaders/vk14/smoke.vert create mode 100644 shaders/vk14/tonemapping.glsl create mode 100644 shaders/vk14/trackblend.glsl create mode 100644 shaders/vk14/traction.frag create mode 100644 shaders/vk14/traction.vert create mode 100644 shaders/vk14/vbocolor.vert create mode 100644 shaders/vk14/vertex.vert create mode 100644 shaders/vk14/vertexonly.vert diff --git a/CMakeLists.txt b/CMakeLists.txt index 71fc460e..15919cc0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -309,6 +309,8 @@ set(SOURCES "gl/cubemap.cpp" "gl/glsl_common.cpp" "rendering/common/shadersource.cpp" +"rendering/common/scenewalk.cpp" +"rendering/common/locallights.cpp" "gl/buffer.cpp" "gl/fence.cpp" "gl/pbo.cpp" @@ -394,7 +396,6 @@ if (WITH_OPENGL_MODERN) "rendering/opengl46/opengl46weather.cpp" "rendering/opengl46/opengl46exposure.cpp" "rendering/opengl46/opengl46ao.cpp" - "rendering/opengl46/opengl46locallights.cpp" "rendering/opengl46/opengl46localshadows.cpp" "rendering/opengl46/opengl46instancecull.cpp" "rendering/opengl46/opengl46hiz.cpp" @@ -433,6 +434,9 @@ if (WITH_VULKAN) "rendering/vulkan14/vulkan14texture.cpp" "rendering/vulkan14/vulkan14imgui.cpp" "rendering/vulkan14/vulkan14renderer.cpp" + "rendering/vulkan14/vulkan14geometry.cpp" + "rendering/vulkan14/vulkan14scene.cpp" + "rendering/vulkan14/vulkan14frame.cpp" "ref/volk/volk.c") set(DEFINITIONS ${DEFINITIONS} "WITH_VULKAN") diff --git a/ci_shadervalidator/main.cpp b/ci_shadervalidator/main.cpp index 38a6553f..36eca132 100644 --- a/ci_shadervalidator/main.cpp +++ b/ci_shadervalidator/main.cpp @@ -116,7 +116,15 @@ bool check_ogl46(std::string const &Name) bool check_vk14(std::string const &Name) { std::cout << "spir-v..." << std::flush; - auto const suc{check_text(gfx::preprocess_shader(gfx::shader_api::vulkan14, Name, {}, {}, false, vk14_dir), "-V --target-env " + vulkan_env)}; + auto const source{gfx::preprocess_shader(gfx::shader_api::vulkan14, Name, {}, {}, false, vk14_dir)}; + auto suc{check_text(source, "-V --target-env " + vulkan_env)}; + // the variant of the passes without the velocity target, of the shaders which have one + if (source.text.find("NO_MOTION_OUTPUT") != std::string::npos) + { + std::cout << "done, no motion output..." << std::flush; + if (!check_text(gfx::preprocess_shader(gfx::shader_api::vulkan14, Name, "#define NO_MOTION_OUTPUT 1\n", {}, false, vk14_dir), "-V --target-env " + vulkan_env)) + suc = false; + } std::cout << "done" << std::flush; return suc; } diff --git a/model/material.cpp b/model/material.cpp index c93f77b4..71630eb1 100644 --- a/model/material.cpp +++ b/model/material.cpp @@ -94,7 +94,7 @@ int opengl_material::texture_slot( std::string const &Key ) return lookup->second; } // ignore unrecognized texture bindings in legacy render mode, it's most likely data for more advanced shaders - if( Global.GfxRenderer == "default" ) { + if( ( Global.GfxRenderer == "default" ) || ( Global.GfxRenderer == "vulkan" ) ) { log_error( "unknown texture binding: " + key ); is_good = false; } @@ -175,7 +175,7 @@ void opengl_material::finalize(bool Loadnow) } // TBD, TODO: move material validation to renderer, to eliminate branching? - if( Global.GfxRenderer == "default" ) { + if( ( Global.GfxRenderer == "default" ) || ( Global.GfxRenderer == "vulkan" ) ) { if( !shader ) { is_good = false; @@ -222,7 +222,7 @@ void opengl_material::finalize(bool Loadnow) parse_info.reset(); } - if( Global.GfxRenderer != "default" ) { + if( ( Global.GfxRenderer != "default" ) && ( Global.GfxRenderer != "vulkan" ) ) { // basic texture validation for legacy branch for( auto texturehandle : textures ) { if( texturehandle == null_handle ) { diff --git a/rendering/opengl46/opengl46locallights.cpp b/rendering/common/locallights.cpp similarity index 92% rename from rendering/opengl46/opengl46locallights.cpp rename to rendering/common/locallights.cpp index cf8c3015..beace1f1 100644 --- a/rendering/opengl46/opengl46locallights.cpp +++ b/rendering/common/locallights.cpp @@ -9,7 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" -#include "rendering/opengl46/opengl46locallights.h" +#include "rendering/common/locallights.h" #include #include @@ -107,7 +107,7 @@ void extend_bounds(TSubModel const *Submodel, glm::mat4 const &Transform, gfx::g } } // namespace -void opengl46_local_lights::gather(glm::dvec3 const &Camera, scene::basic_region &Region, gfx::geometrybank_manager const &Geometry, unsigned int const Framestamp) +void gfx::local_lights::gather(glm::dvec3 const &Camera, scene::basic_region &Region, gfx::geometrybank_manager const &Geometry, unsigned int const Framestamp) { auto const start{std::chrono::steady_clock::now()}; @@ -185,7 +185,7 @@ void opengl46_local_lights::gather(glm::dvec3 const &Camera, scene::basic_region m_gathertime = (m_gathertime == 0.0 ? elapsed : m_gathertime * 0.95 + elapsed * 0.05); } -void opengl46_local_lights::reset() +void gfx::local_lights::reset() { m_lights.clear(); m_cablampon = false; @@ -196,7 +196,7 @@ void opengl46_local_lights::reset() m_scenerycount = m_vehiclecount = m_compartmentcount = m_placed = m_dropped = 0; } -void opengl46_local_lights::gather_instance(TAnimModel *Instance, glm::dvec3 const &Camera, float const Brightness, unsigned int const Framestamp) +void gfx::local_lights::gather_instance(TAnimModel *Instance, glm::dvec3 const &Camera, float const Brightness, unsigned int const Framestamp) { if ((false == Instance->m_visible) || (Instance->pModel == nullptr)) { @@ -226,7 +226,7 @@ void opengl46_local_lights::gather_instance(TAnimModel *Instance, glm::dvec3 con m_scenerycount += static_cast(m_lights.size() - count); } -void opengl46_local_lights::gather_vehicle(TDynamicObject *Vehicle, glm::dvec3 const &Camera, gfx::geometrybank_manager const &Geometry, float const Freespotbrightness, +void gfx::local_lights::gather_vehicle(TDynamicObject *Vehicle, glm::dvec3 const &Camera, gfx::geometrybank_manager const &Geometry, float const Freespotbrightness, float const Compartmentbrightness) { if ((false == Global.render_cab) && (Global.pCamera.m_owner == Vehicle)) @@ -303,7 +303,7 @@ void opengl46_local_lights::gather_vehicle(TDynamicObject *Vehicle, glm::dvec3 c } } -void opengl46_local_lights::gather_cab_lamp(gfx::geometrybank_manager const &Geometry) +void gfx::local_lights::gather_cab_lamp(gfx::geometrybank_manager const &Geometry) { // the cab is drawn, Render_cab() conditions if (FreeFlyModeFlag || (false == Global.render_cab) || (simulation::Train == nullptr)) @@ -343,7 +343,7 @@ void opengl46_local_lights::gather_cab_lamp(gfx::geometrybank_manager const &Geo m_cablampon = (max_component(m_cablamp.color) > 0.f); } -void opengl46_local_lights::walk(TSubModel *Submodel, glm::mat4 const &Transform, glm::dmat4 const &Placement, float const Brightness) +void gfx::local_lights::walk(TSubModel *Submodel, glm::mat4 const &Transform, glm::dmat4 const &Placement, float const Brightness) { // visibility, lod ranges, transforms and animations as in opengl46_renderer::Render_Alpha(TSubModel *) for (auto *submodel{Submodel}; submodel != nullptr; submodel = submodel->Next) @@ -375,7 +375,7 @@ void opengl46_local_lights::walk(TSubModel *Submodel, glm::mat4 const &Transform } } -void opengl46_local_lights::add_light_point(TSubModel const &Submodel, glm::mat4 const &Transform, glm::dmat4 const &Placement, float const Brightness) +void gfx::local_lights::add_light_point(TSubModel const &Submodel, glm::mat4 const &Transform, glm::dmat4 const &Placement, float const Brightness) { // lit points: with self-illumination, past its threshold. the compartments of vehicles set it with their lights if ((Submodel.f4Emision.a <= 0.f) || (Global.fLuminance >= Submodel.fLight) || (Submodel.fVisible <= 0.f)) @@ -409,7 +409,7 @@ void opengl46_local_lights::add_light_point(TSubModel const &Submodel, glm::mat4 m_lights.emplace_back(lightpoint); } -bool opengl46_local_lights::has_light_points(TModel3d *Model) +bool gfx::local_lights::has_light_points(TModel3d *Model) { auto lookup{m_modellights.find(Model)}; if (lookup != m_modellights.end()) @@ -439,7 +439,7 @@ bool opengl46_local_lights::has_light_points(TModel3d *Model) return found; } -opengl46_local_lights::cell_data const &opengl46_local_lights::cell_instances(scene::basic_cell &Cell, bool const Rebuild) +gfx::local_lights::cell_data const &gfx::local_lights::cell_instances(scene::basic_cell &Cell, bool const Rebuild) { // light points are drawn in the translucent phase, so the instances which have them are on the translucent list of the cell auto &instances{Cell.m_instancetranslucent}; @@ -460,7 +460,7 @@ opengl46_local_lights::cell_data const &opengl46_local_lights::cell_instances(sc return data; } -opengl46_local_lights::section_data const &opengl46_local_lights::section(TModel3d *Model, TSubModel *Section, gfx::geometrybank_manager const &Geometry) +gfx::local_lights::section_data const &gfx::local_lights::section(TModel3d *Model, TSubModel *Section, gfx::geometrybank_manager const &Geometry) { auto lookup{m_sections.find(Section)}; if (lookup != m_sections.end()) @@ -483,7 +483,7 @@ opengl46_local_lights::section_data const &opengl46_local_lights::section(TModel return m_sections.emplace(Section, data).first->second; } -opengl46_local_lights::section_data const &opengl46_local_lights::model_bounds(TModel3d *Model, gfx::geometrybank_manager const &Geometry) +gfx::local_lights::section_data const &gfx::local_lights::model_bounds(TModel3d *Model, gfx::geometrybank_manager const &Geometry) { auto lookup{m_models.find(Model)}; if (lookup != m_models.end()) @@ -502,14 +502,14 @@ opengl46_local_lights::section_data const &opengl46_local_lights::model_bounds(T return m_models.emplace(Model, data).first->second; } -std::string opengl46_local_lights::info() const +std::string gfx::local_lights::info() const { return "local lights: " + std::to_string(m_lights.size()) + " (scenery " + std::to_string(m_scenerycount) + ", vehicles " + std::to_string(m_vehiclecount) + ", compartments " + std::to_string(m_compartmentcount) + ")" + (m_cablampon ? " + cab lamp" : "") + ", placed " + std::to_string(m_placed) + ", over capacity " + std::to_string(m_dropped) + ", gather " + to_string(m_gathertime, 2) + " ms"; } -void opengl46_local_lights::draw_debug_ui() +void gfx::local_lights::draw_debug_ui() { ImGui::TextUnformatted("local lights (need clustered lights)"); ImGui::Checkbox("light points of scenery models", &Global.gfx_lights_scenery); diff --git a/rendering/opengl46/opengl46locallights.h b/rendering/common/locallights.h similarity index 94% rename from rendering/opengl46/opengl46locallights.h rename to rendering/common/locallights.h index acb84dad..23ef958a 100644 --- a/rendering/opengl46/opengl46locallights.h +++ b/rendering/common/locallights.h @@ -28,15 +28,18 @@ class TSubModel; class TAnimModel; class TDynamicObject; -// lights of the opengl 4.6 renderer other than the sun and the vehicle headlights, which go to the light clusters: +// lights of the scene other than the sun and the vehicle headlights, which go to the light clusters of the renderers: // - the light points (freespot lights) of scenery models and of vehicles, taken the way the experimental renderer takes them: // a point which is visible, has self-illumination (emission) and is past its self-illumination threshold shines as a spot // light along the -z axis of its submodel, with the cone of its glare. lamps of scenery models switched with Light_On / Light_Off // and the self-illumination of vehicle compartments (TDynamicObject::Sections) work through the same state // - the lit compartments of vehicles (low poly interior sections) without light points of their own: a point light in the middle // - the lamp of the occupied cab: a point light under the top of the cab model, in place of the ambient light it used to add -// gathered once per frame around the camera, written to the light buffer by opengl46_renderer::Update_Lights() -class opengl46_local_lights +// gathered once per frame around the camera, written to the light buffers by the renderers (Update_Lights()) +namespace gfx +{ + +class local_lights { public: struct light @@ -53,9 +56,9 @@ class opengl46_local_lights void const *part{nullptr}; }; - opengl46_local_lights() = default; - opengl46_local_lights(const opengl46_local_lights &) = delete; - opengl46_local_lights &operator=(const opengl46_local_lights &) = delete; + local_lights() = default; + local_lights(const local_lights &) = delete; + local_lights &operator=(const local_lights &) = delete; // collects the lights within Global.gfx_lights_range of Camera, for the frame about to be drawn. it sets the state of the // shared models of the instances and vehicles it goes through, as the render does before it draws each of them. @@ -139,3 +142,5 @@ class opengl46_local_lights std::uint32_t m_dropped{0}; double m_gathertime{0.0}; // milliseconds, smoothed }; + +} // namespace gfx diff --git a/rendering/common/scenewalk.cpp b/rendering/common/scenewalk.cpp new file mode 100644 index 00000000..17865bb2 --- /dev/null +++ b/rendering/common/scenewalk.cpp @@ -0,0 +1,3062 @@ +/* +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 walk of the scene shared by the renderers (stage N6 of claude/vulkan14-renderer-plan.md): what is drawn in a pass, in +// which order, with which material and transform. the backends get the draws through the walk_* methods, the opengl renderer +// makes them at once, the vulkan renderer records them. moved out of opengl46renderer.cpp without changes to what it does + +#include "stdafx.h" +#include "rendering/common/scenewalk.h" +#include "utilities/color.h" +#include "utilities/Globals.h" +#include "vehicle/Train.h" +#include "vehicle/Camera.h" +#include "vehicle/DynObj.h" +#include "simulation/simulation.h" +#include "world/Track.h" +#include "world/Traction.h" +#include "world/MemCell.h" +#include "world/TrackSleepers.h" +#include "model/AnimModel.h" +#include "model/Model3d.h" + +namespace gfx +{ + +// the top view of the editor shows everything this close to the camera, the corners of the screen included +float scene_walk::editor_ortho_reach() +{ + auto const aspect{Global.window_size.x / std::max(1.f, static_cast(Global.window_size.y))}; + return Global.EditorOrthoExtent * std::sqrt(1.f + aspect * aspect) + 500.f; +} + +float scene_walk::editor_track_view_range(float const Range, bool const Apply) +{ + if (false == Apply || false == Global.editor_tracks) + return Range; + float extra = 50000.0f; + if (Global.EditorOrtho) + extra = editor_ortho_reach(); + return Range > extra ? Range : extra; +} + +// the top view of the editor may leave the ballast of the railway out, for the ground under the tracks to show +bool scene_walk::editor_ballast_hidden(TTrack const *Track, bool const Colour) +{ + return Colour && EditorModeFlag && Global.EditorOrtho && false == Global.EditorOrthoBallast && (Track->iCategoryFlag & 15) == 1; +} + +// the top view draws the scenery as far as the tracks; its models keep their own ranges +bool scene_walk::editor_far_tracks_only(bool const Apply, scene::basic_cell const *Cell, glm::dvec3 const &Camera, double const SceneryLimit) +{ + if (false == Apply || Global.EditorOrtho) + return false; + double nearest = glm::length(Cell->m_area.center - Camera) - static_cast(Cell->m_area.radius); + if (nearest < 0.0) + nearest = 0.0; + return nearest > SceneryLimit; +} + +material_handle scene_walk::submodel_material( TSubModel const *Submodel ) { + + if( Submodel->m_material >= 0 ) { + return Submodel->m_material; + } + return ( + TSubModel::ReplacableSkinId != nullptr ? + TSubModel::ReplacableSkinId[ -Submodel->m_material ] : + null_handle ); +} + +glm::vec3 scene_walk::pick_color(std::size_t const Index) +{ + return glm::vec3{((Index & 0xff0000) >> 16) / 255.0f, ((Index & 0x00ff00) >> 8) / 255.0f, (Index & 0x0000ff) / 255.0f}; +} + +std::size_t scene_walk::pick_index(glm::ivec3 const &Color) +{ + return Color.b + (Color.g * 256) + (Color.r * 256 * 256); +} + +bool scene_walk::Render_interior( bool const Alpha ) { + // TODO: early frustum based cull, camera might be pointing elsewhere + std::vector< std::pair > dynamics; + auto *dynamic { simulation::Train->Dynamic() }; + // draw interiors of the occupied vehicle, and all vehicles behind it with permanent coupling, in case they're open-ended + while( dynamic != nullptr ) { + + glm::dvec3 const originoffset { dynamic->vPosition - m_renderpass.pass_camera.position() }; + float const squaredistance{ glm::length2( glm::vec3{ originoffset } / Global.ZoomFactor ) }; + dynamics.emplace_back( squaredistance, dynamic ); + dynamic = dynamic->Next( coupling::permanent ); + } + // draw also interiors of permanently coupled vehicles in front, if there's any + dynamic = simulation::Train->Dynamic()->Prev( coupling::permanent ); + while( dynamic != nullptr ) { + + glm::dvec3 const originoffset { dynamic->vPosition - m_renderpass.pass_camera.position() }; + float const squaredistance{ glm::length2( glm::vec3{ originoffset } / Global.ZoomFactor ) }; + dynamics.emplace_back( squaredistance, dynamic ); + dynamic = dynamic->Prev( coupling::permanent ); + } + if( Alpha ) { + std::sort( + std::begin( dynamics ), std::end( dynamics ), + []( std::pair const &Left, std::pair const &Right ) { + return ( Left.first ) > ( Right.first ); } ); + } + for( auto &dynamic : dynamics ) { + Render_lowpoly( dynamic.second, dynamic.first, true, Alpha ); + } + + return true; +} + +bool scene_walk::Render_lowpoly( TDynamicObject *Dynamic, float const Squaredistance, bool const Setup, bool const Alpha ) { + + if( Dynamic->mdLowPolyInt == nullptr ) { return false; } + + // low poly interior + if( Setup ) { + + TSubModel::iInstance = reinterpret_cast( Dynamic ); //żeby nie robić cudzych animacji + TSubModel::iVariantSeed = Dynamic->variant_seed(); + glm::dvec3 const originoffset{ Dynamic->vPosition - m_renderpass.pass_camera.position() }; + + Dynamic->ABuLittleUpdate( Squaredistance ); // ustawianie zmiennych submodeli dla wspólnego modelu + + glm::mat4 mv = OpenGLMatrices.data( GL_MODELVIEW ); + + ::glPushMatrix(); + ::glTranslated( originoffset.x, originoffset.y, originoffset.z ); + ::glMultMatrixd( glm::value_ptr(Dynamic->mMatrix) ); + } + // HACK: reduce light level for vehicle interior if there's strong global lighting source + if( false == Alpha ) { +/* + auto const luminance { static_cast( std::min( + ( Dynamic->fShade > 0.0 ? Dynamic->fShade * Global.fLuminance : Global.fLuminance ), + Global.fLuminance - 0.5 * ( std::max( 0.3, Global.fLuminance - Global.Overcast ) ) ) ) }; +*/ + auto const luminance{ static_cast( + 0.5 + * ( std::max( + 0.3, + ( Global.fLuminance - Global.Overcast ) + * ( Dynamic->fShade > 0.0 ? + Dynamic->fShade : + 1.0 ) ) ) ) }; + walk_sunlight( + std::clamp( ( + Dynamic->fShade > 0.f ? + Dynamic->fShade : + 1.f ) + - luminance, + 0.f, 1.f ) ); + Render( Dynamic->mdLowPolyInt, Dynamic->Material(), Squaredistance ); + // HACK: if the model has low poly interior, we presume the load is placed inside and also affected by reduced light level + if( Dynamic->mdLoad ) { + // renderowanie nieprzezroczystego ładunku + Render( Dynamic->mdLoad, Dynamic->Material(), Squaredistance, { 0.f, Dynamic->LoadOffset, 0.f }, {} ); + } + walk_sunlight( Dynamic->fShade > 0.f ? Dynamic->fShade : 1.f ); + } + else { +// Render_Alpha( Dynamic->mdLowPolyInt, Dynamic->Material(), Squaredistance ); + // HACK: some models have windows included as part of the main model instead of lowpoly + if( Dynamic->mdModel ) { + // main model + Render_Alpha( Dynamic->mdModel, Dynamic->Material(), Squaredistance ); + } + } + + if( Setup ) { + + if( Dynamic->fShade > 0.0f ) { + // restore regular light level + walk_sunlight(); + } + + ::glPopMatrix(); + + // TODO: check if this reset is needed. In theory each object should render all parts based on its own instance data anyway? + if( Dynamic->btnOn ) { + Dynamic->TurnOff(); // przywrócenie domyślnych pozycji submodeli + } + } + + return true; +} + +bool scene_walk::Render_coupler_adapter( TDynamicObject *Dynamic, float const Squaredistance, int const End, bool const Alpha ) { + + if( Dynamic->m_coupleradapters[ End ] == nullptr ) { return false; } + + auto const position { glm::dvec3 { + 0.f, + Dynamic->MoverParameters->Couplers[ End ].adapter_height, + ( Dynamic->MoverParameters->Couplers[ End ].adapter_length + Dynamic->MoverParameters->Dim.L * 0.5 ) * ( End == end::front ? 1 : -1 ) } }; + + auto const angle { glm::vec3{ + 0, + ( End == end::front ? 0 : 180 ), + 0 } }; + + if( Alpha ) { + Render_Alpha( Dynamic->m_coupleradapters[ End ], Dynamic->Material(), Squaredistance, position, angle ); + } + else { + Render( Dynamic->m_coupleradapters[ End ], Dynamic->Material(), Squaredistance, position, angle ); + } + + return true; +} + +void scene_walk::Render(scene::basic_region *Region) +{ + + m_sectionqueue.clear(); + m_cellqueue.clear(); + // discard last pass's accumulated instance buckets before this pass starts. + // the vectors themselves are kept, so their allocations get reused by the following passes + for( auto &bucket : m_frame_instance_buckets ) { + bucket.second.clear(); + } + // the backend gets ready for the pass (the gpu culling of the opengl renderer) + walk_region_begin(); + m_modelcpudistances.clear(); + // build a list of region sections to render + glm::vec3 const cameraposition{m_renderpass.pass_camera.position()}; + auto const camerax = static_cast(std::floor(cameraposition.x / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2)); + auto const cameraz = static_cast(std::floor(cameraposition.z / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2)); + float section_range = m_renderpass.draw_range * Global.fDistanceFactor; + section_range = editor_track_view_range(section_range, EditorModeFlag && (m_renderpass.draw_mode == rendermode::color || m_renderpass.draw_mode == rendermode::pickscenery)); + int const segmentcount = 2 * static_cast(std::ceil(section_range / scene::EU07_SECTIONSIZE)); + int const originx = camerax - segmentcount / 2; + int const originz = cameraz - segmentcount / 2; + + for (int row = originz; row <= originz + segmentcount; ++row) + { + if (row < 0) + { + continue; + } + if (row >= scene::EU07_REGIONSIDESECTIONCOUNT) + { + break; + } + for (int column = originx; column <= originx + segmentcount; ++column) + { + if (column < 0) + { + continue; + } + if (column >= scene::EU07_REGIONSIDESECTIONCOUNT) + { + break; + } + auto *section{Region->m_sections[row * scene::EU07_REGIONSIDESECTIONCOUNT + column]}; + if ((section != nullptr) && (m_renderpass.pass_camera.visible(section->m_area))) + { + m_sectionqueue.emplace_back(section); + } + } + } + + switch (m_renderpass.draw_mode) + { + case rendermode::color: + { + Render(std::begin(m_sectionqueue), std::end(m_sectionqueue)); + // draw queue is filled while rendering sections + if (EditorModeFlag) + { + // when editor mode is active calculate world position of the cursor + // at this stage the z-buffer is filled with only ground geometry + walk_editor_cursor(); + } + // draw opaque cells front-to-back: with the depth test enabled this + // lets the GPU reject hidden fragments early, before the (expensive) + // lit fragment shader runs on them. Order is irrelevant to the final + // image for opaque geometry, so this is purely a fill-rate win. + std::sort( std::begin( m_cellqueue ), std::end( m_cellqueue ), + []( distancecell_pair const &Left, distancecell_pair const &Right ) { return Left.first < Right.first; } ); + Render(std::begin(m_cellqueue), std::end(m_cellqueue)); + break; + } + case rendermode::shadows: + case rendermode::pickscenery: + { + // these render modes don't bother with lights + Render(std::begin(m_sectionqueue), std::end(m_sectionqueue)); + // they can also skip queue sorting, as they only deal with opaque geometry + // NOTE: there's benefit from rendering front-to-back, but is it significant enough? TODO: investigate + Render(std::begin(m_cellqueue), std::end(m_cellqueue)); + break; + } + case rendermode::reflections: + { + Render(std::begin(m_sectionqueue), std::end(m_sectionqueue)); + if( Global.reflectiontune.fidelity >= 1 ) { + Render(std::begin(m_cellqueue), std::end(m_cellqueue)); + } + break; + } + case rendermode::pickcontrols: + default: + { + // no need to render anything ourside of the cab in control picking mode + break; + } + } +} + +void scene_walk::Render(section_sequence::iterator First, section_sequence::iterator Last) +{ + + switch (m_renderpass.draw_mode) + { + case rendermode::color: + case rendermode::reflections: + case rendermode::shadows: + break; + case rendermode::pickscenery: + { + // non-interactive scenery elements get neutral colour + model_ubs.param[0] = colors::none; + break; + } + default: + break; + } + + while (First != Last) + { + + auto *section = *First; + section->create_geometry(); + + // render shapes held by the section + switch (m_renderpass.draw_mode) + { + case rendermode::color: + case rendermode::reflections: + case rendermode::shadows: + case rendermode::pickscenery: + { + if (false == section->m_shapes.empty()) + { + // since all shapes of the section share center point we can optimize out a few calls here + ::glPushMatrix(); + auto const originoffset{section->m_area.center - m_renderpass.pass_camera.position()}; + ::glTranslated(originoffset.x, originoffset.y, originoffset.z); + // render + for (auto const &shape : section->m_shapes) + { + Render(shape, true); + } + // post-render cleanup + ::glPopMatrix(); + } + break; + } + case rendermode::pickcontrols: + default: + { + break; + } + } + + // add the section's cells to the cell queue + switch (m_renderpass.draw_mode) + { + case rendermode::color: + case rendermode::shadows: + case rendermode::pickscenery: + { + // TBD, TODO: refactor into a method to reuse below? + for (auto &cell : section->m_cells) + { + if ((true == cell.m_active) && (m_renderpass.pass_camera.visible(cell.m_area))) + { + // store visible cells with content as well as their current distance, for sorting later + m_cellqueue.emplace_back(glm::length2(m_renderpass.pass_camera.position() - cell.m_area.center), &cell); + } + } + break; + } + case rendermode::reflections: + { + // we can skip filling the cell queue if reflections pass isn't going to use it + if( Global.reflectiontune.fidelity == 0 ) { break; } + for (auto &cell : section->m_cells) + { + if ((true == cell.m_active) && (m_renderpass.pass_camera.visible(cell.m_area))) + { + // store visible cells with content as well as their current distance, for sorting later + m_cellqueue.emplace_back(glm::length2(m_renderpass.pass_camera.position() - cell.m_area.center), &cell); + } + } + break; + } + case rendermode::pickcontrols: + default: + { + break; + } + } + // proceed to next section + ++First; + } + + switch (m_renderpass.draw_mode) + { + case rendermode::shadows: + { + break; + } + default: + { + break; + } + } +} + +void scene_walk::Render(cell_sequence::iterator First, cell_sequence::iterator Last) +{ + + // cache initial iterator for the second sweep + auto first{First}; + auto const scenery_limit{static_cast(Global.BaseDrawRange) * Global.fDistanceFactor + 250.0}; + bool const far_paths{EditorModeFlag && Global.editor_tracks && (m_renderpass.draw_mode == rendermode::color || m_renderpass.draw_mode == rendermode::pickscenery)}; + glm::dvec3 const camera{m_renderpass.pass_camera.position()}; + // first pass draws elements which we know are located in section banks, to reduce vbo switching + while (First != Last) + { + + auto *cell = First->second; + // przeliczenia animacji torów w sektorze + cell->RaAnimate(m_framestamp); + bool const tracks_only{editor_far_tracks_only(far_paths, cell, camera, scenery_limit)}; + + switch (m_renderpass.draw_mode) + { + case rendermode::color: + { + // since all shapes of the section share center point we can optimize out a few calls here + ::glPushMatrix(); + auto const originoffset{cell->m_area.center - m_renderpass.pass_camera.position()}; + ::glTranslated(originoffset.x, originoffset.y, originoffset.z); + + // render + // opaque non-instanced shapes + if (false == tracks_only) + { + for (auto const &shape : cell->m_shapesopaque) + { + Render(shape, false); + } + } + // tracks + // TODO: update after path node refactoring + Render(std::begin(cell->m_paths), std::end(cell->m_paths)); + // post-render cleanup + ::glPopMatrix(); + + break; + } + case rendermode::reflections: + { + // since all shapes of the section share center point we can optimize out a few calls here + ::glPushMatrix(); + auto const originoffset{cell->m_area.center - m_renderpass.pass_camera.position()}; + ::glTranslated(originoffset.x, originoffset.y, originoffset.z); + + // render + // opaque non-instanced shapes + for (auto const &shape : cell->m_shapesopaque) + Render(shape, false); + + // post-render cleanup + ::glPopMatrix(); + + break; + } + case rendermode::shadows: + { + // since all shapes of the section share center point we can optimize out a few calls here + ::glPushMatrix(); + auto const originoffset{cell->m_area.center - m_renderpass.pass_camera.position()}; + ::glTranslated(originoffset.x, originoffset.y, originoffset.z); + + // render + // opaque non-instanced shapes + for (auto const &shape : cell->m_shapesopaque) + Render(shape, false); + // tracks + Render(std::begin(cell->m_paths), std::end(cell->m_paths)); + + // post-render cleanup + ::glPopMatrix(); + + break; + } + case rendermode::pickscenery: + { + // same procedure like with regular render, but editor-enabled nodes receive custom colour used for picking + // since all shapes of the section share center point we can optimize out a few calls here + ::glPushMatrix(); + auto const originoffset{cell->m_area.center - m_renderpass.pass_camera.position()}; + ::glTranslated(originoffset.x, originoffset.y, originoffset.z); + // render + // opaque non-instanced shapes + // non-interactive scenery elements get neutral colour + if (false == tracks_only) + { + model_ubs.param[0] = colors::none; + for (auto const &shape : cell->m_shapesopaque) + Render(shape, false); + } + // tracks + for (auto *path : cell->m_paths) + { + model_ubs.param[0] = glm::vec4(pick_color(m_picksceneryitems.size() + 1), 1.0f); + Render(path); + } + // post-render cleanup + ::glPopMatrix(); + break; + } + case rendermode::pickcontrols: + default: + { + break; + } + } + + ++First; + } + // second pass draws elements with their own vbos + while (first != Last) + { + + auto const *cell = first->second; + + switch (m_renderpass.draw_mode) + { + case rendermode::color: + case rendermode::shadows: + { + if (editor_far_tracks_only(far_paths, cell, camera, scenery_limit)) + break; + // TBD, TODO: refactor in to a method to reuse in branch below? + // opaque parts of instanced models -- accumulate this cell's buckets + // into the frame-level map; the actual Render_Instanced() calls are + // issued once per unique model after the cell loop (see flush below). + // + // Cell-level far-distance pre-cull: when the whole cell lies beyond + // the instance draw distance, every instance in it would fail the + // per-instance drawdistancethreshold test inside Render_Instanced(), + // so there is no point merging its buckets into the frame map. The + // test reproduces Render_Instanced()'s distance maths exactly -- same + // ZoomFactor / fDistanceFactor scaling, same +250 margin -- applied to + // the nearest point of the cell's bounding sphere. basic_cell::enclose_area + // guarantees m_area.radius >= |m_area.center - instance.location()| for + // every contained instance, so the nearest-point distance is a true + // lower bound on every instance's distance: the cull can never drop a + // cell that still holds a drawable instance. Shadows measure from the + // real (viewport) camera, matching Render_Instanced()'s shadow branch; + // every other gated mode measures from the pass camera. Non-instanced + // scenery drawn by the cpu and vehicles below keep their own per-node + // culling and are intentionally left untouched; the non-instanced models + // culled on the gpu (Queue_Model()) are left out with the cell the same way. + { + auto const &distancecamera = ( + m_renderpass.draw_mode == rendermode::shadows + ? m_renderpass.viewport_camera + : m_renderpass.pass_camera ); + auto const cellcenterdistance { glm::length( cell->m_area.center - distancecamera.position() ) }; + auto const cellnearestdistance { std::max( 0.0, cellcenterdistance - cell->m_area.radius ) }; + auto const cellnearestdistancesquared { + ( cellnearestdistance * cellnearestdistance ) + / ( static_cast( Global.ZoomFactor ) * static_cast( Global.ZoomFactor ) ) + / static_cast( Global.fDistanceFactor ) }; + auto const cellinrange { cellnearestdistancesquared <= sq( static_cast( m_renderpass.draw_range ) + 250.0 ) }; + if( cellinrange ) { + Queue_Instances( cell ); + } + // remaining (non-instanceable) opaque instance nodes go through the per-node path, or with the instances + // to the gpu culling, each on its own + for (auto *instance : cell->m_instancesopaque) + { + if( instance->m_instanceable ) { continue; } // already handled via bucket + // the model of a lamp would shadow everything in the map of the lamp + if( instance == m_cubeskipinstance ) { continue; } + if( walk_queue_model( instance, cellinrange ) ) { continue; } + Render(instance); + m_modelcomparekey = nullptr; + } + } + // opaque parts of vehicles. the kept scenery part of the map of a lamp is drawn without them + if( false == m_cubescenery ) { + for (auto *path : cell->m_paths) + { + for (auto *dynamic : path->Dynamics) + { + // the vehicle of the headlights a map is drawn for would cover all of it + if (dynamic == m_shadowskipvehicle) + continue; + Render(dynamic); + } + } + } + break; + } + case rendermode::reflections: + { + // a cell lying whole beyond the range the reflections draw model instances at is left out, as each + // of its instances would fail the range test done for it in Render_Instanced() and Render( TAnimModel * ). + // like the cull of the regular passes above, the test goes by the nearest point of the bounding sphere + // of the cell, which holds the locations of all its instances + auto const cellnearestdistance { std::max( 0.0, glm::length( cell->m_area.center - m_renderpass.pass_camera.position() ) - cell->m_area.radius ) }; + if( ( Global.reflectiontune.fidelity >= 1 ) + && ( cellnearestdistance <= Global.reflectiontune.range_instances ) ) { + // opaque parts of instanced models -- accumulate into the + // frame-level map; flushed once per unique model after the loop. + Queue_Instances( cell ); + for( auto *instance : cell->m_instancesopaque ) { + if( instance->m_instanceable ) { continue; } + if( walk_queue_model( instance, true ) ) { continue; } + Render( instance ); + m_modelcomparekey = nullptr; + } + } + if( Global.reflectiontune.fidelity >= 2 ) { + // opaque parts of vehicles + for( auto *path : cell->m_paths ) { + for( auto *dynamic : path->Dynamics ) { + Render( dynamic ); + } + } + } + break; + } + case rendermode::pickscenery: + { + if (editor_far_tracks_only(far_paths, cell, camera, scenery_limit)) + break; + // opaque parts of instanced models + // same procedure like with regular render, but each node receives custom colour used for picking. + // picking always uses the per-instance path because each instance needs a unique pick colour; + // batching would assign the same colour to every instance in the bucket. + for (auto *instance : cell->m_instancesopaque) + { + model_ubs.param[0] = glm::vec4(pick_color(m_picksceneryitems.size() + 1), 1.0f); + Render(instance); + } + // vehicles aren't included in scenery picking for the time being + break; + } + case rendermode::pickcontrols: + default: + { + break; + } + } + + ++first; + } + + // flush accumulated instance buckets: issue one Render_Instanced() per unique + // (TModel3d*, skins) key across every cell visited in this pass, instead of + // one call per cell. All per-instance frustum/distance/pixel-area culling + // still happens inside Render_Instanced(), so correctness is unchanged -- only + // the number of calls and glBufferSubData round-trips drops sharply. + // (For pickscenery/pickcontrols modes the map stays empty, so this is a no-op.) + // what the backend draws of the instances itself (the gpu culling of the opengl renderer), and the comparison with it + auto const compare { walk_instances_begin() }; + for( auto &bucket : m_frame_instance_buckets ) { + if( bucket.second.empty() ) { continue; } + Render_Instanced( bucket.first.pModel, bucket.second, ( compare ? &m_instance_bandcounts : nullptr ) ); + if( compare ) { + walk_instances_compare( bucket.first, m_instance_bandcounts ); + } + bucket.second.clear(); + } + walk_instances_end(); + m_modelcpudistances.clear(); +} + +void scene_walk::Render(scene::shape_node const &Shape, bool const Ignorerange) +{ + auto const &data{Shape.data()}; + + // a shape lying wholly outside the view of the pass draws nothing. the section and the cell holding it were tested as a + // whole, while a section holds the chunks of the heightmap terrain (250 m each, 16 of them) and the large shapes of the + // scenery, drawn in every pass the section is seen in: in a cascade of the sun shadows or a face of the environment map + // most of them are out of view. a shape merged from others has its radius invalidated (-1) and keeps being drawn + if ((data.area.radius > 0.f) && (false == m_renderpass.pass_camera.visible(data.area))) + { + return; + } + + if (false == Ignorerange) + { + double distancesquared; + switch (m_renderpass.draw_mode) + { + case rendermode::shadows: + { + // 'camera' for the light pass is the light source, but we need to draw what the 'real' camera sees + distancesquared = glm::length2((data.area.center - m_renderpass.viewport_camera.position()) / (double)Global.ZoomFactor) / Global.fDistanceFactor; + break; + } + case rendermode::reflections: + { + // reflection mode draws simplified version of the shapes, by artificially increasing view range + distancesquared = + // TBD, TODO: bind offset value with setting variable? + sq(EU07_REFLECTIONFIDELITYOFFSET) + + glm::length2( ( data.area.center - m_renderpass.pass_camera.position() ) ); +/* + // TBD: take into account distance multipliers? + / Global.fDistanceFactor; +*/ + break; + } + default: + { + distancesquared = glm::length2((data.area.center - m_renderpass.pass_camera.position()) / (double)Global.ZoomFactor) / Global.fDistanceFactor; + break; + } + } + if ((distancesquared < data.rangesquared_min) || (distancesquared >= data.rangesquared_max)) + { + return; + } + } + + // setup + switch (m_renderpass.draw_mode) + { + case rendermode::color: + case rendermode::reflections: + walk_material(data.material, nullptr, &Shape.data().lighting ); + break; + case rendermode::shadows: + // skip if the shadow caster rank is too low for currently set threshold + if( walk_material_data( data.material ).shadow_rank > Global.gfx_shadow_rank_cutoff ) + return; + walk_shadow_material(data.material); + break; + case rendermode::pickscenery: + case rendermode::pickcontrols: + walk_program( walk_shader::pick ); + break; + default: + break; + } + // render + + walk_draw(data.geometry); + // debug data + ++m_renderpass.draw_stats.shapes; + ++m_renderpass.draw_stats.drawcalls; +} + +void scene_walk::Render(TAnimModel *Instance) +{ + + if (false == Instance->m_visible) { return; } + if( false == m_renderpass.pass_camera.visible( Instance->m_area ) ) { return; } + + double distancesquared; + switch (m_renderpass.draw_mode) + { + case rendermode::shadows: + { + // 'camera' for the light pass is the light source, but we need to draw what the 'real' camera sees + distancesquared = glm::length2((Instance->location() - m_renderpass.viewport_camera.position()) / (double)Global.ZoomFactor) / Global.fDistanceFactor; + break; + } + case rendermode::reflections: + { + // reflection mode draws simplified version of the shapes, by artificially increasing view range + // it also ignores zoom settings and distance multipliers + // TBD: take into account distance multipliers? + distancesquared = glm::length2((Instance->location() - m_renderpass.pass_camera.position())) /* / Global.fDistanceFactor */; + // NOTE: arbitrary draw range limit + if( distancesquared > Global.reflectiontune.range_instances * Global.reflectiontune.range_instances ) { + return; + } + // TBD, TODO: bind offset value with setting variable? + distancesquared += sq(EU07_REFLECTIONFIDELITYOFFSET); + break; + } + default: + { + distancesquared = glm::length2((Instance->location() - m_renderpass.pass_camera.position()) / (double)Global.ZoomFactor) / Global.fDistanceFactor; + break; + } + } + if ((distancesquared < Instance->m_rangesquaredmin) || (distancesquared >= Instance->m_rangesquaredmax)) + { + return; + } + // crude way to reject early items too far to affect the output (mostly relevant for shadow passes) + auto const drawdistancethreshold{ m_renderpass.draw_range + 250 }; + if( distancesquared > sq(drawdistancethreshold) ) { + return; + } + // second stage visibility cull, reject modelstoo far away to be noticeable + auto const radiussquared { Instance->radius() * Instance->radius() }; + if( radiussquared * Global.ZoomFactor / distancesquared < 0.003 * 0.003 ) { + return; + } + if( m_modelcomparekey != nullptr ) { + // queued in the gpu culling as well, to compare with (Queue_Model()) + m_modelcpudistances.emplace_back( *m_modelcomparekey, static_cast( distancesquared ) ); + } + + switch (m_renderpass.draw_mode) + { + case rendermode::pickscenery: + { + // add the node to the pick list + m_picksceneryitems.emplace_back(Instance); + break; + } + default: + { + break; + } + } + + Instance->RaAnimate(m_framestamp); // jednorazowe przeliczenie animacji + Instance->RaPrepare(); + if (Instance->pModel) + { + // per-instance uniform scale: applied between the rotation and the + // submodel-local transform chain. Pushing it on the matrix stack here + // means Render(TModel3d, ..., position, angle) doesn't need a new + // signature and the same scale flows naturally into Render_Alpha and + // any other recursive callers. + auto const &scale = Instance->Scale(); + bool const scaled = ( scale.x != 1.0f || scale.y != 1.0f || scale.z != 1.0f ); + if( scaled ) { + // Per-axis scale applied between rotation and the submodel-local + // chain: build the per-instance transform here so we can inject + // glScalef before the submodel walk. + ::glPushMatrix(); + auto const Position = Instance->location() - m_renderpass.pass_camera.position(); + auto const Angle = Instance->vAngle; + ::glTranslated( Position.x, Position.y, Position.z ); + if( Angle.y != 0.0 ) ::glRotated( Angle.y, 0.f, 1.f, 0.f ); + if( Angle.x != 0.0 ) ::glRotated( Angle.x, 1.f, 0.f, 0.f ); + if( Angle.z != 0.0 ) ::glRotated( Angle.z, 0.f, 0.f, 1.f ); + ::glScalef( scale.x, scale.y, scale.z ); + Render( Instance->pModel, Instance->Material(), distancesquared ); + ::glPopMatrix(); + } + else { + // fast path for unscaled (the common case): existing behaviour + Render(Instance->pModel, Instance->Material(), distancesquared, Instance->location() - m_renderpass.pass_camera.position(), Instance->vAngle); + } + // debug data + ++m_renderpass.draw_stats.models; + } +} + +// adds the bucketed instances of a scene cell to the frame-level buckets, for Render_Instanced() to draw. +// Render_Instanced() culls each instance it's given by its record. the tests it makes are repeated here, with some slack +// for the single precision of the records, and the instances which fail are left out of the queue +void scene_walk::Queue_Instances( scene::basic_cell const *Cell ) +{ + // the editor moves and resizes the instances in place. the records are made anew for each frame then, + // but aren't relied on to leave instances out of the queue + auto const userecords { false == EditorModeFlag }; + auto const reflections { m_renderpass.draw_mode == rendermode::reflections }; + // same choices as in Render_Instanced(): shadows measure the distance from the real camera, reflections + // ignore zoom and distance factor, test the plain distance against their own range and add the fidelity offset + auto const &distancecamera { m_renderpass.draw_mode == rendermode::shadows ? m_renderpass.viewport_camera : m_renderpass.pass_camera }; + glm::dvec3 const cameraposition { distancecamera.position() }; + auto const distancescale { reflections ? 1.0 : 1.0 / ( static_cast( Global.ZoomFactor ) * static_cast( Global.ZoomFactor ) * static_cast( Global.fDistanceFactor ) ) }; + auto const distanceoffset { reflections ? sq( static_cast( EU07_REFLECTIONFIDELITYOFFSET ) ) : 0.0 }; + auto const slack { 1.001 }; + auto const distancelimit { slack * ( reflections ? sq( static_cast( Global.reflectiontune.range_instances ) ) : sq( static_cast( m_renderpass.draw_range ) + 250.0 ) ) }; + // up close the rounding of the records is too large a part of the distance for the slack to cover it + auto const distancetested { sq( 64.0 ) }; + // a bucket whose visible instances all fail the tests of the culling by the bounds of their records (Bucket_Culled()) is left + // out of the pass before the culling. the bounds are made with the records, which the editor doesn't let go by + auto const buckettests { Global.gfx_gpucull_bucketrange && userecords }; + + for( auto const &bucket : Cell->m_instancebuckets_opaque ) { + auto const &instancebucket { bucket.second }; + if( ( instancebucket.recordsversion != m_instancerecordsversion ) + || ( instancebucket.records.size() != instancebucket.instances.size() ) ) { + Update_Instance_Records( bucket.first.pModel, instancebucket, Cell->m_area.center ); + } + if( buckettests && Bucket_Culled( instancebucket ) ) { + walk_bucket_culled( instancebucket ); + continue; + } + // the backend may draw the instances of the bucket itself (the gpu culling of the opengl renderer); the lod bands drawn go + // by the distances the locations of the instances can be at: of their own bounds, or of the cell, which holds them too + if( false == walk_bucket( bucket.first, instancebucket, buckettests, Cell->m_area ) ) { continue; } + auto const &records { instancebucket.records }; + std::vector *dest { nullptr }; // looked up when there's something to put in it + for( std::size_t idx = 0; idx < records.size(); ++idx ) { + if( userecords ) { + auto const &record { records[ idx ] }; + auto const center { instancebucket.recordorigin + glm::dvec3{ record.position } }; + auto const distancesquared { glm::length2( center - cameraposition ) }; + if( distancesquared > distancetested ) { + auto const scaleddistancesquared { distancesquared * distancescale }; + if( scaleddistancesquared > distancelimit ) { continue; } + if( scaleddistancesquared + distanceoffset >= slack * std::min( instancebucket.lodrangesquared, record.rangesquaredmax ) ) { continue; } + } + if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ center, record.radius } ) ) { continue; } + } + if( dest == nullptr ) { + dest = &m_frame_instance_buckets[ bucket.first ]; + } + dest->push_back( { &instancebucket, idx } ); + } + } +} + +// makes the records of the instances held by a bucket of a scene cell, with locations relative to Origin +void scene_walk::Update_Instance_Records( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket, glm::dvec3 const &Origin ) +{ + // a submodel is drawn only closer than its fSquareMaxDist, see Render( TSubModel * ). past the largest of these + // there's nothing of the model left to draw, which makes it another limit of the range of its instances + auto lodrangesquared { ( Model != nullptr && Model->Root != nullptr ) ? 0.f : std::numeric_limits::max() }; + m_instance_lodpending.clear(); + m_instance_lodpending.push_back( Model != nullptr ? Model->Root : nullptr ); + while( false == m_instance_lodpending.empty() ) { + auto const *submodel = m_instance_lodpending.back(); + m_instance_lodpending.pop_back(); + if( submodel == nullptr ) { continue; } + lodrangesquared = std::max( lodrangesquared, submodel->fSquareMaxDist ); + m_instance_lodpending.push_back( submodel->Child ); + m_instance_lodpending.push_back( submodel->Next ); + } + Bucket.lodrangesquared = lodrangesquared; + Bucket.recordorigin = Origin; + + auto const tofloat = []( double const Value ) { + return static_cast( std::min( Value, static_cast( std::numeric_limits::max() ) ) ); }; + + Bucket.records.clear(); + Bucket.records.reserve( Bucket.instances.size() ); + // the range of the bucket: past it none of its instances is drawn, by its own range or by the submodels of the model + auto rangesquared { 0.f }; + for( auto *instance : Bucket.instances ) { + scene::basic_cell::instance_record record; + if( instance != nullptr ) { + record.position = packed_vec3{ instance->location() - Origin }; + record.radius = instance->radius(); + record.rangesquaredmin = tofloat( instance->m_rangesquaredmin ); + record.rangesquaredmax = tofloat( instance->m_rangesquaredmax ); + rangesquared = std::max( rangesquared, record.rangesquaredmax ); + record.flags = + ( instance->visible() ? scene::basic_cell::instance_visible : 0u ) + | ( instance->m_instanceable ? scene::basic_cell::instance_instanceable : 0u ); + auto const &rotationscale { instance->rotation_scale() }; + for( int column = 0; column < 3; ++column ) { + record.rotationscale[ column ] = rotationscale[ column ]; + } + } + // an empty slot keeps its record without flags, and isn't drawn + Bucket.records.emplace_back( record ); + } + Bucket.rangesquared = std::min( rangesquared, lodrangesquared ); + + // bounds of the visible instances, for Bucket_Culled(): the centre of the box around their locations, and the spheres + // around it which hold the locations and the instances whole. the gpu leaves out the ones which aren't visible + glm::dvec3 low { std::numeric_limits::max() }; + glm::dvec3 high { std::numeric_limits::lowest() }; + auto radiusmax { 0.f }; + for( auto const &record : Bucket.records ) { + if( ( record.flags & scene::basic_cell::instance_visible ) == 0 ) { continue; } + glm::dvec3 const position { record.position }; + low = glm::min( low, position ); + high = glm::max( high, position ); + radiusmax = std::max( radiusmax, record.radius ); + } + Bucket.recordlocationradius = 0.f; + Bucket.recordradius = -1.f; + Bucket.recordradiusmax = radiusmax; + if( low.x <= high.x ) { + auto const center { 0.5 * ( low + high ) }; + auto locationradius { 0.0 }; + auto radius { 0.0 }; + for( auto const &record : Bucket.records ) { + if( ( record.flags & scene::basic_cell::instance_visible ) == 0 ) { continue; } + auto const distance { glm::length( glm::dvec3{ record.position } - center ) }; + locationradius = std::max( locationradius, distance ); + radius = std::max( radius, distance + record.radius ); + } + Bucket.recordcenter = Origin + center; + Bucket.recordlocationradius = static_cast( locationradius ); + Bucket.recordradius = static_cast( radius ); + } + else { + Bucket.recordcenter = Origin; + } + Bucket.recordsversion = m_instancerecordsversion; + Bucket.gpustale = true; +} + +// whether none of the instances of a bucket can pass the tests of the culling of the pass (Render_Instanced(), on the gpu +// instance_cull.comp): by the bounds of their records the visible ones lie wholly out of the frustum of the pass, beyond their +// range or the draw range, or are too small on the screen. the tests are those of the culling with the nearest location and +// the largest instance of the bucket, with a margin for the single precision of the records and of the gpu, so they leave out +// only buckets which the culling would leave out whole +bool scene_walk::Bucket_Culled( scene::basic_cell::instance_bucket const &Bucket ) const +{ + if( Bucket.recordradius < 0.f ) { return true; } + + auto const slack { 1.001 }; + auto const margin { 0.5 }; // metres + // the planes of the frustum in world coordinates, the ones the gpu culling moves to the camera of the pass + auto const &planes { m_renderpass.pass_camera.frustum().planes() }; + auto const ¢er { Bucket.recordcenter }; + auto const radius { slack * Bucket.recordradius + margin }; + for( auto const *plane : planes ) { + if( plane[ 0 ] * center.x + plane[ 1 ] * center.y + plane[ 2 ] * center.z + plane[ 3 ] <= -radius ) { return true; } + } + + // the same choices as in Render_Instanced(): shadows measure the distance from the real camera, reflections ignore zoom and + // distance factor, test the plain distance against their own range and add the fidelity offset + auto const reflections { m_renderpass.draw_mode == rendermode::reflections }; + auto const &distancecamera { m_renderpass.draw_mode == rendermode::shadows ? m_renderpass.viewport_camera : m_renderpass.pass_camera }; + auto const nearest { std::max( 0.0, glm::length( center - distancecamera.position() ) - ( slack * Bucket.recordlocationradius + margin ) ) }; + auto const zoom { static_cast( Global.ZoomFactor ) }; + auto const scaledsquared { nearest * nearest * ( reflections ? 1.0 : 1.0 / ( zoom * zoom * static_cast( Global.fDistanceFactor ) ) ) }; + if( reflections && ( scaledsquared > slack * sq( static_cast( Global.reflectiontune.range_instances ) ) ) ) { return true; } + auto const distancesquared { scaledsquared + ( reflections ? sq( static_cast( EU07_REFLECTIONFIDELITYOFFSET ) ) : 0.0 ) }; + if( distancesquared >= slack * Bucket.rangesquared ) { return true; } + if( distancesquared > slack * sq( static_cast( m_renderpass.draw_range ) + 250.0 ) ) { return true; } + // the largest instance at the nearest location takes the largest part of the view + auto const radiusmax { static_cast( Bucket.recordradiusmax ) }; + if( radiusmax * radiusmax * zoom * slack < 0.003 * 0.003 * distancesquared ) { return true; } + + return false; +} + +// the sphere around the locations of the visible instances of a bucket, with the margin of Bucket_Culled(): their distances +// from it set the lod bands the gpu culling can put them in +scene::bounding_area scene_walk::Bucket_Area( scene::basic_cell::instance_bucket const &Bucket ) +{ + return scene::bounding_area{ Bucket.recordcenter, static_cast( 1.001 * Bucket.recordlocationradius + 0.5 ) }; +} + +// True GPU-instanced render path for a group of TAnimModel instances sharing the same +// TModel3d. The submodel tree is walked ONCE per lod band; at every submodel that draws +// geometry we issue a single glDrawElementsInstancedBaseVertexBaseInstance(N) covering all +// visible instances of the band. The vertex shader multiplies effective_modelview = +// instance_modelview[gl_BaseInstance + gl_InstanceID] * model_ubs.modelview, where +// instance_modelview[i] is the camera-space root transform of an instance (precomputed below) +// and model_ubs.modelview is the submodel-local chain (accumulated by the matrix stack starting +// from identity, NOT the camera/instance frame). +void scene_walk::Render_Instanced( TModel3d *Model, std::vector const &Instances, std::vector *Bandcounts ) +{ + if( Bandcounts != nullptr ) { Bandcounts->clear(); } + if( Model == nullptr ) { return; } + if( Instances.empty() ) { return; } + + // LOD: a submodel is drawn only while fSquareDist lies inside its [fSquareMinDist, fSquareMaxDist) + // range, so the distinct range bounds split distance into bands with constant LOD. The bucket + // collects instances from every visible cell, so a single fSquareDist for all of them would draw + // far instances at the LOD of the nearest one. Each instance is assigned to its band and every + // band is drawn with its own fSquareDist, like in Render_Sleepers(). The grouping is a counting + // sort, linear in the instance count. + // the same walk also checks whether any piece of the model can get into the shadow map. Models + // whose materials are all excluded by the shadow rank cutoff (e.g. grass) would otherwise go through + // the whole per-instance culling and the batched draws in every shadow pass, only for every submodel + // to be skipped in Render(TSubModel*) + auto const *frontinstance { Instances.front().bucket->instances[ Instances.front().index ] }; + auto const *skins { frontinstance != nullptr && frontinstance->Material() != nullptr ? frontinstance->Material()->replacable_skins : nullptr }; + bool castsshadow { m_renderpass.draw_mode != rendermode::shadows }; + m_instance_lodbounds.clear(); + m_instance_lodpending.clear(); + m_instance_lodpending.push_back( Model->Root ); + while( false == m_instance_lodpending.empty() ) { + auto const *submodel = m_instance_lodpending.back(); + m_instance_lodpending.pop_back(); + if( submodel == nullptr ) { continue; } + if( submodel->fSquareMinDist > 0.f ) { m_instance_lodbounds.emplace_back( submodel->fSquareMinDist ); } + if( submodel->fSquareMaxDist < std::numeric_limits::max() ) { m_instance_lodbounds.emplace_back( submodel->fSquareMaxDist ); } + if( ( false == castsshadow ) && ( 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 ); + } + if( false == castsshadow ) { return; } + std::sort( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ); + m_instance_lodbounds.erase( std::unique( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ), m_instance_lodbounds.end() ); + auto const bandcount { m_instance_lodbounds.size() + 1 }; + if( Bandcounts != nullptr ) { Bandcounts->assign( bandcount, 0 ); } + + // 1. Visibility / distance cull, by the records of the instances. Collect surviving instances with + // their precomputed camera-space root modelview matrices and lod bands. m_instance_survivors and the + // other scratch vectors are persistent members reused across every Render_Instanced() call: + // clear() drops the contents but keeps the allocated capacity, so after the first few + // frames this stops calling malloc/free entirely. + m_instance_survivors.clear(); + m_instance_survivors.reserve( Instances.size() ); + + // Pull the current pass camera/view transform once. We use the current GL + // modelview matrix as the view matrix because at the point Render_Instanced + // is called, OpenGLMatrices is set to the camera view (scene root) — no + // per-instance transform has been pushed yet. + glm::mat4 const view_matrix = OpenGLMatrices.data( GL_MODELVIEW ); + + TAnimModel *batch_instance { nullptr }; // the first instance drawn; its RaPrepare() and material serve the whole bucket + material_data const *batch_material { nullptr }; + float farthest_distancesquared { 0.f }; + + for( auto const &queued : Instances ) { + auto const &record { queued.bucket->records[ queued.index ] }; + // instances which lost their instanceable status after being bucketed (animation or skin + // assigned by an event) are drawn by the per-node path + if( ( record.flags & scene::basic_cell::instance_instanceable ) == 0 ) { continue; } + if( ( record.flags & scene::basic_cell::instance_visible ) == 0 ) { continue; } + glm::dvec3 const location { queued.bucket->recordorigin + glm::dvec3{ record.position } }; + if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ location, record.radius } ) ) { continue; } + + double distancesquared; + switch( m_renderpass.draw_mode ) { + case rendermode::shadows: + distancesquared = glm::length2( ( location - m_renderpass.viewport_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor; + break; + case rendermode::reflections: + distancesquared = glm::length2( ( location - m_renderpass.pass_camera.position() ) ); + if( distancesquared > Global.reflectiontune.range_instances * Global.reflectiontune.range_instances ) { continue; } + distancesquared += sq( EU07_REFLECTIONFIDELITYOFFSET ); + break; + default: + distancesquared = glm::length2( ( location - m_renderpass.pass_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor; + break; + } + if( ( distancesquared < record.rangesquaredmin ) + || ( distancesquared >= record.rangesquaredmax ) ) { continue; } + + auto const drawdistancethreshold { m_renderpass.draw_range + 250 }; + if( distancesquared > sq( drawdistancethreshold ) ) { continue; } + + auto const radiussquared { record.radius * record.radius }; + if( radiussquared * Global.ZoomFactor / distancesquared < 0.003 * 0.003 ) { continue; } + + if( batch_instance == nullptr ) { + batch_instance = queued.bucket->instances[ queued.index ]; + batch_instance->RaPrepare(); + batch_material = batch_instance->Material(); + } + + // Build the camera-relative root modelview for this instance: + // mv = view * translate(instance_pos - camera_pos) * rotate(instance_angles) * scale(m_scale) + // The rotation and scale part comes from the record, the translation goes into its last + // column. The scale is folded in so the GPU-instanced path produces visually identical output + // to the regular per-instance path; the shader applies it via effective_modelview = instance_mv * model_local. + glm::mat4 const root { + record.rotationscale[ 0 ], + record.rotationscale[ 1 ], + record.rotationscale[ 2 ], + glm::vec4( glm::vec3( location - m_renderpass.pass_camera.position() ), 1.f ) }; + + auto const distance { static_cast( distancesquared ) }; + farthest_distancesquared = std::max( farthest_distancesquared, distance ); + m_instance_survivors.push_back( { + view_matrix * root, + distance, + static_cast( std::upper_bound( m_instance_lodbounds.begin(), m_instance_lodbounds.end(), distance ) - m_instance_lodbounds.begin() ) } ); + } + + if( m_instance_survivors.empty() ) { return; } + + // group the modelviews by lod band into a contiguous array, for the upload. every distance in + // a band selects the same lod; the nearest one represents the band. + // within a band the instances are additionally ordered roughly near-to-far, in distance slots. dense + // alpha tested foliage then gets the most out of the early depth test, as the nearest instances fill + // the depth buffer first and hide the fragments of those behind them before they get shaded. + // both levels are done with a single counting sort, linear in the instance count + auto const distanceslots { 16 }; + auto const keycount { bandcount * distanceslots }; + auto const slotscale { distanceslots / std::sqrt( std::max( farthest_distancesquared, 1.f ) ) }; + m_instance_bandstarts.assign( keycount + 1, 0 ); + m_instance_banddistances.assign( bandcount, std::numeric_limits::max() ); + for( auto &survivor : m_instance_survivors ) { + m_instance_banddistances[ survivor.band ] = std::min( m_instance_banddistances[ survivor.band ], survivor.distancesquared ); + auto const slot { std::min( distanceslots - 1, static_cast( std::sqrt( survivor.distancesquared ) * slotscale ) ) }; + survivor.band = static_cast( survivor.band * distanceslots + slot ); // from now on it's the sort key + ++m_instance_bandstarts[ survivor.band + 1 ]; + } + for( std::size_t key = 1; key <= keycount; ++key ) { + m_instance_bandstarts[ key ] += m_instance_bandstarts[ key - 1 ]; + } + m_instance_bandcursors.assign( m_instance_bandstarts.begin(), m_instance_bandstarts.end() - 1 ); + m_instance_modelviews.resize( m_instance_survivors.size() ); + for( auto const &survivor : m_instance_survivors ) { + m_instance_modelviews[ m_instance_bandcursors[ survivor.band ]++ ] = survivor.modelview; + } + if( Bandcounts != nullptr ) { + for( std::size_t band = 0; band < bandcount; ++band ) { + ( *Bandcounts )[ band ] = static_cast( m_instance_bandstarts[ ( band + 1 ) * distanceslots ] - m_instance_bandstarts[ band * distanceslots ] ); + } + } + // all bands go to the instance buffer at once, each band is drawn from its own base instance + auto const baseinstance { walk_instances( m_instance_modelviews.data(), m_instance_modelviews.size() ) }; + + auto alpha = ( batch_material != nullptr ? batch_material->textures_alpha : 0x30300030 ); + alpha ^= 0x0F0F000F; + + // 2. Walk the submodel tree once per lod band. The submodel-local matrix + // stack starts at identity; the per-instance camera transform comes from + // instance_modelview[gl_BaseInstance + gl_InstanceID] in the shader. + for( std::size_t band = 0; band < bandcount; ++band ) { + + std::size_t const band_start = m_instance_bandstarts[ band * distanceslots ]; + std::size_t const band_end = m_instance_bandstarts[ ( band + 1 ) * distanceslots ]; + if( band_start == band_end ) { continue; } + + // 2a. Push identity onto the matrix stack so submodel transforms + // accumulate from identity, not from camera/instance space. + ::glPushMatrix(); + ::glLoadIdentity(); + + // 2b. Configure the existing TModel3d/TSubModel render path. Setting + // m_current_instance_count routes walk_draw(handle) calls to draw_instanced. + m_current_instance_count = band_end - band_start; + m_current_instance_base = baseinstance + static_cast( band_start ); + + Model->Root->fSquareDist = m_instance_banddistances[ band ]; // shared global, used by submodel LOD + Model->Root->ReplacableSet( ( batch_material != nullptr ? batch_material->replacable_skins : nullptr ), alpha ); + Model->Root->pRoot = Model; + + Render( Model->Root ); + + m_current_instance_count = 0; + m_current_instance_base = 0; + + ::glPopMatrix(); + + ++m_renderpass.draw_stats.instanced_drawcalls; + } + + m_renderpass.draw_stats.instances += static_cast( m_instance_survivors.size() ); + m_renderpass.draw_stats.models += static_cast( m_instance_survivors.size() ); +} + +void scene_walk::Render_Sleepers( TTrack *Track ) +{ + if( Track == nullptr ) { return; } + if( false == Track->m_sleeper_enabled ) { return; } + if( Track->m_sleeper_model == nullptr ) { return; } + if( Track->m_sleeper_local_transforms.empty() ) { return; } + if( Global.SleeperDistance <= 0.f ) { return; } + + // only the color and reflection passes draw sleepers; shadow/pick skip them on purpose + // (sleeper shadows would mostly fall back under the trackbed and pick already operates on + // the track itself). + switch( m_renderpass.draw_mode ) { + case rendermode::color: + case rendermode::reflections: + break; + default: + return; + } + + // distance gate -- compare against Globals.SleeperDistance squared to avoid the sqrt + auto const camerapos = m_renderpass.pass_camera.position(); + auto const trackpos = Track->location(); + auto const distsq = glm::length2( trackpos - camerapos ); + auto const cutoffsq = static_cast( Global.SleeperDistance ) * static_cast( Global.SleeperDistance ); + if( distsq > cutoffsq ) { return; } + + // build camera-space modelview matrices. + // each sleeper's stored matrix is in track-local space (relative to Track->m_origin). + // Render_Sleepers is called from inside the per-cell origin push -- the cell's center + // already equals Track->m_origin (see basic_cell::insert), so the current GL_MODELVIEW + // is already view * translate(m_origin - camera). We just need to compose with each + // per-sleeper local transform to get the final modelview. + glm::mat4 const origin_mv = OpenGLMatrices.data( GL_MODELVIEW ); + + // per-sleeper frustum cull + LOD selection. The whole-track SleeperDistance + // gate above keeps or drops the track as a unit; everything from here on + // operates on individual sleepers. + // + // Each m_sleeper_local_transforms entry is + // translate(world_pos - m_origin) * rotate(...) * translate(local_offset) + // so its translation column is the sleeper position relative to m_origin; + // adding m_origin back yields the sleeper's world-space position. The model's + // bounding radius (floored to a small minimum, in case it was never measured) + // serves both as the frustum test sphere and -- via the distance to that + // world position -- as the per-sleeper LOD distance. + auto const sleeperradius = std::max( Track->m_sleeper_model->bounding_radius(), 2.0f ); + + // Phase 1 -- frustum cull. For every sleeper that survives, store its + // camera-space modelview paired with the squared distance from the camera + // to ITS OWN world position (not the track / segment origin). That + // per-sleeper distance is what drives LOD selection below. A sleeper whose + // origin sits just off screen while its geometry still reaches into view is + // kept rather than wrongly culled. + std::vector> survivors; + survivors.reserve( Track->m_sleeper_local_transforms.size() ); + for( auto const &local : Track->m_sleeper_local_transforms ) { + glm::dvec3 const sleeperworldpos { + Track->m_origin + glm::dvec3( local[ 3 ].x, local[ 3 ].y, local[ 3 ].z ) }; + if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ sleeperworldpos, sleeperradius } ) ) { + continue; + } + auto const sleeperdistancesquared = static_cast( glm::length2( sleeperworldpos - camerapos ) ); + survivors.emplace_back( sleeperdistancesquared, origin_mv * local ); + } + + // every sleeper of this track was frustum-culled -- nothing left to draw + if( survivors.empty() ) { return; } + + // Phase 2 -- sort survivors near-to-far. A submodel is drawn only while + // fSquareDist lies inside its [fSquareMinDist, fSquareMaxDist) range, so + // every distance between two consecutive range bounds selects an identical + // set of submodels -- i.e. the same LOD. Sorting turns each such LOD band + // into a contiguous run, letting the draw loop emit one instanced batch per + // band instead of one track-wide batch at a single distance. + std::sort( survivors.begin(), survivors.end(), + []( std::pair const &Left, std::pair const &Right ) { + return Left.first < Right.first; } ); + + // contiguous copy of the sorted modelview matrices, for the upload. + // Per-sleeper distances stay available as survivors[i].first, in lockstep. + std::vector instance_modelviews; + instance_modelviews.reserve( survivors.size() ); + for( auto const &survivor : survivors ) { + instance_modelviews.emplace_back( survivor.second ); + } + + // collect the model's distinct LOD distance bounds. The sorted, de-duplicated + // set of every submodel's fSquareMinDist / fSquareMaxDist partitions distance + // into bands within which the selected LOD is constant. A model with no LOD + // yields a single band, and the draw loop below then behaves exactly like a + // single plain batched draw. + std::vector lodbounds; + { + std::vector pending; + if( Track->m_sleeper_model->Root != nullptr ) { + pending.push_back( Track->m_sleeper_model->Root ); + } + while( false == pending.empty() ) { + auto const *submodel = pending.back(); + pending.pop_back(); + lodbounds.emplace_back( submodel->fSquareMinDist ); + lodbounds.emplace_back( submodel->fSquareMaxDist ); + if( submodel->Child != nullptr ) { pending.push_back( submodel->Child ); } + if( submodel->Next != nullptr ) { pending.push_back( submodel->Next ); } + } + } + std::sort( lodbounds.begin(), lodbounds.end() ); + lodbounds.erase( std::unique( lodbounds.begin(), lodbounds.end() ), lodbounds.end() ); + + // optional replacable skin: build a transient material_data so we can drive ReplacableSet + // the same way Render_Instanced does. when no skin is set we fall back to the model defaults. + material_data sleeper_material {}; + bool const has_skin = ( Track->m_sleeper_skin != null_handle ); + if( has_skin ) { + sleeper_material.replacable_skins[ 1 ] = Track->m_sleeper_skin; + } + + // Phase 3 -- draw. Walk the sorted survivors one LOD band at a time. Each + // band is submitted as one instanced draw from its own base instance; every + // draw uses an in-band fSquareDist, so each sleeper renders at the LOD its own + // distance selects while instancing stays fully in effect. + auto *Model = Track->m_sleeper_model; + std::size_t const total = instance_modelviews.size(); + auto const baseinstance { walk_instances( instance_modelviews.data(), total ) }; + std::size_t band_start = 0; + while( band_start < total ) { + // the band ends at the first sleeper distance that reaches the next LOD + // bound above the band's starting distance. + auto const upperbound = std::upper_bound( + lodbounds.begin(), lodbounds.end(), survivors[ band_start ].first ); + float const band_limit = ( upperbound == lodbounds.end() + ? std::numeric_limits::max() + : *upperbound ); + std::size_t band_end = band_start; + while( ( band_end < total ) && ( survivors[ band_end ].first < band_limit ) ) { + ++band_end; + } + // every distance in the band selects the same LOD; the band's nearest + // sleeper is used as the representative fSquareDist. + float const band_distancesquared = survivors[ band_start ].first; + + ::glPushMatrix(); + ::glLoadIdentity(); + + m_current_instance_count = band_end - band_start; + m_current_instance_base = baseinstance + static_cast( band_start ); + + Model->Root->fSquareDist = band_distancesquared; + auto alpha = ( has_skin ? sleeper_material.textures_alpha : 0x30300030 ); + alpha ^= 0x0F0F000F; + Model->Root->ReplacableSet( ( has_skin ? sleeper_material.replacable_skins : nullptr ), alpha ); + Model->Root->pRoot = Model; + + Render( Model->Root ); + + m_current_instance_count = 0; + m_current_instance_base = 0; + + ::glPopMatrix(); + + ++m_renderpass.draw_stats.instanced_drawcalls; + + band_start = band_end; + } + + m_renderpass.draw_stats.instances += static_cast( total ); + m_renderpass.draw_stats.models += static_cast( total ); + + // the comparison with the gpu culling of the opengl renderer + walk_sleepers_drawn( *Track, lodbounds, survivors ); +} + +// Renders the sleepers and switch timbers of a run of tracks of a cell. Each beam is made of instances of the parts of a model or +// of a whole model, kept by its track relative to the origin of the track. The runs of beams in range and in sight are gathered +// for the whole run of tracks by the model and its part or its level of detail, and each is drawn in one instanced batch. +// In the shadow pass only those near the camera are drawn, their shadows falling where they can be seen. +void scene_walk::Render_Timbers( scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last ) +{ + if( Global.SleeperDistance <= 0.f ) { return; } + + auto const shadows = ( m_renderpass.draw_mode == rendermode::shadows ); + // the distance is taken from the camera of the view, the shadow pass culling against its own frustum + auto const camera = ( shadows ? m_renderpass.viewport_camera.position() : m_renderpass.pass_camera.position() ); + auto const range = static_cast( shadows ? std::min( Global.SleeperDistance, 50.f ) : Global.SleeperDistance ); + if( m_renderpass.draw_mode == rendermode::color && m_timbersweepframe != m_framestamp ) { + // once a frame the beams of the tracks well past where they're laid and worked out ahead are let go + m_timbersweepframe = m_framestamp; + track_sleepers::release_far( camera, range + 600.0 ); + } + glm::mat4 const originmodelview = OpenGLMatrices.data( GL_MODELVIEW ); + for( auto &bucket : m_timberbuckets ) { + bucket.instances.clear(); + } + auto const bucket_for = [&]( track_sleepers::beam_model const *Model, int const Part, float const Band ) -> std::vector & { + for( auto &bucket : m_timberbuckets ) { + if( bucket.model == Model && bucket.part == Part && bucket.band == Band ) { + return bucket.instances; + } + } + m_timberbuckets.push_back( { Model, Part, Band, {} } ); + return m_timberbuckets.back().instances; }; + // the level of detail of the whole model a beam is seen at, from its distance + auto const band_of = []( track_sleepers::beam_model const &Model, double const Distance ) { + auto const squaredistance { static_cast( Distance * Distance ) }; + auto band { Model.bands.empty() ? 0.f : Model.bands.front() }; + for( auto const start : Model.bands ) { + if( squaredistance >= start ) { + band = start; + } + } + return band; }; + // the beams of a run made of a whole model, each at the level of detail of its own distance; the run is culled as one + auto const put_by_band = [&]( track_sleepers::beam_model const &Model, std::vector const &Transforms, std::uint32_t const From, std::uint32_t const To, glm::dvec3 const &Origin ) { + std::vector *instances { nullptr }; + auto current { 0.f }; + for( auto index = From; index < To; ++index ) { + auto const &local = Transforms[ index ]; + auto const band = band_of( Model, glm::length( Origin + glm::dvec3( glm::vec3( local[ 3 ] ) ) - camera ) ); + if( instances == nullptr || band != current ) { + // looked up again only as the band changes; a bucket added on the way moves the others, so the pointer goes with it + instances = &bucket_for( &Model, -1, band ); + current = band; + } + instances->emplace_back( originmodelview * local ); + } + }; + for( auto first { First }; first != Last; ++first ) { + auto *track = *first; + if( false == track->m_visible || false == track->m_sleepers3d.active ) { continue; } + if( glm::length( track->location() - camera ) > range + track->radius() ) { + // worked out ahead, in the background, before it comes in range + if( false == shadows && track->m_sleepers3d.dirty && glm::length( track->location() - camera ) < range + track->radius() + 300.0 ) { + track_sleepers::prefetch( *track ); + } + continue; + } + if( track->m_sleepers3d.dirty ) { + track_sleepers::lay( *track ); + } + auto const &laid = track->m_sleepers3d; + if( laid.timbers == nullptr ) { continue; } + // the transforms of the beams are relative to the origin of the track, which the modelview is translated to already + auto const &trackmodelview = originmodelview; + for( auto const &chunk : laid.chunks ) { + auto const centre = track->m_origin + glm::dvec3( chunk.centre ); + auto const distance = glm::length( centre - camera ); + if( distance > range + chunk.radius ) { continue; } + if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ centre, chunk.radius } ) ) { continue; } + if( laid.timbers->stretchable ) { + for( int part = 0; part < track_sleepers::part_count; ++part ) { + auto &instances = bucket_for( laid.timbers, part, 0.f ); + for( auto index = chunk.first[ part ]; index < chunk.last[ part ]; ++index ) { + instances.emplace_back( trackmodelview * laid.instances[ part ][ index ] ); + } + } + } + if( chunk.wholelast > chunk.wholefirst ) { + put_by_band( *laid.timbers, laid.whole, chunk.wholefirst, chunk.wholelast, track->m_origin ); + } + if( laid.plains != nullptr && chunk.plainlast > chunk.plainfirst ) { + put_by_band( *laid.plains, laid.plain, chunk.plainfirst, chunk.plainlast, track->m_origin ); + } + } + } + + auto const alpha = ( 0x30300030 ^ 0x0F0F000F ); + std::size_t total { 0 }; + for( auto &bucket : m_timberbuckets ) { + auto const &instances = bucket.instances; + if( instances.empty() ) { continue; } + total += instances.size(); + auto const &beammodel = *bucket.model; + auto *model = beammodel.model; + if( bucket.part >= 0 ) { + // only the part of the model is visible, its siblings are skipped as the model is walked + for( int part = 0; part < track_sleepers::part_count; ++part ) { + beammodel.parts[ part ]->iVisible = ( part == bucket.part ? 1 : 0 ); + } + } + material_data skins {}; + if( beammodel.skin != null_handle ) { + skins.replacable_skins[ 1 ] = beammodel.skin; + } + // the instances go to the instance stream of the frame, as in Render_Sleepers(), which the recorded walk of the depth + // pass keeps for its replay as well + auto const baseinstance { walk_instances( instances.data(), instances.size() ) }; + ::glPushMatrix(); + ::glLoadIdentity(); + m_current_instance_count = instances.size(); + m_current_instance_base = baseinstance; + // inside the range of the level of detail the beams are at + model->Root->fSquareDist = ( bucket.part >= 0 ? 0.f : bucket.band + 1.f ); + model->Root->ReplacableSet( ( beammodel.skin != null_handle ? skins.replacable_skins : nullptr ), alpha ); + model->Root->pRoot = model; + Render( model->Root ); + m_current_instance_count = 0; + m_current_instance_base = 0; + ::glPopMatrix(); + ++m_renderpass.draw_stats.instanced_drawcalls; + if( bucket.part >= 0 ) { + for( int part = 0; part < track_sleepers::part_count; ++part ) { + beammodel.parts[ part ]->iVisible = 1; + } + } + } + m_renderpass.draw_stats.instances += static_cast( total ); +} + +bool scene_walk::Render(TDynamicObject *Dynamic) +{ + glDebug("Render TDynamicObject"); + + if (!Global.render_cab && Global.pCamera.m_owner == Dynamic) + return false; + + Dynamic->renderme = ( + ( Global.pCamera.m_owner == Dynamic && !FreeFlyModeFlag ) + || ( m_renderpass.pass_camera.visible( Dynamic ) ) ); + + if (false == Dynamic->renderme) { return false; } + + // setup + TSubModel::iInstance = reinterpret_cast(Dynamic); //żeby nie robić cudzych animacji + TSubModel::iVariantSeed = Dynamic->variant_seed(); + glm::dvec3 const originoffset = Dynamic->vPosition - m_renderpass.pass_camera.position(); + // lod visibility ranges are defined for base (x 1.0) viewing distance. for render we adjust them for actual range multiplier and zoom + float squaredistance; + switch (m_renderpass.draw_mode) + { + case rendermode::shadows: + { + squaredistance = glm::length2(glm::vec3{glm::dvec3{Dynamic->vPosition - m_renderpass.viewport_camera.position()}} / Global.ZoomFactor); + if( false == FreeFlyModeFlag ) { + // filter out small details if we're in vehicle cab + squaredistance = std::max( 100.f * 100.f, squaredistance ); + } + break; + } + case rendermode::reflections: + { + // reflection mode draws simplified version of the shapes, by artificially increasing view range + // it also ignores zoom settings and distance multipliers + squaredistance = + std::max( + 100.f * 100.f, + // TBD: take into account distance multipliers? + glm::length2( glm::vec3{ originoffset } ) /* / Global.fDistanceFactor */ ); + // NOTE: arbitrary draw range limit + if( squaredistance > Global.reflectiontune.range_vehicles * Global.reflectiontune.range_vehicles ) { + return false; + } + // TBD, TODO: bind offset value with setting variable? + // NOTE: combined 'squared' distance doesn't equal actual squared (distance + offset) but, eh + squaredistance += sq(EU07_REFLECTIONFIDELITYOFFSET); + break; + } + default: + { + squaredistance = glm::length2(glm::vec3{originoffset} / Global.ZoomFactor); + // TODO: filter out small details based on fidelity setting + break; + } + } + // crude way to reject early items too far to affect the output (mostly relevant for shadow and reflection passes) + auto const drawdistancethreshold{ m_renderpass.draw_range + 250 }; + if( squaredistance > sq(drawdistancethreshold) ) { + return false; + } + // second stage visibility cull, reject vehicles too far away to be noticeable + auto const squareradius{ Dynamic->radius() * Dynamic->radius() }; + Dynamic->renderme = ( squareradius * Global.ZoomFactor / squaredistance > 0.003 * 0.003 ); + if( false == Dynamic->renderme ) { + return false; + } + + // debug data + ++m_renderpass.draw_stats.dynamics; + + Dynamic->ABuLittleUpdate(squaredistance); // ustawianie zmiennych submodeli dla wspólnego modelu + + glm::mat4 future_stack = model_ubs.future; + + glm::mat4 mv = OpenGLMatrices.data(GL_MODELVIEW); + model_ubs.future *= glm::translate(mv, glm::vec3(Dynamic->get_future_movement())) * glm::inverse(mv); + // the velocity it leaves in the colour pass keeps the motion blur on (Render_pass()) + if ((m_renderpass.draw_mode == rendermode::color) && (Dynamic->get_future_movement() != glm::dvec3{0.0})) + m_vehiclesmoving = true; + + ::glPushMatrix(); + ::glTranslated(originoffset.x, originoffset.y, originoffset.z); + ::glMultMatrixd(glm::value_ptr(Dynamic->mMatrix)); + + switch (m_renderpass.draw_mode) + { + + case rendermode::color: + case rendermode::reflections: + { + if (Dynamic->fShade > 0.0f) + { + // change light level based on light level of the occupied track + walk_sunlight(Dynamic->fShade); + } + + // render + if( Dynamic->mdLowPolyInt ) { + // low poly interior + Render_lowpoly( Dynamic, squaredistance, false ); + } + else { + // HACK: if the model lacks low poly interior, we presume the load is placed outside + if( Dynamic->mdLoad ) { + // renderowanie nieprzezroczystego ładunku + Render( Dynamic->mdLoad, Dynamic->Material(), squaredistance, { 0.f, Dynamic->LoadOffset, 0.f }, {} ); + } + } + if( Dynamic->mdModel ) { + // main model + Render( Dynamic->mdModel, Dynamic->Material(), squaredistance ); + } + // optional attached models + for( auto *attachment : Dynamic->mdAttachments ) { + Render( attachment, Dynamic->Material(), squaredistance ); + } + // optional coupling adapters + Render_coupler_adapter( Dynamic, squaredistance, end::front ); + Render_coupler_adapter( Dynamic, squaredistance, end::rear ); + + // post-render cleanup + if (Dynamic->fShade > 0.0f) + { + // restore regular light level + walk_sunlight(); + } + break; + } + case rendermode::shadows: + { + if( Dynamic->mdLowPolyInt ) { + // low poly interior + Render( Dynamic->mdLowPolyInt, Dynamic->Material(), squaredistance ); + } + if( Dynamic->mdModel ) { + // main model + Render( Dynamic->mdModel, Dynamic->Material(), squaredistance ); + } + // optional attached models + for( auto *attachment : Dynamic->mdAttachments ) { + Render( attachment, Dynamic->Material(), squaredistance ); + } + // optional coupling adapters + Render_coupler_adapter( Dynamic, squaredistance, end::front ); + Render_coupler_adapter( Dynamic, squaredistance, end::rear ); + if( Dynamic->mdLoad ) { + // renderowanie nieprzezroczystego ładunku + Render( Dynamic->mdLoad, Dynamic->Material(), squaredistance, { 0.f, Dynamic->LoadOffset, 0.f }, {} ); + } + // post-render cleanup + break; + } + case rendermode::pickcontrols: + { + if (Dynamic->mdLowPolyInt) { + // low poly interior + Render(Dynamic->mdLowPolyInt, Dynamic->Material(), squaredistance); + } + break; + } + case rendermode::pickscenery: + default: + { + break; + } + } + + ::glPopMatrix(); + + model_ubs.future = future_stack; + + // TODO: check if this reset is needed. In theory each object should render all parts based on its own instance data anyway? + if (Dynamic->btnOn) + Dynamic->TurnOff(); // przywrócenie domyślnych pozycji submodeli + + return true; +} + +// rendering kabiny gdy jest oddzielnym modelem i ma byc wyswietlana +bool scene_walk::Render_cab(TDynamicObject const *Dynamic, float const Lightlevel, bool const Alpha) +{ + + if (Dynamic == nullptr) + { + + TSubModel::iInstance = 0; + TSubModel::iVariantSeed = 0; + return false; + } + + TSubModel::iInstance = reinterpret_cast(Dynamic); + TSubModel::iVariantSeed = Dynamic->variant_seed(); + + if ((true == FreeFlyModeFlag) || (false == Dynamic->bDisplayCab) || (Dynamic->mdKabina == Dynamic->mdModel)) + { + // ABu: Rendering kabiny jako ostatniej, zeby bylo widac przez szyby, tylko w widoku ze srodka + return false; + } + + if (Dynamic->mdKabina) + { // bo mogła zniknąć przy przechodzeniu do innego pojazdu + // setup shared by all render paths + ::glPushMatrix(); + + auto const originoffset = Dynamic->GetPosition() - m_renderpass.pass_camera.position(); + ::glTranslated(originoffset.x, originoffset.y, originoffset.z); + ::glMultMatrixd(glm::value_ptr(Dynamic->mMatrix)); + + switch (m_renderpass.draw_mode) + { + case rendermode::color: + { + // render path specific setup: + if (Dynamic->fShade > 0.0f) + { + // change light level based on light level of the occupied track + walk_sunlight(Dynamic->fShade); + } + + walk_ambient_save(); + auto const luminance { Global.fLuminance * ( Dynamic->fShade > 0.0f ? Dynamic->fShade : 1.0f ) }; + if( Lightlevel > 0.f ) { + // crude way to light the cabin, until we have something more complete in place. with the lamp of the cab among the + // lights a share of it is kept for the light which bounces around the cab + auto const share { m_cablamplit ? Global.gfx_lights_cab_bounce : 1.f }; + walk_ambient_add( ( Dynamic->InteriorLight * Lightlevel ) * std::clamp( 1.25f - (float)luminance, 0.f, 1.f ) * share ); + } + + // render + if (true == Alpha) + { + // translucent parts + Render_Alpha(Dynamic->mdKabina, Dynamic->Material(), 0.0); + } + else + { + // opaque parts + Render(Dynamic->mdKabina, Dynamic->Material(), 0.0); + } + // post-render restore + walk_ambient_restore(); + walk_sunlight(); + + break; + } + case rendermode::shadows: + { + if( true == Alpha ) { + // translucent parts + Render_Alpha( Dynamic->mdKabina, Dynamic->Material(), 0.0 ); + } + else { + // opaque parts + Render( Dynamic->mdKabina, Dynamic->Material(), 0.0 ); + } + break; + } + case rendermode::pickcontrols: + { + Render(Dynamic->mdKabina, Dynamic->Material(), 0.0); + break; + } + default: + { + break; + } + } + // post-render restore + ::glPopMatrix(); + } + + return true; +} + +bool scene_walk::Render(TModel3d *Model, material_data const *Material, float const Squaredistance) +{ + + auto alpha = (Material != nullptr ? Material->textures_alpha : 0x30300030); + alpha ^= 0x0F0F000F; // odwrócenie flag tekstur, aby wyłapać nieprzezroczyste + if (0 == (alpha & Model->iFlags & 0x1F1F001F)) + { + // czy w ogóle jest co robić w tym cyklu? + return false; + } + + Model->Root->fSquareDist = Squaredistance; // zmienna globalna! + + // setup + Model->Root->ReplacableSet((Material != nullptr ? Material->replacable_skins : nullptr), alpha); + + Model->Root->pRoot = Model; + + // render + Render(Model->Root); + + // post-render cleanup + + return true; +} + +bool scene_walk::Render(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle) +{ + ::glPushMatrix(); + ::glTranslated(Position.x, Position.y, Position.z); + if (Angle.y != 0.0) + ::glRotated(Angle.y, 0.f, 1.f, 0.f); + if (Angle.x != 0.0) + ::glRotated(Angle.x, 1.f, 0.f, 0.f); + if (Angle.z != 0.0) + ::glRotated(Angle.z, 0.f, 0.f, 1.f); + + auto const result = Render(Model, Material, Squaredistance); + + ::glPopMatrix(); + + return result; +} + +void scene_walk::Render(TSubModel *Submodel) +{ + glDebug("Render TSubModel"); + + if ((Submodel->iVisible) && (TSubModel::fSquareDist >= Submodel->fSquareMinDist) && (TSubModel::fSquareDist < Submodel->fSquareMaxDist) && (false == m_previewing || Submodel->fVisible > 0.f)) + { + glm::mat4 future_stack = model_ubs.future; + + if (Submodel->iFlags & 0xC000) + { + ::glPushMatrix(); + if (Submodel->fMatrix) + ::glMultMatrixf(Submodel->fMatrix->readArray()); + if (Submodel->b_aAnim != TAnimType::at_None) + { + Submodel->RaAnimation(Submodel->b_aAnim); + + glm::mat4 mv = OpenGLMatrices.data(GL_MODELVIEW); + model_ubs.future *= (mv * Submodel->future_transform) * glm::inverse(mv); + // the placement may be different in the next frame: the walk isn't kept (Record_Indirect_Draws()) + m_indirectchanging = true; + } + } + + if (Submodel->eType < TP_ROTATOR) + { + // renderowanie obiektów OpenGL + if (Submodel->iAlpha & Submodel->iFlags & 0x1F) + { + // rysuj gdy element nieprzezroczysty + + // debug data + ++m_renderpass.draw_stats.submodels; + ++m_renderpass.draw_stats.drawcalls; + + switch (m_renderpass.draw_mode) + { + case rendermode::color: + case rendermode::reflections: + { + // material configuration: + // transparency hack + if (Submodel->fVisible < 1.0f) + walk_blending(true); + + // textures... + if (Submodel->m_material < 0) + { // zmienialne skóry + walk_material(submodel_material(Submodel), Submodel); + } + else + { + // również 0 + walk_material(Submodel->m_material, Submodel); + } + + // ...luminance + auto const isemissive { ( Submodel->f4Emision.a > 0.f ) && ( Global.fLuminance < Submodel->fLight ) }; + if( ( Submodel->f4Emision.a > 0.f ) && ( Submodel->fLight > -1.f ) ) + { + // lit or not by the luminance of the moment: the walk isn't kept (Record_Indirect_Draws()) + m_indirectchanging = true; + } + if (isemissive) + model_ubs.emission = Submodel->f4Emision.a; + + // main draw call + walk_draw(Submodel->m_geometry.handle); + + // post-draw reset + model_ubs.emission = 0.0f; + if (Submodel->fVisible < 1.0f) + walk_blending(false); + + break; + } + case rendermode::shadows: + { + // skip if the shadow caster rank is too low for currently set threshold + // or if the submodel is hidden (colour pass only fades it out, e.g. inactive seasonal variant) + if( ( walk_material_data( Submodel ).shadow_rank > Global.gfx_shadow_rank_cutoff ) + || ( Submodel->fVisible <= 0.f ) ) + { + --m_renderpass.draw_stats.submodels; + --m_renderpass.draw_stats.drawcalls; + break; + } + + if (Submodel->m_material < 0) + { // zmienialne skóry + walk_shadow_material(submodel_material(Submodel)); + } + else + { + // również 0 + walk_shadow_material(Submodel->m_material); + } + walk_draw(Submodel->m_geometry.handle); + break; + } + case rendermode::pickscenery: + { + walk_program( walk_shader::pick ); + walk_draw(Submodel->m_geometry.handle); + break; + } + case rendermode::pickcontrols: + { + if (Submodel->screen_touch_list) { + // touch screen gradient + walk_program( walk_shader::pick_surface ); + model_ubs.param[0] = glm::vec4(1.0f - m_picksurfaceitems.size() / 255.0f, 0.0f, 0.0f, 1.0f); + m_picksurfaceitems.emplace_back(Submodel); + } + else { + // control picking applies individual colour for each submodel + walk_program( walk_shader::pick ); + m_pickcontrolsitems.emplace_back(Submodel); + model_ubs.param[0] = glm::vec4(pick_color(m_pickcontrolsitems.size()), 1.0f); + } + + walk_draw(Submodel->m_geometry.handle); + break; + } + default: + { + break; + } + } + } + } + else if (Submodel->eType == TP_STARS) + { + + switch (m_renderpass.draw_mode) + { + // colour points are only rendered in colour mode(s) + case rendermode::color: + case rendermode::reflections: + { + m_indirectchanging = true; + if (Global.fLuminance < Submodel->fLight) + { + walk_material(Submodel->m_material, Submodel); + + // main draw call + model_ubs.param[1].x = 8.0f; + + walk_draw(Submodel->m_geometry.handle); + } + break; + } + default: + { + break; + } + } + } + if (Submodel->Child != nullptr) + if (Submodel->iAlpha & Submodel->iFlags & 0x001F0000) + Render(Submodel->Child); + + if (Submodel->iFlags & 0xC000) + { + model_ubs.future = future_stack; + ::glPopMatrix(); + } + } + /* + if( Submodel->b_Anim < at_SecondsJump ) + Submodel->b_Anim = at_None; // wyłączenie animacji dla kolejnego użycia subm + */ + if (Submodel->Next) + if (Submodel->iAlpha & Submodel->iFlags & 0x1F000000) + Render(Submodel->Next); // dalsze rekurencyjnie +} + +void scene_walk::Render(TTrack *Track) +{ + if ((Track->m_material1 == 0) && (Track->m_material2 == 0) && (Track->eType != tt_Switch || Track->SwitchExtension->m_material3 == 0)) + { + return; + } + if (false == Track->m_visible) + { + return; + } + + ++m_renderpass.draw_stats.paths; + ++m_renderpass.draw_stats.drawcalls; + + switch (m_renderpass.draw_mode) + { + // single path pieces are rendererd in pick scenery mode only + case rendermode::pickscenery: + { + walk_program( walk_shader::pick ); + m_picksceneryitems.emplace_back( Track ); + + walk_draw(std::begin(Track->Geometry1), std::end(Track->Geometry1)); + walk_draw(std::begin(Track->Geometry2), std::end(Track->Geometry2)); + for (auto &tread : Track->GeometryTreads) + walk_draw(std::begin(tread.geometry), std::end(tread.geometry)); + if (Track->eType == tt_Switch) + walk_draw(Track->SwitchExtension->Geometry3); + break; + } + default: + { + break; + } + } +} + +// experimental, does track rendering in two passes, to take advantage of reduced texture switching +void scene_walk::Render(scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last) +{ + + // setup + switch (m_renderpass.draw_mode) + { + case rendermode::shadows: + { + // NOTE: roads-based platforms tend to miss parts of shadows if rendered with either back or front culling + walk_cull_face(false); + break; + } + default: + { + break; + } + } + + // geometry of neighbouring tracks which share the material is gathered and drawn together. + // only neighbours are combined, the tracks are still drawn in their order in the cell + material_handle batchmaterial{null_handle}; + auto const flush_batch = [&]() + { + if (false == m_pathbatch.empty()) + { + walk_draw(std::begin(m_pathbatch), std::end(m_pathbatch)); + m_pathbatch.clear(); + } + batchmaterial = null_handle; + }; + // returns: true if the material starts a new batch, false if it continues the current one + auto const begin_batch = [&](material_handle const Material) + { + if (Material == batchmaterial) + { + return false; + } + flush_batch(); + walk_material(Material); + batchmaterial = Material; + return true; + }; + + // TODO: render auto generated trackbeds together with regular trackbeds in pass 1, and all rails in pass 2 + // first pass, material 1 + for (auto first{First}; first != Last; ++first) + { + + auto const track{*first}; + + if (track->m_material1 == 0) + { + continue; + } + if (false == track->m_visible) + { + continue; + } + + ++m_renderpass.draw_stats.paths; + ++m_renderpass.draw_stats.drawcalls; + + switch (m_renderpass.draw_mode) + { + case rendermode::color: + case rendermode::reflections: + { + if (track->eEnvironment != e_flat) + { + // track with light level of its own is drawn on its own + flush_batch(); + walk_environment(track->eEnvironment); + walk_material(track->m_blendmaterial1 != null_handle ? track->m_blendmaterial1 : track->m_material1); + walk_draw(std::begin(track->Geometry1), std::end(track->Geometry1)); + // restore default lighting + walk_environment(); + break; + } + if (false == begin_batch(track->m_blendmaterial1 != null_handle ? track->m_blendmaterial1 : track->m_material1)) + { + // the track is drawn with the call of its predecessor + --m_renderpass.draw_stats.drawcalls; + } + m_pathbatch.insert(std::end(m_pathbatch), std::begin(track->Geometry1), std::end(track->Geometry1)); + break; + } + case rendermode::shadows: + { + if ((std::abs(track->fTexHeight1) < 0.35f) || (track->iCategoryFlag != 2)) + { + // shadows are only calculated for high enough roads, typically meaning track platforms + --m_renderpass.draw_stats.paths; + --m_renderpass.draw_stats.drawcalls; + continue; + } + if( walk_material_data( track->m_material1 ).shadow_rank > Global.gfx_shadow_rank_cutoff ) { + // skip if the shadow caster rank is too low for currently set threshold + --m_renderpass.draw_stats.paths; + --m_renderpass.draw_stats.drawcalls; + continue; + } + walk_shadow_material(track->m_material1); + walk_draw(std::begin(track->Geometry1), std::end(track->Geometry1)); + break; + } + case rendermode::pickscenery: // pick scenery mode uses piece-by-piece approach + case rendermode::pickcontrols: + default: + { + break; + } + } + } + flush_batch(); + // second pass, material 2 + for (auto first{First}; first != Last; ++first) + { + auto const track{*first}; + + if (track->m_material2 == 0) + { + continue; + } + if (false == track->m_visible) + { + continue; + } + if (track->eType == tt_Normal && editor_ballast_hidden(track, m_renderpass.draw_mode == rendermode::color)) + { + continue; + } + + switch (m_renderpass.draw_mode) + { + case rendermode::color: + case rendermode::reflections: + { + if (track->eEnvironment != e_flat) + { + flush_batch(); + walk_environment(track->eEnvironment); + walk_material(track->m_blendmaterial2 != null_handle ? track->m_blendmaterial2 : track->m_material2); + walk_draw(std::begin(track->Geometry2), std::end(track->Geometry2)); + // restore default lighting + walk_environment(); + break; + } + begin_batch(track->m_blendmaterial2 != null_handle ? track->m_blendmaterial2 : track->m_material2); + m_pathbatch.insert(std::end(m_pathbatch), std::begin(track->Geometry2), std::end(track->Geometry2)); + break; + } + case rendermode::shadows: + { + if ((std::abs(track->fTexHeight1) < 0.35f) || ((track->iCategoryFlag == 1) && (track->eType != tt_Normal))) + { + // shadows are only calculated for high enough trackbeds + continue; + } + if( walk_material_data( track->m_material2 ).shadow_rank > Global.gfx_shadow_rank_cutoff ) { + // skip if the shadow caster rank is too low for currently set threshold + continue; + } + walk_shadow_material(track->m_material2); + walk_draw(std::begin(track->Geometry2), std::end(track->Geometry2)); + break; + } + case rendermode::pickscenery: // pick scenery mode uses piece-by-piece approach + case rendermode::pickcontrols: + default: + { + break; + } + } + } + flush_batch(); + + // third pass, material 3 + for (auto first{First}; first != Last; ++first) + { + + auto const track{*first}; + + if (track->eType != tt_Switch) + { + continue; + } + if (track->SwitchExtension->m_material3 == 0) + { + continue; + } + if (false == track->m_visible) + { + continue; + } + if (editor_ballast_hidden(track, m_renderpass.draw_mode == rendermode::color)) + { + continue; + } + + switch (m_renderpass.draw_mode) + { + case rendermode::color: + case rendermode::reflections: + { + if (track->eEnvironment != e_flat) + { + flush_batch(); + walk_environment(track->eEnvironment); + walk_material(track->m_blendmaterial3 != null_handle ? track->m_blendmaterial3 : track->SwitchExtension->m_material3); + walk_draw(track->SwitchExtension->Geometry3); + // restore default lighting + walk_environment(); + break; + } + begin_batch(track->m_blendmaterial3 != null_handle ? track->m_blendmaterial3 : track->SwitchExtension->m_material3); + m_pathbatch.emplace_back(track->SwitchExtension->Geometry3); + break; + } + case rendermode::shadows: + { + if ((std::abs(track->fTexHeight1) < 0.35f) || ((track->iCategoryFlag == 1) && (track->eType != tt_Normal))) + { + // shadows are only calculated for high enough trackbeds + continue; + } + if( walk_material_data( track->m_material2 ).shadow_rank > Global.gfx_shadow_rank_cutoff ) { + // skip if the shadow caster rank is too low for currently set threshold + continue; + } + walk_shadow_material(track->SwitchExtension->m_material3); + walk_draw(track->SwitchExtension->Geometry3); + break; + } + case rendermode::pickscenery: // pick scenery mode uses piece-by-piece approach + case rendermode::pickcontrols: + default: + { + break; + } + } + } + flush_batch(); + + // cut faces of rails, in matte variants of the rail materials + if ((m_renderpass.draw_mode == rendermode::color) || (m_renderpass.draw_mode == rendermode::reflections)) + { + for (auto first{First}; first != Last; ++first) + { + auto const track{*first}; + + if (false == track->m_visible) + { + continue; + } + std::vector> parts{{track->m_materialcut1, &track->GeometryCut1}, {track->m_materialcut2, &track->GeometryCut2}}; + for (auto &tread : track->GeometryTreads) + { + parts.emplace_back(tread.material, &tread.geometry); + parts.emplace_back(tread.matte, &tread.cuts); + } + for (auto const &cuts : parts) + { + if ((cuts.first == null_handle) || cuts.second->empty()) + { + continue; + } + if (track->eEnvironment != e_flat) + { + flush_batch(); + walk_environment(track->eEnvironment); + walk_material(cuts.first); + walk_draw(std::begin(*cuts.second), std::end(*cuts.second)); + walk_environment(); + continue; + } + begin_batch(cuts.first); + m_pathbatch.insert(std::end(m_pathbatch), std::begin(*cuts.second), std::end(*cuts.second)); + } + } + flush_batch(); + } + + // fourth pass: per-track sleeper models (sleepermodel optional directive). + // drawn after rails/trackbeds so depth pre-pass culling is favourable, and only in passes + // where Render_Sleepers actually does work (it gates itself on draw mode / distance). + switch( m_renderpass.draw_mode ) { + case rendermode::color: + case rendermode::reflections: { + for( auto first { First }; first != Last; ++first ) { + auto *track = *first; + if( false == track->m_visible ) { continue; } + if( false == track->m_sleeper_enabled ) { continue; } + // a track with the sleepers and timbers of eu07.ini lays its sleeper model there + if( track->m_sleepers3d.active ) { continue; } + // the backend may draw them itself (the gpu culling of the opengl renderer) + if( walk_queue_sleepers( track ) ) { continue; } + Render_Sleepers( track ); + } + if( m_renderpass.draw_mode == rendermode::color ) { + Render_Timbers( First, Last ); + } + break; + } + case rendermode::shadows: { + Render_Timbers( First, Last ); + break; + } + default: + break; + } + + // post-render reset + switch (m_renderpass.draw_mode) + { + case rendermode::shadows: + { + // restore standard face cull mode + walk_cull_face(true); + break; + } + default: + { + break; + } + } +} + +void scene_walk::Render(TMemCell *Memcell) +{ + + ::glPushMatrix(); + auto const position = Memcell->location() - m_renderpass.pass_camera.position(); + ::glTranslated(position.x, position.y + 0.5, position.z); + + switch (m_renderpass.draw_mode) + { + case rendermode::color: + { + break; + } + case rendermode::shadows: + case rendermode::pickscenery: + { + break; + } + case rendermode::reflections: + case rendermode::pickcontrols: + { + break; + } + default: + { + break; + } + } + + ::glPopMatrix(); +} + +void scene_walk::Render_Alpha(scene::basic_region *Region) +{ + + // sort the nodes based on their distance to viewer + std::sort(std::begin(m_cellqueue), std::end(m_cellqueue), [](distancecell_pair const &Left, distancecell_pair const &Right) { return (Left.first) < (Right.first); }); + + Render_Alpha(std::rbegin(m_cellqueue), std::rend(m_cellqueue)); +} + +void scene_walk::Render_Alpha(cell_sequence::reverse_iterator First, cell_sequence::reverse_iterator Last) +{ + + // NOTE: this method is launched only during color pass therefore we don't bother with mode test here + // first pass draws elements which we know are located in section banks, to reduce vbo switching + { + auto first{First}; + while (first != Last) + { + + auto const *cell = first->second; + + if (false == cell->m_shapestranslucent.empty()) + { + // since all shapes of the cell share center point we can optimize out a few calls here + ::glPushMatrix(); + auto const originoffset{cell->m_area.center - m_renderpass.pass_camera.position()}; + ::glTranslated(originoffset.x, originoffset.y, originoffset.z); + // render + // NOTE: we can reuse the method used to draw opaque geometry + for (auto const &shape : cell->m_shapestranslucent) + { + Render(shape, false); + } + // post-render cleanup + ::glPopMatrix(); + } + + ++first; + } + } + // second pass draws elements with their own vbos + { + auto first{First}; + while (first != Last) + { + + auto const *cell = first->second; + + // translucent parts of instanced models + for (auto *instance : cell->m_instancetranslucent) + { + Render_Alpha(instance); + } + // translucent parts of vehicles + for (auto *path : cell->m_paths) + { + for (auto *dynamic : path->Dynamics) + { + Render_Alpha(dynamic); + } + } + + ++first; + } + } + // third pass draws the wires; + // wires use section vbos, but for the time being we want to draw them at the very end + { + auto first{First}; + while (first != Last) + { + + auto const *cell = first->second; + + if ((false == cell->m_traction.empty() || (false == cell->m_lines.empty()))) + { + // since all shapes of the cell share center point we can optimize out a few calls here + ::glPushMatrix(); + auto const originoffset{cell->m_area.center - m_renderpass.pass_camera.position()}; + ::glTranslated(originoffset.x, originoffset.y, originoffset.z); + walk_material(null_handle); + // render + for (auto *traction : cell->m_traction) + { + Render_Alpha(traction); + } + for (auto &lines : cell->m_lines) + { + Render_Alpha(lines); + } + // post-render cleanup + ::glPopMatrix(); + } + + ++first; + } + } +} + +void scene_walk::Render_Alpha(TAnimModel *Instance) +{ + + if (false == Instance->m_visible) { return; } + if( false == m_renderpass.pass_camera.visible( Instance->m_area ) ) { return; } + + double distancesquared; + switch (m_renderpass.draw_mode) + { + case rendermode::shadows: + { + // 'camera' for the light pass is the light source, but we need to draw what the 'real' camera sees + distancesquared = glm::length2((Instance->location() - m_renderpass.viewport_camera.position()) / (double)Global.ZoomFactor) / Global.fDistanceFactor; + break; + } + default: + { + distancesquared = glm::length2((Instance->location() - m_renderpass.pass_camera.position()) / (double)Global.ZoomFactor) / Global.fDistanceFactor; + break; + } + } + if ((distancesquared < Instance->m_rangesquaredmin) || (distancesquared >= Instance->m_rangesquaredmax)) + { + return; + } + // crude way to reject early items too far to affect the output (mostly relevant for shadow passes) + auto const drawdistancethreshold{ m_renderpass.draw_range + 250 }; + if( distancesquared > sq(drawdistancethreshold) ) { + return; + } + // second stage visibility cull, reject modelstoo far away to be noticeable + auto const radiussquared { Instance->radius() * Instance->radius() }; + if( radiussquared * Global.ZoomFactor / distancesquared < 0.003 * 0.003 ) { + return; + } + + Instance->RaPrepare(); + if (Instance->pModel) + { + auto const &scale = Instance->Scale(); + bool const scaled = ( scale.x != 1.0f || scale.y != 1.0f || scale.z != 1.0f ); + if( scaled ) { + // scaled instance: build the per-instance transform locally and call + // the distance-only Render_Alpha so we can inject glScalef before + // the submodel walk. Mirror of the opaque path in Render(TAnimModel*). + ::glPushMatrix(); + auto const Position = Instance->location() - m_renderpass.pass_camera.position(); + auto const Angle = Instance->vAngle; + ::glTranslated( Position.x, Position.y, Position.z ); + if( Angle.y != 0.0 ) ::glRotated( Angle.y, 0.f, 1.f, 0.f ); + if( Angle.x != 0.0 ) ::glRotated( Angle.x, 1.f, 0.f, 0.f ); + if( Angle.z != 0.0 ) ::glRotated( Angle.z, 0.f, 0.f, 1.f ); + ::glScalef( scale.x, scale.y, scale.z ); + Render_Alpha( Instance->pModel, Instance->Material(), distancesquared ); + ::glPopMatrix(); + } + else { + Render_Alpha(Instance->pModel, Instance->Material(), distancesquared, Instance->location() - m_renderpass.pass_camera.position(), Instance->vAngle); + } + } +} + +void scene_walk::Render_Alpha(TTraction *Traction) +{ + glDebug("Render_Alpha TTraction"); + + auto const distancesquared { glm::length2( ( Traction->location() - m_renderpass.pass_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor }; + if ((distancesquared < Traction->m_rangesquaredmin) || (distancesquared >= Traction->m_rangesquaredmax)) + { + return; + } + + if (false == Traction->m_visible) + { + return; + } + // rysuj jesli sa druty i nie zerwana + if ((Traction->Wires == 0) || (true == TestFlag(Traction->DamageFlag, 128))) + { + return; + } + // setup + auto const distance{static_cast(std::sqrt(distancesquared))}; + auto const linealpha = 20.f * Traction->WireThickness / std::max(0.5f * Traction->radius() + 1.f, distance - (0.5f * Traction->radius())); + + // render + + // McZapkie-261102: kolor zalezy od materialu i zasniedzenia + model_ubs.param[0] = glm::vec4(Traction->wire_color(), glm::min(1.0f, linealpha)); + + if (m_renderpass.draw_mode == rendermode::shadows) + walk_shadow_material(null_handle); + else + walk_program( walk_shader::line ); + + walk_draw(Traction->m_geometry); + + // debug data + ++m_renderpass.draw_stats.traction; + ++m_renderpass.draw_stats.drawcalls; +} + +void scene_walk::Render_Alpha(scene::lines_node const &Lines) +{ + glDebug("Render_Alpha scene::lines_node"); + + auto const &data{Lines.data()}; + + auto const distancesquared { glm::length2( ( data.area.center - m_renderpass.pass_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor }; + if ((distancesquared < data.rangesquared_min) || (distancesquared >= data.rangesquared_max)) + { + return; + } + // setup + auto const distance{static_cast(std::sqrt(distancesquared))}; + auto const linealpha = + (data.line_width > 0.f ? 10.f * data.line_width / std::max(0.5f * data.area.radius + 1.f, distance - (0.5f * data.area.radius)) : 1.f); // negative width means the lines are always opague + + model_ubs.param[0] = glm::vec4(glm::vec3(data.lighting.diffuse * walk_sun_ambient()), glm::min(1.0f, linealpha)); + + if (m_renderpass.draw_mode == rendermode::shadows) + walk_shadow_material(null_handle); + else + walk_program( walk_shader::line ); + walk_draw(data.geometry); + + ++m_renderpass.draw_stats.lines; + ++m_renderpass.draw_stats.drawcalls; +} + +bool scene_walk::Render_Alpha(TDynamicObject *Dynamic) +{ + if (!Global.render_cab && Global.pCamera.m_owner == Dynamic) + return false; + + if (false == Dynamic->renderme) + { + return false; + } + + // setup + TSubModel::iInstance = (size_t)Dynamic; //żeby nie robić cudzych animacji + TSubModel::iVariantSeed = Dynamic->variant_seed(); + glm::dvec3 const originoffset = Dynamic->vPosition - m_renderpass.pass_camera.position(); + // lod visibility ranges are defined for base (x 1.0) viewing distance. for render we adjust them for actual range multiplier and zoom + float squaredistance; + switch (m_renderpass.draw_mode) + { + case rendermode::shadows: + default: + { + squaredistance = glm::length2(glm::vec3{originoffset} / Global.ZoomFactor); + break; + } + } + Dynamic->ABuLittleUpdate(squaredistance); // ustawianie zmiennych submodeli dla wspólnego modelu + + glm::mat4 future_stack = model_ubs.future; + + glm::mat4 mv = OpenGLMatrices.data(GL_MODELVIEW); + model_ubs.future *= glm::translate(mv, glm::vec3(Dynamic->get_future_movement())) * glm::inverse(mv); + // the velocity it leaves in the colour pass keeps the motion blur on (Render_pass()) + if ((m_renderpass.draw_mode == rendermode::color) && (Dynamic->get_future_movement() != glm::dvec3{0.0})) + m_vehiclesmoving = true; + + ::glPushMatrix(); + + ::glTranslated(originoffset.x, originoffset.y, originoffset.z); + ::glMultMatrixd(glm::value_ptr(Dynamic->mMatrix)); + + if (Dynamic->fShade > 0.0f) + { + // change light level based on light level of the occupied track + walk_sunlight(Dynamic->fShade); + } + + // render + if( Dynamic->mdLowPolyInt ) { + // low poly interior + Render_Alpha( Dynamic->mdLowPolyInt, Dynamic->Material(), squaredistance ); + } + if( Dynamic->mdModel ) { + // main model + Render_Alpha( Dynamic->mdModel, Dynamic->Material(), squaredistance ); + } + // optional attached models + for( auto *attachment : Dynamic->mdAttachments ) { + Render_Alpha( attachment, Dynamic->Material(), squaredistance ); + } + // optional coupling adapters + Render_coupler_adapter( Dynamic, squaredistance, end::front, true ); + Render_coupler_adapter( Dynamic, squaredistance, end::rear, true ); + if( Dynamic->mdLoad ) { + // renderowanie nieprzezroczystego ładunku + Render_Alpha( Dynamic->mdLoad, Dynamic->Material(), squaredistance, { 0.f, Dynamic->LoadOffset, 0.f }, {} ); + } + + // post-render cleanup + if (Dynamic->fShade > 0.0f) + { + // restore regular light level + walk_sunlight(); + } + + ::glPopMatrix(); + + model_ubs.future = future_stack; + + if (Dynamic->btnOn) + Dynamic->TurnOff(); // przywrócenie domyślnych pozycji submodeli + + return true; +} + +bool scene_walk::Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance) +{ + + auto alpha = (Material != nullptr ? Material->textures_alpha : 0x30300030); + + if (0 == (alpha & Model->iFlags & 0x2F2F002F)) + { + // nothing to render + return false; + } + + Model->Root->fSquareDist = Squaredistance; // zmienna globalna! + + // setup + Model->Root->ReplacableSet((Material != nullptr ? Material->replacable_skins : nullptr), alpha); + + Model->Root->pRoot = Model; + + // render + Render_Alpha(Model->Root); + + // post-render cleanup + + return true; +} + +bool scene_walk::Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle) +{ + ::glPushMatrix(); + ::glTranslated(Position.x, Position.y, Position.z); + if (Angle.y != 0.0) + ::glRotated(Angle.y, 0.f, 1.f, 0.f); + if (Angle.x != 0.0) + ::glRotated(Angle.x, 1.f, 0.f, 0.f); + if (Angle.z != 0.0) + ::glRotated(Angle.z, 0.f, 0.f, 1.f); + + auto const result = Render_Alpha(Model, Material, Squaredistance); // position is effectively camera offset + + ::glPopMatrix(); + + return result; +} + +// characters of a text display, laid out in a row. the text can be encoded in utf-8 or in windows-1250 +void scene_walk::Render_Alpha_text(TSubModel *Submodel) +{ + if (TSubModel::pasText == nullptr) + { + return; + } + for (auto *p : Submodel->text_letters(*TSubModel::pasText)) + { // translucent only, for the time being + Render_Alpha(p); + if (p->fMatrix) + ::glMultMatrixf(p->fMatrix->readArray()); // move on to the place of the next character + } +} + +void scene_walk::Render_Alpha(TSubModel *Submodel) +{ + // renderowanie przezroczystych przez DL + if ((Submodel->iVisible) && (TSubModel::fSquareDist >= Submodel->fSquareMinDist) && (TSubModel::fSquareDist < Submodel->fSquareMaxDist) && (false == m_previewing || Submodel->fVisible > 0.f)) + { + glm::mat4 future_stack = model_ubs.future; + + if (Submodel->iFlags & 0xC000) + { + ::glPushMatrix(); + if (Submodel->fMatrix) + ::glMultMatrixf(Submodel->fMatrix->readArray()); + if (Submodel->b_aAnim != TAnimType::at_None) + { + Submodel->RaAnimation(Submodel->b_aAnim); + + glm::mat4 mv = OpenGLMatrices.data(GL_MODELVIEW); + model_ubs.future *= (mv * Submodel->future_transform) * glm::inverse(mv); + } + } + + if (Submodel->eType < TP_ROTATOR) + { + // renderowanie obiektów OpenGL + if (Submodel->iAlpha & Submodel->iFlags & 0x2F) + { + // rysuj gdy element przezroczysty + + // debug data + ++m_renderpass.draw_stats.submodels; + ++m_renderpass.draw_stats.drawcalls; + + switch (m_renderpass.draw_mode) + { + case rendermode::color: + { + // material configuration: + // textures... + if (Submodel->m_material < 0) + { // zmienialne skóry + walk_material(submodel_material(Submodel), Submodel); + } + else + { + walk_material(Submodel->m_material, Submodel); + } + // ...luminance + + auto const isemissive { ( Submodel->f4Emision.a > 0.f ) && ( Global.fLuminance < Submodel->fLight ) }; + if (isemissive) + model_ubs.emission = Submodel->f4Emision.a; + + // main draw call + walk_draw(Submodel->m_geometry.handle); + + model_ubs.emission = 0.0f; + break; + } + case rendermode::shadows: + { + // skip if the shadow caster rank is too low for currently set threshold + // or if the submodel is hidden (colour pass only fades it out, e.g. inactive seasonal variant) + if( ( walk_material_data( Submodel ).shadow_rank > Global.gfx_shadow_rank_cutoff ) + || ( Submodel->fVisible <= 0.f ) ) + { + --m_renderpass.draw_stats.submodels; + --m_renderpass.draw_stats.drawcalls; + break; + } + + if (Submodel->m_material < 0) + { // zmienialne skóry + walk_shadow_material(submodel_material(Submodel)); + } + else + { + walk_shadow_material(Submodel->m_material); + } + walk_draw(Submodel->m_geometry.handle); + break; + } + default: + { + break; + } + } + } + } + else if (Submodel->eType == TP_FREESPOTLIGHT) + { + // NOTE: we're forced here to redo view angle calculations etc, because this data isn't instanced but stored along with the single mesh + // TODO: separate instance data from reusable geometry + auto const &modelview = OpenGLMatrices.data(GL_MODELVIEW); + auto const lightcenter = modelview * glm::mix(glm::vec4(0.f, 0.f, -0.05f, 1.f), glm::vec4(0.f, 0.f, -0.25f, 1.f), + static_cast(TSubModel::fSquareDist / Submodel->fSquareMaxDist)); // pozycja punktu świecącego względem kamery + Submodel->fCosViewAngle = glm::dot(glm::normalize(modelview * glm::vec4(0.f, 0.f, -1.f, 1.f) - lightcenter), glm::normalize(-lightcenter)); + + if (Global.fLuminance < Submodel->fLight || Global.Overcast > 1.0f) + { + if (Submodel->fCosViewAngle > Submodel->fCosFalloffAngle) + { + // only bother if the viewer is inside the visibility cone + // luminosity at night is at level of ~0.1, so the overall resulting transparency in clear conditions is ~0.5 at full 'brightness' + auto glarelevel{std::clamp(std::max(0.6f - Global.fLuminance, // reduce the glare in bright daylight + Global.Overcast - 1.f), // ensure some glare in rainy/foggy conditions + 0.f, 1.f)}; + // view angle attenuation + float const anglefactor{std::clamp((Submodel->fCosViewAngle - Submodel->fCosFalloffAngle) / (Submodel->fCosHotspotAngle - Submodel->fCosFalloffAngle), 0.f, 1.f)}; + + glarelevel *= anglefactor; + + if (glarelevel > 0.0f) + { + walk_glare(true); + + ::glPushMatrix(); + ::glLoadIdentity(); // macierz jedynkowa + ::glTranslatef(lightcenter.x, lightcenter.y, lightcenter.z); // początek układu zostaje bez zmian + ::glRotated(std::atan2(lightcenter.x, lightcenter.z) * 180.0 / M_PI, 0.0, 1.0, 0.0); // jedynie obracamy w pionie o kąt + + auto const lightcolor = glm::vec3(Submodel->DiffuseOverride.r < 0.f ? // -1 indicates no override + Submodel->f4Diffuse : + Submodel->DiffuseOverride); + + walk_program( walk_shader::billboard ); + walk_texture(0, m_glaretexture); + model_ubs.param[0] = glm::vec4(glm::vec3(lightcolor), Submodel->fVisible * glarelevel); + + // main draw call + // drawn where the light itself was seen in the last frame (an occlusion query of the opengl renderer) + walk_glare_draw(Submodel, m_billboardgeometry); + + walk_glare(false); + + ::glPopMatrix(); + } + } + } + + if (Submodel->fCosViewAngle > Submodel->fCosFalloffAngle) + { + // kąt większy niż maksymalny stożek swiatła + float lightlevel = 1.f; // TODO, TBD: parameter to control light strength + // view angle attenuation + float const anglefactor = std::clamp((Submodel->fCosViewAngle - Submodel->fCosFalloffAngle) / (Submodel->fCosHotspotAngle - Submodel->fCosFalloffAngle), 0.f, 1.f); + lightlevel *= anglefactor; + + // distance attenuation. NOTE: since it's fixed pipeline with built-in gamma correction we're using linear attenuation + // we're capping how much effect the distance attenuation can have, otherwise the lights get too tiny at regular distances + float const distancefactor{std::max(0.5f, (Submodel->fSquareMaxDist - TSubModel::fSquareDist) / Submodel->fSquareMaxDist)}; + auto const pointsize{std::max(3.f, 5.f * distancefactor * anglefactor)}; + // additionally reduce light strength for farther sources in rain or snow + if (Global.Overcast > 0.75f) + { + float const precipitationfactor{std::lerp(std::lerp(1.f, 0.25f, std::clamp(Global.Overcast * 0.75f - 0.5f, 0.f, 1.f)), 1.f, distancefactor)}; + lightlevel *= precipitationfactor; + } + + if (lightlevel > 0.f) + { + ::glPushMatrix(); + ::glLoadIdentity(); + ::glTranslatef(lightcenter.x, lightcenter.y, lightcenter.z); // początek układu zostaje bez zmian + + // material configuration: + // limit impact of dense fog on the lights + auto const lightrange { std::max( 500, m_fogrange * 2 ) }; // arbitrary, visibility at least 500m + model_ubs.fog_density = 1.0 / lightrange; + + // main draw call + model_ubs.emission = 1.0f; + + auto lightcolor = glm::vec3(Submodel->DiffuseOverride.r < 0.f ? // -1 indicates no override + Submodel->f4Diffuse : + Submodel->DiffuseOverride); + + walk_program( walk_shader::freespot ); + + if (Global.Overcast > 1.0f) + { + // fake fog halo + float const fogfactor{std::lerp(1.5f, 1.f, std::clamp(Global.fFogEnd / 2000, 0.f, 1.f)) * std::max(1.f, Global.Overcast)}; + model_ubs.param[1].x = pointsize * fogfactor * 4.0f; + model_ubs.param[0] = glm::vec4(glm::vec3(lightcolor), Submodel->fVisible * std::min(1.f, lightlevel) * 0.5f); + + walk_draw(Submodel->m_geometry.handle); + } + model_ubs.param[1].x = pointsize * 4.0f; + model_ubs.param[0] = glm::vec4(glm::vec3(lightcolor), Submodel->fVisible * std::min(1.f, lightlevel)); + + walk_light_query(Submodel, true); + + walk_draw(Submodel->m_geometry.handle); + + walk_light_query(Submodel, false); + + // post-draw reset + model_ubs.emission = 0.0f; + model_ubs.fog_density = 1.0f / m_fogrange; + + ::glPopMatrix(); + } + } + } + + if (Submodel->Child != nullptr) + { + if (Submodel->eType == TP_TEXT) + { // tekst renderujemy w specjalny sposób, zamiast submodeli z łańcucha Child + Render_Alpha_text(Submodel); + } + else if (Submodel->iAlpha & Submodel->iFlags & 0x002F0000) + Render_Alpha(Submodel->Child); + } + + if (Submodel->iFlags & 0xC000) + { + model_ubs.future = future_stack; + ::glPopMatrix(); + } + } + /* + if( Submodel->b_aAnim < at_SecondsJump ) + Submodel->b_aAnim = at_None; // wyłączenie animacji dla kolejnego użycia submodelu + */ + if (Submodel->Next != nullptr) + if (Submodel->iAlpha & Submodel->iFlags & 0x2F000000) + Render_Alpha(Submodel->Next); +}; + +} // namespace gfx diff --git a/rendering/common/scenewalk.h b/rendering/common/scenewalk.h new file mode 100644 index 00000000..517c98c1 --- /dev/null +++ b/rendering/common/scenewalk.h @@ -0,0 +1,346 @@ +/* +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 walk of the scene shared by the renderers (stage N6 of claude/vulkan14-renderer-plan.md, decision K11): the decisions of +// what is drawn in a pass (the sections, cells, tracks, shapes, models, instances, vehicles, the cab, the translucent parts), +// in which order, with which material, transform and level of detail. a renderer derives from it and gets the draws through +// the walk_* methods: the opengl renderer makes them at once (the same gl calls in the same order as before the walk was +// shared), the vulkan renderer records them. what the backend may draw itself (the gpu culling of the opengl renderer) is +// asked about with the walk_* methods which do nothing by default. +// +// the walk keeps the state of the draw in model_ubs (the block of the model of the shaders) and the transform on the matrix +// stack of OpenGLMatrices (a cpu stack, despite the name) + +#pragma once + +#include "rendering/geometrybank.h" +#include "rendering/openglcamera.h" +#include "scene/scene.h" +#include "world/Track.h" +#include "world/TrackSleepers.h" +#include "model/material.h" +#include "gl/ubo.h" + +#include +#include +#include + +class TAnimModel; +class TDynamicObject; +class TModel3d; +class TSubModel; +class TTraction; +class TMemCell; +struct material_data; +struct lighting_data; + +namespace gfx +{ + +class scene_walk +{ + public: + virtual ~scene_walk() = default; + + protected: + // types + enum class rendermode + { + none, + color, + shadows, + reflections, + pickcontrols, + pickscenery + }; + + struct debug_stats + { + int dynamics{0}; + int models{0}; + int submodels{0}; + int paths{0}; + int traction{0}; + int shapes{0}; + int lines{0}; + int particles{0}; + int drawcalls{0}; + int instanced_drawcalls{0}; // drawcalls issued via instanced render path (one per TModel3d batch) + int instances{0}; // total instances drawn via instanced render path + int triangles{0}; + + debug_stats &operator+=(const debug_stats &Right) + { + dynamics += Right.dynamics; + models += Right.models; + submodels += Right.submodels; + paths += Right.paths; + traction += Right.traction; + shapes += Right.shapes; + lines += Right.lines; + particles += Right.particles; + drawcalls += Right.drawcalls; + instanced_drawcalls += Right.instanced_drawcalls; + instances += Right.instances; + return *this; + } + }; + + using section_sequence = std::vector; + using distancecell_pair = std::pair; + using cell_sequence = std::vector; + + struct renderpass_config + { + opengl_camera pass_camera; + opengl_camera viewport_camera; + rendermode draw_mode{rendermode::none}; + float draw_range{0.0f}; + // depth range of the projection of a color pass; the far plane is only the end of the range which is drawn, + // as the projection matrix itself has none + float znear{0.0f}; + float zfar{0.0f}; + debug_stats draw_stats; + }; + + // an instance of a bucket of a scene cell, queued to be drawn by Render_Instanced() + struct queued_instance + { + scene::basic_cell::instance_bucket const *bucket; + std::size_t index; // of the instance and its record in the bucket + }; + + // the programs of the draws which aren't drawn with the programs of their materials + enum class walk_shader + { + pick, // a colour for the picking of the scenery or the controls + pick_surface, // the gradient of a touch screen for the picking of the controls + line, // the wires of the traction and the lines + billboard, // the glare of a lamp + freespot // the point of light of a lamp + }; + + // how far the reflection pass draws the simplified versions of things, as if they were farther away + static int constexpr EU07_REFLECTIONFIDELITYOFFSET{250}; + + // the walk + void Render(scene::basic_region *Region); + void Render(section_sequence::iterator First, section_sequence::iterator Last); + void Render(cell_sequence::iterator First, cell_sequence::iterator Last); + void Render(scene::shape_node const &Shape, bool const Ignorerange); + void Render(TAnimModel *Instance); + // batched render path for many TAnimModel instances that share the same TModel3d. + // Caller guarantees every instance was instanceable when bucketed (no per-instance lights, + // no submodel animation, same replacable skins); the records of the instances tell which + // ones still are. Instances are individually frustum/distance culled by their records, the + // TAnimModel itself is read only for the first instance drawn. Each lod band drawn counts as + // one instanced_drawcall in draw_stats; the instances drawn are added to the instances counter. + // Bandcounts, when given, gets the number of instances drawn in each lod band. + void Render_Instanced(TModel3d *Model, std::vector const &Instances, std::vector *Bandcounts = nullptr); + // adds the bucketed instances of a scene cell to the frame-level buckets drawn by Render_Instanced(), + // less the ones their records show to be out of sight in the current render pass + void Queue_Instances(scene::basic_cell const *Cell); + // makes the records of the instances held by a bucket of a scene cell, with locations relative to Origin + void Update_Instance_Records(TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket, glm::dvec3 const &Origin); + // whether none of the instances of a bucket can pass the culling of the current pass, by the bounds of their records + bool Bucket_Culled(scene::basic_cell::instance_bucket const &Bucket) const; + // the sphere around the locations of the visible instances of a bucket, with a margin for the precision of the records + static scene::bounding_area Bucket_Area(scene::basic_cell::instance_bucket const &Bucket); + bool Render(TDynamicObject *Dynamic); + bool Render(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle); + bool Render(TModel3d *Model, material_data const *Material, float const Squaredistance); + void Render(TSubModel *Submodel); + void Render(TTrack *Track); + void Render(scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last); + // renders the per-track sleeper instances (TTrack::m_sleeper_local_transforms) via GPU instancing. + // caller must already have the camera-relative world-space transform set on the matrix stack. + // no-op if the track has no sleepermodel, Global.SleeperDistance is 0, or the camera is beyond + // Global.SleeperDistance from the track origin. + void Render_Sleepers(TTrack *Track); + // renders the sleepers and switch timbers of the tracks in range (TTrack::m_sleepers3d), laying them first where they're out of date. + // the parts of the model are drawn in instanced batches for the whole run of tracks, the runs of beams out of sight left out + void Render_Timbers(scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last); + bool Render_cab(TDynamicObject const *Dynamic, float const Lightlevel, bool const Alpha = false); + bool Render_interior(bool const Alpha = false); + bool Render_lowpoly(TDynamicObject *Dynamic, float const Squaredistance, bool const Setup, bool const Alpha = false); + bool Render_coupler_adapter(TDynamicObject *Dynamic, float const Squaredistance, int const End, bool const Alpha = false); + void Render(TMemCell *Memcell); + void Render_Alpha(scene::basic_region *Region); + void Render_Alpha(cell_sequence::reverse_iterator First, cell_sequence::reverse_iterator Last); + void Render_Alpha(TAnimModel *Instance); + void Render_Alpha(TTraction *Traction); + void Render_Alpha(scene::lines_node const &Lines); + bool Render_Alpha(TDynamicObject *Dynamic); + bool Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle); + bool Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance); + void Render_Alpha(TSubModel *Submodel); + void Render_Alpha_text(TSubModel *Submodel); // characters of a text display + + // the colour of the picking of the item of the index, and the index of a colour read back + static glm::vec3 pick_color(std::size_t const Index); + static std::size_t pick_index(glm::ivec3 const &Color); + // the range the top view of the editor draws the tracks to, and the parts of the scenery it leaves out + static float editor_track_view_range(float const Range, bool const Apply); + static bool editor_ballast_hidden(TTrack const *Track, bool const Colour); + static bool editor_far_tracks_only(bool const Apply, scene::basic_cell const *Cell, glm::dvec3 const &Camera, double const SceneryLimit); + // material assigned to the submodel, resolving replacable skins. null_handle if no skin set is bound + static material_handle submodel_material(TSubModel const *Submodel); + + // what the backend does with the draws of the walk + // binds the material for the colour passes: its program, its textures and its parameters in model_ubs (opacity, alpha_mult, + // the params of the submodel or of the lighting of a shape) + virtual void walk_material(material_handle const Material, TSubModel const *Submodel = nullptr, lighting_data const *Lighting = nullptr) = 0; + // the same for the shadow passes: the program of the shadows, with the alpha test of the diffuse texture when there is one + virtual void walk_shadow_material(material_handle const Material) = 0; + virtual opengl_material const &walk_material_data(material_handle const Material) const = 0; + virtual opengl_material const &walk_material_data(TSubModel const *Submodel) const = 0; + virtual void walk_program(walk_shader const Program) = 0; + virtual void walk_texture(std::size_t const Unit, texture_handle const Texture) = 0; + // what follows is translucent (blended, without the depth written) or opaque, culled as the pass culls + virtual void walk_blending(bool const Alpha) = 0; + // the strength of the sun and the ambient light, for things in the shade (tunnels, vehicles in them), 1: the full light + virtual void walk_sunlight(float const Factor = 1.f) = 0; + virtual void walk_environment(TEnvironmentType const Environment = e_flat) = 0; + // the ambient light of the cab: kept, added to (the lamp of the cab), put back + virtual void walk_ambient_save() = 0; + virtual void walk_ambient_add(glm::vec3 const &Addition) = 0; + virtual void walk_ambient_restore() = 0; + // the ambient light of the sun, which lights the lines + virtual glm::vec4 walk_sun_ambient() const = 0; + // a draw of the chunk with the state of the walk: model_ubs with the modelview of OpenGLMatrices, and the instances of + // m_current_instance_count / m_current_instance_base when there are any. the second draws the chunks together + virtual void walk_draw(gfx::geometry_handle const &Geometry) = 0; + virtual void walk_draw(std::vector::iterator First, std::vector::iterator Last) = 0; + // places the modelview matrices of instances in the instance buffer of the frame. returns: base instance of the first of them + virtual std::uint32_t walk_instances(glm::mat4 const *Modelviews, std::size_t const Count) = 0; + virtual void walk_cull_face(bool const Enabled) = 0; + // the glare of a lamp: drawn without the depth test, added to what is there; drawn only where the point of the light was + // seen (walk_light_query() of the last frame, when the backend has it) + virtual void walk_glare(bool const Enabled) = 0; + virtual void walk_glare_draw(TSubModel *Submodel, gfx::geometry_handle const &Geometry) = 0; + virtual void walk_light_query(TSubModel *Submodel, bool const Begin) = 0; + // what the backend may draw itself (the gpu culling of the opengl renderer); by default the walk draws everything + // the start of the walk of a region + 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 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; + } + // 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) {} + // 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) {} + virtual void walk_instances_end() {} + + // members + renderpass_config m_renderpass; // parameters for current render pass + section_sequence m_sectionqueue; // list of sections in current render pass + cell_sequence m_cellqueue; + // frame-level accumulation of per-cell opaque instance buckets. Each visited + // cell's buckets are merged here keyed by (TModel3d*, skins), so that + // Render_Instanced() runs once per unique model across the whole pass instead + // of once per cell -- collapsing many tiny instanced draws into a few large + // batches. Reused every pass; the vectors are emptied (not freed) at the top of Render(scene::basic_region*). + std::unordered_map, scene::basic_cell::instance_bucket_key_hash> m_frame_instance_buckets; + // geometry of neighbouring tracks of a cell which share the material, gathered to be drawn together. reused by each Render(path_sequence) + std::vector m_pathbatch; + // instance records kept by the buckets of scene cells are good as long as they carry this number. + // it changes with each frame drawn with the scenery editor active, as the editor moves and resizes instances in place, + // and whenever scene::node_state_version does, as instances are shown, hidden or drawn on their own + unsigned int m_instancerecordsversion{1}; + // the model block of the draw being made + gl::model_ubs model_ubs; + unsigned int m_framestamp{0}; // id of currently rendered gfx frame + unsigned int m_timbersweepframe{0}; // the frame the beams of the tracks far away were last let go in + float m_fogrange{2000.0f}; + // when > 0, the walk is inside a Render_Instanced() call: the draws are made for this many instances from + // m_current_instance_base onwards. The vertex shader reads per-instance modelview from the instance buffer + std::size_t m_current_instance_count{0}; + std::uint32_t m_current_instance_base{0}; + // camera-space transforms of the sleepers and timbers drawn by Render_Timbers, by the model and its part or its level of + // detail; kept from frame to frame to reuse their allocations + struct timber_bucket + { + track_sleepers::beam_model const *model{nullptr}; + int part{-1}; // of a model put together from parts; -1: the whole model + float band{0.f}; // the squared distance the level of detail of the whole model is seen from + std::vector instances; + }; + std::vector m_timberbuckets; + // persistent scratch buffer for Render_Instanced(): holds the per-instance + // camera-space root modelview matrices for the batch currently being built. + // Kept as a member rather than a function-local so its heap allocation is + // reused across calls -- clear() retains capacity, so once it has grown to + // the largest batch seen, steady-state frames perform no allocation here. + std::vector m_instance_modelviews; + // further persistent scratch buffers for Render_Instanced(): culled instances paired with their + // squared distance, and the lod bounds of the drawn model which split them into lod bands + struct instance_survivor + { + glm::mat4 modelview; + float distancesquared; + std::uint32_t band; + }; + std::vector m_instance_survivors; + std::vector m_instance_lodbounds; + std::vector m_instance_lodpending; + // per sort key (lod band and distance slot): first index in m_instance_modelviews (with end sentinel) + // and fill cursor; per lod band: nearest distance + std::vector m_instance_bandstarts; + std::vector m_instance_bandcursors; + std::vector m_instance_banddistances; + std::vector m_instance_bandcounts; // reported by Render_Instanced() for the comparison + // comparison of the gpu culling: the key of the model Render( TAnimModel * ) is called for, and the models it drew in + // the pass with their scaled squared distances + 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 + // 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 + bool m_previewing{false}; + // the last lighting of the pass placed the cab lamp among the lights: the cab is lit by it, not by an addition to the ambient light + bool m_cablamplit{false}; + // the shadow pass draws the kept scenery part of the map of a lamp: no vehicles, and not the model of the lamp itself + bool m_cubescenery{false}; + TAnimModel const *m_cubeskipinstance{nullptr}; + // the shadow pass leaves this vehicle out (the one the headlights of the map being drawn belong to) + TDynamicObject const *m_shadowskipvehicle{nullptr}; + // what the picking passes drew, by the index of their colours + std::vector m_pickcontrolsitems; + std::vector m_picksurfaceitems; + std::vector m_picksceneryitems; + // the quad of the glare of lamps and its texture + gfx::geometry_handle m_billboardgeometry{0, 0}; + texture_handle m_glaretexture{-1}; + + private: + static float editor_ortho_reach(); +}; + +} // namespace gfx diff --git a/rendering/opengl46/opengl46localshadows.cpp b/rendering/opengl46/opengl46localshadows.cpp index 7520eb67..502d6ada 100644 --- a/rendering/opengl46/opengl46localshadows.cpp +++ b/rendering/opengl46/opengl46localshadows.cpp @@ -350,7 +350,7 @@ glm::vec4 opengl46_local_shadows::weights() const 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) +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(); @@ -368,7 +368,7 @@ void opengl46_local_shadows::update_lamps(std::vector(Global.gfx_lights_shadows_lamps_range)}; // the lights come the most important first: the first ones within reach get the slots - std::vector picked; + std::vector picked; for (auto const &light : Lights) { if (picked.size() >= m_slots.size()) @@ -382,7 +382,7 @@ void opengl46_local_shadows::update_lamps(std::vector unplaced; + std::vector unplaced; for (auto const *light : picked) { auto placed{false}; @@ -520,7 +520,7 @@ void opengl46_local_shadows::lamps_timer_end() m_lampscputime = (m_lampscputime == 0.0 ? elapsed : m_lampscputime * 0.9 + elapsed * 0.1); } -opengl46_local_shadows::lamp_slot const *opengl46_local_shadows::lamp(opengl46_local_lights::light const &Light, std::size_t &Slot) const +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) { diff --git a/rendering/opengl46/opengl46localshadows.h b/rendering/opengl46/opengl46localshadows.h index 0b8b3607..604a11d0 100644 --- a/rendering/opengl46/opengl46localshadows.h +++ b/rendering/opengl46/opengl46localshadows.h @@ -18,7 +18,7 @@ http://mozilla.org/MPL/2.0/. #include "gl/shader.h" #include "gl/query.h" -#include "rendering/opengl46/opengl46locallights.h" +#include "rendering/common/locallights.h" #include "rendering/lightarray.h" class TDynamicObject; @@ -98,7 +98,7 @@ class opengl46_local_shadows // a lamp with a slot struct lamp_slot { - void const *owner{nullptr}; // the light it is, see opengl46_local_lights::light + 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 @@ -111,7 +111,7 @@ class opengl46_local_shadows // 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); + 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 @@ -141,7 +141,7 @@ class opengl46_local_shadows 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(opengl46_local_lights::light const &Light, std::size_t &Slot) const; + 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; diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 1be23268..c9a70c7c 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -34,62 +34,6 @@ http://mozilla.org/MPL/2.0/. //#define EU07_DEBUG_OPENGL int constexpr EU07_PICKBUFFERSIZE{ 1024 }; // size of (square) textures bound with the pick framebuffer -int constexpr EU07_REFLECTIONFIDELITYOFFSET { 250 }; // artificial increase of range for reflection pass detail reduction - -namespace { - -// the top view of the editor shows everything this close to the camera, the corners of the screen included -float -editor_ortho_reach() -{ - auto const aspect{Global.window_size.x / std::max(1.f, static_cast(Global.window_size.y))}; - return Global.EditorOrthoExtent * std::sqrt(1.f + aspect * aspect) + 500.f; -} - -float -editor_track_view_range(float const Range, bool const Apply) -{ - if (false == Apply || false == Global.editor_tracks) - return Range; - float extra = 50000.0f; - if (Global.EditorOrtho) - extra = editor_ortho_reach(); - return Range > extra ? Range : extra; -} - -// the top view of the editor may leave the ballast of the railway out, for the ground under the tracks to show -bool -editor_ballast_hidden(TTrack const *Track, bool const Colour) -{ - return Colour && EditorModeFlag && Global.EditorOrtho && false == Global.EditorOrthoBallast && (Track->iCategoryFlag & 15) == 1; -} - -// the top view draws the scenery as far as the tracks; its models keep their own ranges -bool -editor_far_tracks_only(bool const Apply, scene::basic_cell const *Cell, glm::dvec3 const &Camera, double const SceneryLimit) -{ - if (false == Apply || Global.EditorOrtho) - return false; - double nearest = glm::length(Cell->m_area.center - Camera) - static_cast(Cell->m_area.radius); - if (nearest < 0.0) - nearest = 0.0; - return nearest > SceneryLimit; -} - -// returns material assigned to the submodel, resolving replacable skins. null_handle if no skin set is bound -material_handle -submodel_material( TSubModel const *Submodel ) { - - if( Submodel->m_material >= 0 ) { - return Submodel->m_material; - } - return ( - TSubModel::ReplacableSkinId != nullptr ? - TSubModel::ReplacableSkinId[ -Submodel->m_material ] : - null_handle ); -} - -} // namespace void GLAPIENTRY ErrorCallback( GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam ) { @@ -2263,139 +2207,6 @@ if (!Global.gfx_skippipeline) } -bool opengl46_renderer::Render_interior( bool const Alpha ) { - // TODO: early frustum based cull, camera might be pointing elsewhere - std::vector< std::pair > dynamics; - auto *dynamic { simulation::Train->Dynamic() }; - // draw interiors of the occupied vehicle, and all vehicles behind it with permanent coupling, in case they're open-ended - while( dynamic != nullptr ) { - - glm::dvec3 const originoffset { dynamic->vPosition - m_renderpass.pass_camera.position() }; - float const squaredistance{ glm::length2( glm::vec3{ originoffset } / Global.ZoomFactor ) }; - dynamics.emplace_back( squaredistance, dynamic ); - dynamic = dynamic->Next( coupling::permanent ); - } - // draw also interiors of permanently coupled vehicles in front, if there's any - dynamic = simulation::Train->Dynamic()->Prev( coupling::permanent ); - while( dynamic != nullptr ) { - - glm::dvec3 const originoffset { dynamic->vPosition - m_renderpass.pass_camera.position() }; - float const squaredistance{ glm::length2( glm::vec3{ originoffset } / Global.ZoomFactor ) }; - dynamics.emplace_back( squaredistance, dynamic ); - dynamic = dynamic->Prev( coupling::permanent ); - } - if( Alpha ) { - std::sort( - std::begin( dynamics ), std::end( dynamics ), - []( std::pair const &Left, std::pair const &Right ) { - return ( Left.first ) > ( Right.first ); } ); - } - for( auto &dynamic : dynamics ) { - Render_lowpoly( dynamic.second, dynamic.first, true, Alpha ); - } - - return true; -} - -bool opengl46_renderer::Render_lowpoly( TDynamicObject *Dynamic, float const Squaredistance, bool const Setup, bool const Alpha ) { - - if( Dynamic->mdLowPolyInt == nullptr ) { return false; } - - // low poly interior - if( Setup ) { - - TSubModel::iInstance = reinterpret_cast( Dynamic ); //żeby nie robić cudzych animacji - TSubModel::iVariantSeed = Dynamic->variant_seed(); - glm::dvec3 const originoffset{ Dynamic->vPosition - m_renderpass.pass_camera.position() }; - - Dynamic->ABuLittleUpdate( Squaredistance ); // ustawianie zmiennych submodeli dla wspólnego modelu - - glm::mat4 mv = OpenGLMatrices.data( GL_MODELVIEW ); - - ::glPushMatrix(); - ::glTranslated( originoffset.x, originoffset.y, originoffset.z ); - ::glMultMatrixd( glm::value_ptr(Dynamic->mMatrix) ); - } - // HACK: reduce light level for vehicle interior if there's strong global lighting source - if( false == Alpha ) { -/* - auto const luminance { static_cast( std::min( - ( Dynamic->fShade > 0.0 ? Dynamic->fShade * Global.fLuminance : Global.fLuminance ), - Global.fLuminance - 0.5 * ( std::max( 0.3, Global.fLuminance - Global.Overcast ) ) ) ) }; -*/ - auto const luminance{ static_cast( - 0.5 - * ( std::max( - 0.3, - ( Global.fLuminance - Global.Overcast ) - * ( Dynamic->fShade > 0.0 ? - Dynamic->fShade : - 1.0 ) ) ) ) }; - setup_sunlight_intensity( - std::clamp( ( - Dynamic->fShade > 0.f ? - Dynamic->fShade : - 1.f ) - - luminance, - 0.f, 1.f ) ); - Render( Dynamic->mdLowPolyInt, Dynamic->Material(), Squaredistance ); - // HACK: if the model has low poly interior, we presume the load is placed inside and also affected by reduced light level - if( Dynamic->mdLoad ) { - // renderowanie nieprzezroczystego ładunku - Render( Dynamic->mdLoad, Dynamic->Material(), Squaredistance, { 0.f, Dynamic->LoadOffset, 0.f }, {} ); - } - setup_sunlight_intensity( Dynamic->fShade > 0.f ? Dynamic->fShade : 1.f ); - } - else { -// Render_Alpha( Dynamic->mdLowPolyInt, Dynamic->Material(), Squaredistance ); - // HACK: some models have windows included as part of the main model instead of lowpoly - if( Dynamic->mdModel ) { - // main model - Render_Alpha( Dynamic->mdModel, Dynamic->Material(), Squaredistance ); - } - } - - if( Setup ) { - - if( Dynamic->fShade > 0.0f ) { - // restore regular light level - setup_sunlight_intensity(); - } - - ::glPopMatrix(); - - // TODO: check if this reset is needed. In theory each object should render all parts based on its own instance data anyway? - if( Dynamic->btnOn ) { - Dynamic->TurnOff(); // przywrócenie domyślnych pozycji submodeli - } - } - - return true; -} - -bool opengl46_renderer::Render_coupler_adapter( TDynamicObject *Dynamic, float const Squaredistance, int const End, bool const Alpha ) { - - if( Dynamic->m_coupleradapters[ End ] == nullptr ) { return false; } - - auto const position { glm::dvec3 { - 0.f, - Dynamic->MoverParameters->Couplers[ End ].adapter_height, - ( Dynamic->MoverParameters->Couplers[ End ].adapter_length + Dynamic->MoverParameters->Dim.L * 0.5 ) * ( End == end::front ? 1 : -1 ) } }; - - auto const angle { glm::vec3{ - 0, - ( End == end::front ? 0 : 180 ), - 0 } }; - - if( Alpha ) { - Render_Alpha( Dynamic->m_coupleradapters[ End ], Dynamic->Material(), Squaredistance, position, angle ); - } - else { - Render( Dynamic->m_coupleradapters[ End ], Dynamic->Material(), Squaredistance, position, angle ); - } - - return true; -} // creates dynamic environment cubemap bool opengl46_renderer::Render_reflections(viewport_config &vp) @@ -3591,579 +3402,6 @@ void opengl46_renderer::Update_AnimModel(TAnimModel *model) model->RaAnimate(m_framestamp); } -void opengl46_renderer::Render(scene::basic_region *Region) -{ - - m_sectionqueue.clear(); - m_cellqueue.clear(); - // discard last pass's accumulated instance buckets before this pass starts. - // the vectors themselves are kept, so their allocations get reused by the following passes - for( auto &bucket : m_frame_instance_buckets ) { - bucket.second.clear(); - } - // 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; - 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; - } - 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; - m_shadowcasters.clear(); - } - else if( ( m_renderpass.draw_mode == rendermode::shadows ) - && ( Global.gfx_gpucull_shadows ) - && ( m_shadowcascade >= 0 ) - && ( m_shadowcascade < static_cast( m_shadowcull.size() ) ) - && ( false == m_cubeshadows ) ) { - // the cascades of the sun - m_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; - } - } - 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 ); - m_modelcpudistances.clear(); - // build a list of region sections to render - glm::vec3 const cameraposition{m_renderpass.pass_camera.position()}; - auto const camerax = static_cast(std::floor(cameraposition.x / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2)); - auto const cameraz = static_cast(std::floor(cameraposition.z / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2)); - float section_range = m_renderpass.draw_range * Global.fDistanceFactor; - section_range = editor_track_view_range(section_range, EditorModeFlag && (m_renderpass.draw_mode == rendermode::color || m_renderpass.draw_mode == rendermode::pickscenery)); - int const segmentcount = 2 * static_cast(std::ceil(section_range / scene::EU07_SECTIONSIZE)); - int const originx = camerax - segmentcount / 2; - int const originz = cameraz - segmentcount / 2; - - for (int row = originz; row <= originz + segmentcount; ++row) - { - if (row < 0) - { - continue; - } - if (row >= scene::EU07_REGIONSIDESECTIONCOUNT) - { - break; - } - for (int column = originx; column <= originx + segmentcount; ++column) - { - if (column < 0) - { - continue; - } - if (column >= scene::EU07_REGIONSIDESECTIONCOUNT) - { - break; - } - auto *section{Region->m_sections[row * scene::EU07_REGIONSIDESECTIONCOUNT + column]}; - if ((section != nullptr) && (m_renderpass.pass_camera.visible(section->m_area))) - { - m_sectionqueue.emplace_back(section); - } - } - } - - switch (m_renderpass.draw_mode) - { - case rendermode::color: - { - Render(std::begin(m_sectionqueue), std::end(m_sectionqueue)); - // draw queue is filled while rendering sections - if (EditorModeFlag && m_current_viewport->main && (false == m_scenedepthdrawn)) - { - // when editor mode is active calculate world position of the cursor - // at this stage the z-buffer is filled with only ground geometry - // (with the depth pass of the whole scene it's done there, at the same stage) - if (m_scenerecording) - { - // the depth of what the walk recorded so far - Render_Scene_Prepass(); - } - Update_Mouse_Position(); - } - // draw opaque cells front-to-back: with the depth test enabled this - // lets the GPU reject hidden fragments early, before the (expensive) - // lit fragment shader runs on them. Order is irrelevant to the final - // image for opaque geometry, so this is purely a fill-rate win. - std::sort( std::begin( m_cellqueue ), std::end( m_cellqueue ), - []( distancecell_pair const &Left, distancecell_pair const &Right ) { return Left.first < Right.first; } ); - Render(std::begin(m_cellqueue), std::end(m_cellqueue)); - break; - } - case rendermode::shadows: - case rendermode::pickscenery: - { - // these render modes don't bother with lights - Render(std::begin(m_sectionqueue), std::end(m_sectionqueue)); - // they can also skip queue sorting, as they only deal with opaque geometry - // NOTE: there's benefit from rendering front-to-back, but is it significant enough? TODO: investigate - Render(std::begin(m_cellqueue), std::end(m_cellqueue)); - break; - } - case rendermode::reflections: - { - Render(std::begin(m_sectionqueue), std::end(m_sectionqueue)); - if( Global.reflectiontune.fidelity >= 1 ) { - Render(std::begin(m_cellqueue), std::end(m_cellqueue)); - } - break; - } - case rendermode::pickcontrols: - default: - { - // no need to render anything ourside of the cab in control picking mode - break; - } - } -} - -void opengl46_renderer::Render(section_sequence::iterator First, section_sequence::iterator Last) -{ - - switch (m_renderpass.draw_mode) - { - case rendermode::color: - case rendermode::reflections: - case rendermode::shadows: - break; - case rendermode::pickscenery: - { - // non-interactive scenery elements get neutral colour - model_ubs.param[0] = colors::none; - break; - } - default: - break; - } - - while (First != Last) - { - - auto *section = *First; - section->create_geometry(); - - // render shapes held by the section - switch (m_renderpass.draw_mode) - { - case rendermode::color: - case rendermode::reflections: - case rendermode::shadows: - case rendermode::pickscenery: - { - if (false == section->m_shapes.empty()) - { - // since all shapes of the section share center point we can optimize out a few calls here - ::glPushMatrix(); - auto const originoffset{section->m_area.center - m_renderpass.pass_camera.position()}; - ::glTranslated(originoffset.x, originoffset.y, originoffset.z); - // render - for (auto const &shape : section->m_shapes) - { - Render(shape, true); - } - // post-render cleanup - ::glPopMatrix(); - } - break; - } - case rendermode::pickcontrols: - default: - { - break; - } - } - - // add the section's cells to the cell queue - switch (m_renderpass.draw_mode) - { - case rendermode::color: - case rendermode::shadows: - case rendermode::pickscenery: - { - // TBD, TODO: refactor into a method to reuse below? - for (auto &cell : section->m_cells) - { - if ((true == cell.m_active) && (m_renderpass.pass_camera.visible(cell.m_area))) - { - // store visible cells with content as well as their current distance, for sorting later - m_cellqueue.emplace_back(glm::length2(m_renderpass.pass_camera.position() - cell.m_area.center), &cell); - } - } - break; - } - case rendermode::reflections: - { - // we can skip filling the cell queue if reflections pass isn't going to use it - if( Global.reflectiontune.fidelity == 0 ) { break; } - for (auto &cell : section->m_cells) - { - if ((true == cell.m_active) && (m_renderpass.pass_camera.visible(cell.m_area))) - { - // store visible cells with content as well as their current distance, for sorting later - m_cellqueue.emplace_back(glm::length2(m_renderpass.pass_camera.position() - cell.m_area.center), &cell); - } - } - break; - } - case rendermode::pickcontrols: - default: - { - break; - } - } - // proceed to next section - ++First; - } - - switch (m_renderpass.draw_mode) - { - case rendermode::shadows: - { - break; - } - default: - { - break; - } - } -} - -void opengl46_renderer::Render(cell_sequence::iterator First, cell_sequence::iterator Last) -{ - - // cache initial iterator for the second sweep - auto first{First}; - auto const scenery_limit{static_cast(Global.BaseDrawRange) * Global.fDistanceFactor + 250.0}; - bool const far_paths{EditorModeFlag && Global.editor_tracks && (m_renderpass.draw_mode == rendermode::color || m_renderpass.draw_mode == rendermode::pickscenery)}; - glm::dvec3 const camera{m_renderpass.pass_camera.position()}; - // first pass draws elements which we know are located in section banks, to reduce vbo switching - while (First != Last) - { - - auto *cell = First->second; - // przeliczenia animacji torów w sektorze - cell->RaAnimate(m_framestamp); - bool const tracks_only{editor_far_tracks_only(far_paths, cell, camera, scenery_limit)}; - - switch (m_renderpass.draw_mode) - { - case rendermode::color: - { - // since all shapes of the section share center point we can optimize out a few calls here - ::glPushMatrix(); - auto const originoffset{cell->m_area.center - m_renderpass.pass_camera.position()}; - ::glTranslated(originoffset.x, originoffset.y, originoffset.z); - - // render - // opaque non-instanced shapes - if (false == tracks_only) - { - for (auto const &shape : cell->m_shapesopaque) - { - Render(shape, false); - } - } - // tracks - // TODO: update after path node refactoring - Render(std::begin(cell->m_paths), std::end(cell->m_paths)); - // post-render cleanup - ::glPopMatrix(); - - break; - } - case rendermode::reflections: - { - // since all shapes of the section share center point we can optimize out a few calls here - ::glPushMatrix(); - auto const originoffset{cell->m_area.center - m_renderpass.pass_camera.position()}; - ::glTranslated(originoffset.x, originoffset.y, originoffset.z); - - // render - // opaque non-instanced shapes - for (auto const &shape : cell->m_shapesopaque) - Render(shape, false); - - // post-render cleanup - ::glPopMatrix(); - - break; - } - case rendermode::shadows: - { - // since all shapes of the section share center point we can optimize out a few calls here - ::glPushMatrix(); - auto const originoffset{cell->m_area.center - m_renderpass.pass_camera.position()}; - ::glTranslated(originoffset.x, originoffset.y, originoffset.z); - - // render - // opaque non-instanced shapes - for (auto const &shape : cell->m_shapesopaque) - Render(shape, false); - // tracks - Render(std::begin(cell->m_paths), std::end(cell->m_paths)); - - // post-render cleanup - ::glPopMatrix(); - - break; - } - case rendermode::pickscenery: - { - // same procedure like with regular render, but editor-enabled nodes receive custom colour used for picking - // since all shapes of the section share center point we can optimize out a few calls here - ::glPushMatrix(); - auto const originoffset{cell->m_area.center - m_renderpass.pass_camera.position()}; - ::glTranslated(originoffset.x, originoffset.y, originoffset.z); - // render - // opaque non-instanced shapes - // non-interactive scenery elements get neutral colour - if (false == tracks_only) - { - model_ubs.param[0] = colors::none; - for (auto const &shape : cell->m_shapesopaque) - Render(shape, false); - } - // tracks - for (auto *path : cell->m_paths) - { - model_ubs.param[0] = glm::vec4(pick_color(m_picksceneryitems.size() + 1), 1.0f); - Render(path); - } - // post-render cleanup - ::glPopMatrix(); - break; - } - case rendermode::pickcontrols: - default: - { - break; - } - } - - ++First; - } - // second pass draws elements with their own vbos - while (first != Last) - { - - auto const *cell = first->second; - - switch (m_renderpass.draw_mode) - { - case rendermode::color: - case rendermode::shadows: - { - if (editor_far_tracks_only(far_paths, cell, camera, scenery_limit)) - break; - // TBD, TODO: refactor in to a method to reuse in branch below? - // opaque parts of instanced models -- accumulate this cell's buckets - // into the frame-level map; the actual Render_Instanced() calls are - // issued once per unique model after the cell loop (see flush below). - // - // Cell-level far-distance pre-cull: when the whole cell lies beyond - // the instance draw distance, every instance in it would fail the - // per-instance drawdistancethreshold test inside Render_Instanced(), - // so there is no point merging its buckets into the frame map. The - // test reproduces Render_Instanced()'s distance maths exactly -- same - // ZoomFactor / fDistanceFactor scaling, same +250 margin -- applied to - // the nearest point of the cell's bounding sphere. basic_cell::enclose_area - // guarantees m_area.radius >= |m_area.center - instance.location()| for - // every contained instance, so the nearest-point distance is a true - // lower bound on every instance's distance: the cull can never drop a - // cell that still holds a drawable instance. Shadows measure from the - // real (viewport) camera, matching Render_Instanced()'s shadow branch; - // every other gated mode measures from the pass camera. Non-instanced - // scenery drawn by the cpu and vehicles below keep their own per-node - // culling and are intentionally left untouched; the non-instanced models - // culled on the gpu (Queue_Model()) are left out with the cell the same way. - { - auto const &distancecamera = ( - m_renderpass.draw_mode == rendermode::shadows - ? m_renderpass.viewport_camera - : m_renderpass.pass_camera ); - auto const cellcenterdistance { glm::length( cell->m_area.center - distancecamera.position() ) }; - auto const cellnearestdistance { std::max( 0.0, cellcenterdistance - cell->m_area.radius ) }; - auto const cellnearestdistancesquared { - ( cellnearestdistance * cellnearestdistance ) - / ( static_cast( Global.ZoomFactor ) * static_cast( Global.ZoomFactor ) ) - / static_cast( Global.fDistanceFactor ) }; - auto const cellinrange { cellnearestdistancesquared <= sq( static_cast( m_renderpass.draw_range ) + 250.0 ) }; - if( cellinrange ) { - Queue_Instances( cell ); - } - // remaining (non-instanceable) opaque instance nodes go through the per-node path, or with the instances - // to the gpu culling, each on its own - for (auto *instance : cell->m_instancesopaque) - { - if( instance->m_instanceable ) { continue; } // already handled via bucket - // the model of a lamp would shadow everything in the map of the lamp - if( instance == m_cubeskipinstance ) { continue; } - if( Queue_Model( instance, cellinrange ) ) { continue; } - Render(instance); - m_modelcomparekey = nullptr; - } - } - // opaque parts of vehicles. the kept scenery part of the map of a lamp is drawn without them - if( false == m_cubescenery ) { - for (auto *path : cell->m_paths) - { - for (auto *dynamic : path->Dynamics) - { - // the vehicle of the headlights a map is drawn for would cover all of it - if (dynamic == m_shadowskipvehicle) - continue; - Render(dynamic); - } - } - } - break; - } - case rendermode::reflections: - { - // a cell lying whole beyond the range the reflections draw model instances at is left out, as each - // of its instances would fail the range test done for it in Render_Instanced() and Render( TAnimModel * ). - // like the cull of the regular passes above, the test goes by the nearest point of the bounding sphere - // of the cell, which holds the locations of all its instances - auto const cellnearestdistance { std::max( 0.0, glm::length( cell->m_area.center - m_renderpass.pass_camera.position() ) - cell->m_area.radius ) }; - if( ( Global.reflectiontune.fidelity >= 1 ) - && ( cellnearestdistance <= Global.reflectiontune.range_instances ) ) { - // opaque parts of instanced models -- accumulate into the - // frame-level map; flushed once per unique model after the loop. - Queue_Instances( cell ); - for( auto *instance : cell->m_instancesopaque ) { - if( instance->m_instanceable ) { continue; } - if( Queue_Model( instance, true ) ) { continue; } - Render( instance ); - m_modelcomparekey = nullptr; - } - } - if( Global.reflectiontune.fidelity >= 2 ) { - // opaque parts of vehicles - for( auto *path : cell->m_paths ) { - for( auto *dynamic : path->Dynamics ) { - Render( dynamic ); - } - } - } - break; - } - case rendermode::pickscenery: - { - if (editor_far_tracks_only(far_paths, cell, camera, scenery_limit)) - break; - // opaque parts of instanced models - // same procedure like with regular render, but each node receives custom colour used for picking. - // picking always uses the per-instance path because each instance needs a unique pick colour; - // batching would assign the same colour to every instance in the bucket. - for (auto *instance : cell->m_instancesopaque) - { - model_ubs.param[0] = glm::vec4(pick_color(m_picksceneryitems.size() + 1), 1.0f); - Render(instance); - } - // vehicles aren't included in scenery picking for the time being - break; - } - case rendermode::pickcontrols: - default: - { - break; - } - } - - ++first; - } - - // flush accumulated instance buckets: issue one Render_Instanced() per unique - // (TModel3d*, skins) key across every cell visited in this pass, instead of - // one call per cell. All per-instance frustum/distance/pixel-area culling - // still happens inside Render_Instanced(), so correctness is unchanged -- only - // the number of calls and glBufferSubData round-trips drops sharply. - // (For pickscenery/pickcontrols modes the map stays empty, so this is a no-op.) - 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 below empty - if( m_passcull == &m_instancecull ) { - Render_Instances_Indirect(); - } - else { - Render_Instances_Indirect_Pass(); - } - } - 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 ); - } - } - } - for( auto &bucket : m_frame_instance_buckets ) { - if( bucket.second.empty() ) { continue; } - Render_Instanced( bucket.first.pModel, bucket.second, ( m_instancecullpass ? &m_instance_bandcounts : nullptr ) ); - if( m_instancecullpass ) { - m_passcull->cpu_result( bucket.first, m_instance_bandcounts ); - } - bucket.second.clear(); - } - if( m_instancecullpass ) { - m_passcull->end_pass(); - m_instancecullpass = false; - m_passcull = nullptr; - } - m_sleeperspass = false; - m_modelspass = false; - m_modelcpudistances.clear(); -} void opengl46_renderer::Draw_Geometry(std::vector::iterator begin, std::vector::iterator end) { @@ -4230,254 +3468,7 @@ void opengl46_renderer::draw(std::vector::iterator it, Draw_Geometry(it, end); } -void opengl46_renderer::Render(scene::shape_node const &Shape, bool const Ignorerange) -{ - auto const &data{Shape.data()}; - // a shape lying wholly outside the view of the pass draws nothing. the section and the cell holding it were tested as a - // whole, while a section holds the chunks of the heightmap terrain (250 m each, 16 of them) and the large shapes of the - // scenery, drawn in every pass the section is seen in: in a cascade of the sun shadows or a face of the environment map - // most of them are out of view. a shape merged from others has its radius invalidated (-1) and keeps being drawn - if ((data.area.radius > 0.f) && (false == m_renderpass.pass_camera.visible(data.area))) - { - return; - } - - if (false == Ignorerange) - { - double distancesquared; - switch (m_renderpass.draw_mode) - { - case rendermode::shadows: - { - // 'camera' for the light pass is the light source, but we need to draw what the 'real' camera sees - distancesquared = glm::length2((data.area.center - m_renderpass.viewport_camera.position()) / (double)Global.ZoomFactor) / Global.fDistanceFactor; - break; - } - case rendermode::reflections: - { - // reflection mode draws simplified version of the shapes, by artificially increasing view range - distancesquared = - // TBD, TODO: bind offset value with setting variable? - sq(EU07_REFLECTIONFIDELITYOFFSET) - + glm::length2( ( data.area.center - m_renderpass.pass_camera.position() ) ); -/* - // TBD: take into account distance multipliers? - / Global.fDistanceFactor; -*/ - break; - } - default: - { - distancesquared = glm::length2((data.area.center - m_renderpass.pass_camera.position()) / (double)Global.ZoomFactor) / Global.fDistanceFactor; - break; - } - } - if ((distancesquared < data.rangesquared_min) || (distancesquared >= data.rangesquared_max)) - { - return; - } - } - - // setup - switch (m_renderpass.draw_mode) - { - case rendermode::color: - case rendermode::reflections: - Bind_Material(data.material, nullptr, &Shape.data().lighting ); - break; - case rendermode::shadows: - // skip if the shadow caster rank is too low for currently set threshold - if( Material( data.material ).shadow_rank > Global.gfx_shadow_rank_cutoff ) - return; - Bind_Material_Shadow(data.material); - break; - case rendermode::pickscenery: - case rendermode::pickcontrols: - m_pick_shader->bind(); - break; - default: - break; - } - // render - - draw(data.geometry); - // debug data - ++m_renderpass.draw_stats.shapes; - ++m_renderpass.draw_stats.drawcalls; -} - -void opengl46_renderer::Render(TAnimModel *Instance) -{ - - if (false == Instance->m_visible) { return; } - if( false == m_renderpass.pass_camera.visible( Instance->m_area ) ) { return; } - - double distancesquared; - switch (m_renderpass.draw_mode) - { - case rendermode::shadows: - { - // 'camera' for the light pass is the light source, but we need to draw what the 'real' camera sees - distancesquared = glm::length2((Instance->location() - m_renderpass.viewport_camera.position()) / (double)Global.ZoomFactor) / Global.fDistanceFactor; - break; - } - case rendermode::reflections: - { - // reflection mode draws simplified version of the shapes, by artificially increasing view range - // it also ignores zoom settings and distance multipliers - // TBD: take into account distance multipliers? - distancesquared = glm::length2((Instance->location() - m_renderpass.pass_camera.position())) /* / Global.fDistanceFactor */; - // NOTE: arbitrary draw range limit - if( distancesquared > Global.reflectiontune.range_instances * Global.reflectiontune.range_instances ) { - return; - } - // TBD, TODO: bind offset value with setting variable? - distancesquared += sq(EU07_REFLECTIONFIDELITYOFFSET); - break; - } - default: - { - distancesquared = glm::length2((Instance->location() - m_renderpass.pass_camera.position()) / (double)Global.ZoomFactor) / Global.fDistanceFactor; - break; - } - } - if ((distancesquared < Instance->m_rangesquaredmin) || (distancesquared >= Instance->m_rangesquaredmax)) - { - return; - } - // crude way to reject early items too far to affect the output (mostly relevant for shadow passes) - auto const drawdistancethreshold{ m_renderpass.draw_range + 250 }; - if( distancesquared > sq(drawdistancethreshold) ) { - return; - } - // second stage visibility cull, reject modelstoo far away to be noticeable - auto const radiussquared { Instance->radius() * Instance->radius() }; - if( radiussquared * Global.ZoomFactor / distancesquared < 0.003 * 0.003 ) { - return; - } - if( m_modelcomparekey != nullptr ) { - // queued in the gpu culling as well, to compare with (Queue_Model()) - m_modelcpudistances.emplace_back( *m_modelcomparekey, static_cast( distancesquared ) ); - } - - switch (m_renderpass.draw_mode) - { - case rendermode::pickscenery: - { - // add the node to the pick list - m_picksceneryitems.emplace_back(Instance); - break; - } - default: - { - break; - } - } - - Instance->RaAnimate(m_framestamp); // jednorazowe przeliczenie animacji - Instance->RaPrepare(); - if (Instance->pModel) - { - // per-instance uniform scale: applied between the rotation and the - // submodel-local transform chain. Pushing it on the matrix stack here - // means Render(TModel3d, ..., position, angle) doesn't need a new - // signature and the same scale flows naturally into Render_Alpha and - // any other recursive callers. - auto const &scale = Instance->Scale(); - bool const scaled = ( scale.x != 1.0f || scale.y != 1.0f || scale.z != 1.0f ); - if( scaled ) { - // Per-axis scale applied between rotation and the submodel-local - // chain: build the per-instance transform here so we can inject - // glScalef before the submodel walk. - ::glPushMatrix(); - auto const Position = Instance->location() - m_renderpass.pass_camera.position(); - auto const Angle = Instance->vAngle; - ::glTranslated( Position.x, Position.y, Position.z ); - if( Angle.y != 0.0 ) ::glRotated( Angle.y, 0.f, 1.f, 0.f ); - if( Angle.x != 0.0 ) ::glRotated( Angle.x, 1.f, 0.f, 0.f ); - if( Angle.z != 0.0 ) ::glRotated( Angle.z, 0.f, 0.f, 1.f ); - ::glScalef( scale.x, scale.y, scale.z ); - Render( Instance->pModel, Instance->Material(), distancesquared ); - ::glPopMatrix(); - } - else { - // fast path for unscaled (the common case): existing behaviour - Render(Instance->pModel, Instance->Material(), distancesquared, Instance->location() - m_renderpass.pass_camera.position(), Instance->vAngle); - } - // debug data - ++m_renderpass.draw_stats.models; - } -} - -// adds the bucketed instances of a scene cell to the frame-level buckets, for Render_Instanced() to draw. -// Render_Instanced() culls each instance it's given by its record. the tests it makes are repeated here, with some slack -// for the single precision of the records, and the instances which fail are left out of the queue -void opengl46_renderer::Queue_Instances( scene::basic_cell const *Cell ) -{ - // the editor moves and resizes the instances in place. the records are made anew for each frame then, - // but aren't relied on to leave instances out of the queue - auto const userecords { false == EditorModeFlag }; - auto const reflections { m_renderpass.draw_mode == rendermode::reflections }; - // same choices as in Render_Instanced(): shadows measure the distance from the real camera, reflections - // ignore zoom and distance factor, test the plain distance against their own range and add the fidelity offset - auto const &distancecamera { m_renderpass.draw_mode == rendermode::shadows ? m_renderpass.viewport_camera : m_renderpass.pass_camera }; - glm::dvec3 const cameraposition { distancecamera.position() }; - auto const distancescale { reflections ? 1.0 : 1.0 / ( static_cast( Global.ZoomFactor ) * static_cast( Global.ZoomFactor ) * static_cast( Global.fDistanceFactor ) ) }; - auto const distanceoffset { reflections ? sq( static_cast( EU07_REFLECTIONFIDELITYOFFSET ) ) : 0.0 }; - auto const slack { 1.001 }; - auto const distancelimit { slack * ( reflections ? sq( static_cast( Global.reflectiontune.range_instances ) ) : sq( static_cast( m_renderpass.draw_range ) + 250.0 ) ) }; - // up close the rounding of the records is too large a part of the distance for the slack to cover it - auto const distancetested { sq( 64.0 ) }; - // a bucket whose visible instances all fail the tests of the culling by the bounds of their records (Bucket_Culled()) is left - // out of the pass before the culling. the bounds are made with the records, which the editor doesn't let go by - auto const buckettests { Global.gfx_gpucull_bucketrange && userecords }; - - for( auto const &bucket : Cell->m_instancebuckets_opaque ) { - auto const &instancebucket { bucket.second }; - if( ( instancebucket.recordsversion != m_instancerecordsversion ) - || ( instancebucket.records.size() != instancebucket.instances.size() ) ) { - Update_Instance_Records( bucket.first.pModel, instancebucket, Cell->m_area.center ); - } - if( buckettests && Bucket_Culled( instancebucket ) ) { - if( m_instancecullpass ) { - m_passcull->skip_bucket( instancebucket ); - } - continue; - } - 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. the lod bands drawn go by the distances - // the locations of the instances can be at: of their own bounds, or of the cell, which holds them too - if( ( m_renderpass.draw_mode != rendermode::shadows ) || Instances_Cast_Shadow( bucket.first, instancebucket ) ) { - m_passcull->add_bucket( bucket.first, instancebucket, ( buckettests ? Bucket_Area( instancebucket ) : Cell->m_area ) ); - } - // and when the instances are drawn from its result, the cpu has nothing more to do with them - if( m_passcull->drawing() ) { continue; } - } - // the depth pass of the whole scene culled them already, the colour pass draws what it recorded - if( m_indirectcolourpending ) { continue; } - auto const &records { instancebucket.records }; - std::vector *dest { nullptr }; // looked up when there's something to put in it - for( std::size_t idx = 0; idx < records.size(); ++idx ) { - if( userecords ) { - auto const &record { records[ idx ] }; - auto const center { instancebucket.recordorigin + glm::dvec3{ record.position } }; - auto const distancesquared { glm::length2( center - cameraposition ) }; - if( distancesquared > distancetested ) { - auto const scaleddistancesquared { distancesquared * distancescale }; - if( scaleddistancesquared > distancelimit ) { continue; } - if( scaleddistancesquared + distanceoffset >= slack * std::min( instancebucket.lodrangesquared, record.rangesquaredmax ) ) { continue; } - } - if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ center, record.radius } ) ) { continue; } - } - if( dest == nullptr ) { - dest = &m_frame_instance_buckets[ bucket.first ]; - } - dest->push_back( { &instancebucket, idx } ); - } - } -} // 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 @@ -4537,132 +3528,6 @@ bool opengl46_renderer::Queue_Model( TAnimModel *Instance, bool const Inrange ) return drawing; } -// makes the records of the instances held by a bucket of a scene cell, with locations relative to Origin -void opengl46_renderer::Update_Instance_Records( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket, glm::dvec3 const &Origin ) -{ - // a submodel is drawn only closer than its fSquareMaxDist, see Render( TSubModel * ). past the largest of these - // there's nothing of the model left to draw, which makes it another limit of the range of its instances - auto lodrangesquared { ( Model != nullptr && Model->Root != nullptr ) ? 0.f : std::numeric_limits::max() }; - m_instance_lodpending.clear(); - m_instance_lodpending.push_back( Model != nullptr ? Model->Root : nullptr ); - while( false == m_instance_lodpending.empty() ) { - auto const *submodel = m_instance_lodpending.back(); - m_instance_lodpending.pop_back(); - if( submodel == nullptr ) { continue; } - lodrangesquared = std::max( lodrangesquared, submodel->fSquareMaxDist ); - m_instance_lodpending.push_back( submodel->Child ); - m_instance_lodpending.push_back( submodel->Next ); - } - Bucket.lodrangesquared = lodrangesquared; - Bucket.recordorigin = Origin; - - auto const tofloat = []( double const Value ) { - return static_cast( std::min( Value, static_cast( std::numeric_limits::max() ) ) ); }; - - Bucket.records.clear(); - Bucket.records.reserve( Bucket.instances.size() ); - // the range of the bucket: past it none of its instances is drawn, by its own range or by the submodels of the model - auto rangesquared { 0.f }; - for( auto *instance : Bucket.instances ) { - scene::basic_cell::instance_record record; - if( instance != nullptr ) { - record.position = packed_vec3{ instance->location() - Origin }; - record.radius = instance->radius(); - record.rangesquaredmin = tofloat( instance->m_rangesquaredmin ); - record.rangesquaredmax = tofloat( instance->m_rangesquaredmax ); - rangesquared = std::max( rangesquared, record.rangesquaredmax ); - record.flags = - ( instance->visible() ? scene::basic_cell::instance_visible : 0u ) - | ( instance->m_instanceable ? scene::basic_cell::instance_instanceable : 0u ); - auto const &rotationscale { instance->rotation_scale() }; - for( int column = 0; column < 3; ++column ) { - record.rotationscale[ column ] = rotationscale[ column ]; - } - } - // an empty slot keeps its record without flags, and isn't drawn - Bucket.records.emplace_back( record ); - } - Bucket.rangesquared = std::min( rangesquared, lodrangesquared ); - - // bounds of the visible instances, for Bucket_Culled(): the centre of the box around their locations, and the spheres - // around it which hold the locations and the instances whole. the gpu leaves out the ones which aren't visible - glm::dvec3 low { std::numeric_limits::max() }; - glm::dvec3 high { std::numeric_limits::lowest() }; - auto radiusmax { 0.f }; - for( auto const &record : Bucket.records ) { - if( ( record.flags & scene::basic_cell::instance_visible ) == 0 ) { continue; } - glm::dvec3 const position { record.position }; - low = glm::min( low, position ); - high = glm::max( high, position ); - radiusmax = std::max( radiusmax, record.radius ); - } - Bucket.recordlocationradius = 0.f; - Bucket.recordradius = -1.f; - Bucket.recordradiusmax = radiusmax; - if( low.x <= high.x ) { - auto const center { 0.5 * ( low + high ) }; - auto locationradius { 0.0 }; - auto radius { 0.0 }; - for( auto const &record : Bucket.records ) { - if( ( record.flags & scene::basic_cell::instance_visible ) == 0 ) { continue; } - auto const distance { glm::length( glm::dvec3{ record.position } - center ) }; - locationradius = std::max( locationradius, distance ); - radius = std::max( radius, distance + record.radius ); - } - Bucket.recordcenter = Origin + center; - Bucket.recordlocationradius = static_cast( locationradius ); - Bucket.recordradius = static_cast( radius ); - } - else { - Bucket.recordcenter = Origin; - } - Bucket.recordsversion = m_instancerecordsversion; - Bucket.gpustale = true; -} - -// whether none of the instances of a bucket can pass the tests of the culling of the pass (Render_Instanced(), on the gpu -// instance_cull.comp): by the bounds of their records the visible ones lie wholly out of the frustum of the pass, beyond their -// range or the draw range, or are too small on the screen. the tests are those of the culling with the nearest location and -// the largest instance of the bucket, with a margin for the single precision of the records and of the gpu, so they leave out -// only buckets which the culling would leave out whole -bool opengl46_renderer::Bucket_Culled( scene::basic_cell::instance_bucket const &Bucket ) const -{ - if( Bucket.recordradius < 0.f ) { return true; } - - auto const slack { 1.001 }; - auto const margin { 0.5 }; // metres - // the planes of the frustum in world coordinates, the ones the gpu culling moves to the camera of the pass - auto const &planes { m_renderpass.pass_camera.frustum().planes() }; - auto const ¢er { Bucket.recordcenter }; - auto const radius { slack * Bucket.recordradius + margin }; - for( auto const *plane : planes ) { - if( plane[ 0 ] * center.x + plane[ 1 ] * center.y + plane[ 2 ] * center.z + plane[ 3 ] <= -radius ) { return true; } - } - - // the same choices as in Render_Instanced(): shadows measure the distance from the real camera, reflections ignore zoom and - // distance factor, test the plain distance against their own range and add the fidelity offset - auto const reflections { m_renderpass.draw_mode == rendermode::reflections }; - auto const &distancecamera { m_renderpass.draw_mode == rendermode::shadows ? m_renderpass.viewport_camera : m_renderpass.pass_camera }; - auto const nearest { std::max( 0.0, glm::length( center - distancecamera.position() ) - ( slack * Bucket.recordlocationradius + margin ) ) }; - auto const zoom { static_cast( Global.ZoomFactor ) }; - auto const scaledsquared { nearest * nearest * ( reflections ? 1.0 : 1.0 / ( zoom * zoom * static_cast( Global.fDistanceFactor ) ) ) }; - if( reflections && ( scaledsquared > slack * sq( static_cast( Global.reflectiontune.range_instances ) ) ) ) { return true; } - auto const distancesquared { scaledsquared + ( reflections ? sq( static_cast( EU07_REFLECTIONFIDELITYOFFSET ) ) : 0.0 ) }; - if( distancesquared >= slack * Bucket.rangesquared ) { return true; } - if( distancesquared > slack * sq( static_cast( m_renderpass.draw_range ) + 250.0 ) ) { return true; } - // the largest instance at the nearest location takes the largest part of the view - auto const radiusmax { static_cast( Bucket.recordradiusmax ) }; - if( radiusmax * radiusmax * zoom * slack < 0.003 * 0.003 * distancesquared ) { return true; } - - return false; -} - -// the sphere around the locations of the visible instances of a bucket, with the margin of Bucket_Culled(): their distances -// from it set the lod bands the gpu culling can put them in -scene::bounding_area opengl46_renderer::Bucket_Area( scene::basic_cell::instance_bucket const &Bucket ) -{ - return scene::bounding_area{ Bucket.recordcenter, static_cast( 1.001 * Bucket.recordlocationradius + 0.5 ) }; -} // 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 ) @@ -4682,204 +3547,6 @@ void opengl46_renderer::Bind_Instance_Stream() const ::glBindBuffersBase( GL_SHADER_STORAGE_BUFFER, gl::INSTANCE_SSBO_BINDING, 1, &buffer ); } -// True GPU-instanced render path for a group of TAnimModel instances sharing the same -// TModel3d. The submodel tree is walked ONCE per lod band; at every submodel that draws -// geometry we issue a single glDrawElementsInstancedBaseVertexBaseInstance(N) covering all -// visible instances of the band. The vertex shader multiplies effective_modelview = -// instance_modelview[gl_BaseInstance + gl_InstanceID] * model_ubs.modelview, where -// instance_modelview[i] is the camera-space root transform of an instance (precomputed below) -// and model_ubs.modelview is the submodel-local chain (accumulated by the matrix stack starting -// from identity, NOT the camera/instance frame). -void opengl46_renderer::Render_Instanced( TModel3d *Model, std::vector const &Instances, std::vector *Bandcounts ) -{ - if( Bandcounts != nullptr ) { Bandcounts->clear(); } - if( Model == nullptr ) { return; } - if( Instances.empty() ) { return; } - - // LOD: a submodel is drawn only while fSquareDist lies inside its [fSquareMinDist, fSquareMaxDist) - // range, so the distinct range bounds split distance into bands with constant LOD. The bucket - // collects instances from every visible cell, so a single fSquareDist for all of them would draw - // far instances at the LOD of the nearest one. Each instance is assigned to its band and every - // band is drawn with its own fSquareDist, like in Render_Sleepers(). The grouping is a counting - // sort, linear in the instance count. - // the same walk also checks whether any piece of the model can get into the shadow map. Models - // whose materials are all excluded by the shadow rank cutoff (e.g. grass) would otherwise go through - // the whole per-instance culling and the batched draws in every shadow pass, only for every submodel - // to be skipped in Render(TSubModel*) - auto const *frontinstance { Instances.front().bucket->instances[ Instances.front().index ] }; - auto const *skins { frontinstance != nullptr && frontinstance->Material() != nullptr ? frontinstance->Material()->replacable_skins : nullptr }; - bool castsshadow { m_renderpass.draw_mode != rendermode::shadows }; - m_instance_lodbounds.clear(); - m_instance_lodpending.clear(); - m_instance_lodpending.push_back( Model->Root ); - while( false == m_instance_lodpending.empty() ) { - auto const *submodel = m_instance_lodpending.back(); - m_instance_lodpending.pop_back(); - if( submodel == nullptr ) { continue; } - if( submodel->fSquareMinDist > 0.f ) { m_instance_lodbounds.emplace_back( submodel->fSquareMinDist ); } - if( submodel->fSquareMaxDist < std::numeric_limits::max() ) { m_instance_lodbounds.emplace_back( submodel->fSquareMaxDist ); } - if( ( false == castsshadow ) && ( 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 ); - } - if( false == castsshadow ) { return; } - std::sort( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ); - m_instance_lodbounds.erase( std::unique( m_instance_lodbounds.begin(), m_instance_lodbounds.end() ), m_instance_lodbounds.end() ); - auto const bandcount { m_instance_lodbounds.size() + 1 }; - if( Bandcounts != nullptr ) { Bandcounts->assign( bandcount, 0 ); } - - // 1. Visibility / distance cull, by the records of the instances. Collect surviving instances with - // their precomputed camera-space root modelview matrices and lod bands. m_instance_survivors and the - // other scratch vectors are persistent members reused across every Render_Instanced() call: - // clear() drops the contents but keeps the allocated capacity, so after the first few - // frames this stops calling malloc/free entirely. - m_instance_survivors.clear(); - m_instance_survivors.reserve( Instances.size() ); - - // Pull the current pass camera/view transform once. We use the current GL - // modelview matrix as the view matrix because at the point Render_Instanced - // is called, OpenGLMatrices is set to the camera view (scene root) — no - // per-instance transform has been pushed yet. - glm::mat4 const view_matrix = OpenGLMatrices.data( GL_MODELVIEW ); - - TAnimModel *batch_instance { nullptr }; // the first instance drawn; its RaPrepare() and material serve the whole bucket - material_data const *batch_material { nullptr }; - float farthest_distancesquared { 0.f }; - - for( auto const &queued : Instances ) { - auto const &record { queued.bucket->records[ queued.index ] }; - // instances which lost their instanceable status after being bucketed (animation or skin - // assigned by an event) are drawn by the per-node path - if( ( record.flags & scene::basic_cell::instance_instanceable ) == 0 ) { continue; } - if( ( record.flags & scene::basic_cell::instance_visible ) == 0 ) { continue; } - glm::dvec3 const location { queued.bucket->recordorigin + glm::dvec3{ record.position } }; - if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ location, record.radius } ) ) { continue; } - - double distancesquared; - switch( m_renderpass.draw_mode ) { - case rendermode::shadows: - distancesquared = glm::length2( ( location - m_renderpass.viewport_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor; - break; - case rendermode::reflections: - distancesquared = glm::length2( ( location - m_renderpass.pass_camera.position() ) ); - if( distancesquared > Global.reflectiontune.range_instances * Global.reflectiontune.range_instances ) { continue; } - distancesquared += sq( EU07_REFLECTIONFIDELITYOFFSET ); - break; - default: - distancesquared = glm::length2( ( location - m_renderpass.pass_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor; - break; - } - if( ( distancesquared < record.rangesquaredmin ) - || ( distancesquared >= record.rangesquaredmax ) ) { continue; } - - auto const drawdistancethreshold { m_renderpass.draw_range + 250 }; - if( distancesquared > sq( drawdistancethreshold ) ) { continue; } - - auto const radiussquared { record.radius * record.radius }; - if( radiussquared * Global.ZoomFactor / distancesquared < 0.003 * 0.003 ) { continue; } - - if( batch_instance == nullptr ) { - batch_instance = queued.bucket->instances[ queued.index ]; - batch_instance->RaPrepare(); - batch_material = batch_instance->Material(); - } - - // Build the camera-relative root modelview for this instance: - // mv = view * translate(instance_pos - camera_pos) * rotate(instance_angles) * scale(m_scale) - // The rotation and scale part comes from the record, the translation goes into its last - // column. The scale is folded in so the GPU-instanced path produces visually identical output - // to the regular per-instance path; the shader applies it via effective_modelview = instance_mv * model_local. - glm::mat4 const root { - record.rotationscale[ 0 ], - record.rotationscale[ 1 ], - record.rotationscale[ 2 ], - glm::vec4( glm::vec3( location - m_renderpass.pass_camera.position() ), 1.f ) }; - - auto const distance { static_cast( distancesquared ) }; - farthest_distancesquared = std::max( farthest_distancesquared, distance ); - m_instance_survivors.push_back( { - view_matrix * root, - distance, - static_cast( std::upper_bound( m_instance_lodbounds.begin(), m_instance_lodbounds.end(), distance ) - m_instance_lodbounds.begin() ) } ); - } - - if( m_instance_survivors.empty() ) { return; } - - // group the modelviews by lod band into a contiguous array, for the upload. every distance in - // a band selects the same lod; the nearest one represents the band. - // within a band the instances are additionally ordered roughly near-to-far, in distance slots. dense - // alpha tested foliage then gets the most out of the early depth test, as the nearest instances fill - // the depth buffer first and hide the fragments of those behind them before they get shaded. - // both levels are done with a single counting sort, linear in the instance count - auto const distanceslots { 16 }; - auto const keycount { bandcount * distanceslots }; - auto const slotscale { distanceslots / std::sqrt( std::max( farthest_distancesquared, 1.f ) ) }; - m_instance_bandstarts.assign( keycount + 1, 0 ); - m_instance_banddistances.assign( bandcount, std::numeric_limits::max() ); - for( auto &survivor : m_instance_survivors ) { - m_instance_banddistances[ survivor.band ] = std::min( m_instance_banddistances[ survivor.band ], survivor.distancesquared ); - auto const slot { std::min( distanceslots - 1, static_cast( std::sqrt( survivor.distancesquared ) * slotscale ) ) }; - survivor.band = static_cast( survivor.band * distanceslots + slot ); // from now on it's the sort key - ++m_instance_bandstarts[ survivor.band + 1 ]; - } - for( std::size_t key = 1; key <= keycount; ++key ) { - m_instance_bandstarts[ key ] += m_instance_bandstarts[ key - 1 ]; - } - m_instance_bandcursors.assign( m_instance_bandstarts.begin(), m_instance_bandstarts.end() - 1 ); - m_instance_modelviews.resize( m_instance_survivors.size() ); - for( auto const &survivor : m_instance_survivors ) { - m_instance_modelviews[ m_instance_bandcursors[ survivor.band ]++ ] = survivor.modelview; - } - if( Bandcounts != nullptr ) { - for( std::size_t band = 0; band < bandcount; ++band ) { - ( *Bandcounts )[ band ] = static_cast( m_instance_bandstarts[ ( band + 1 ) * distanceslots ] - m_instance_bandstarts[ band * distanceslots ] ); - } - } - // all bands go to the instance buffer at once, each band is drawn from its own base instance - auto const baseinstance { Upload_Instance_Modelviews( m_instance_modelviews.data(), m_instance_modelviews.size() ) }; - - auto alpha = ( batch_material != nullptr ? batch_material->textures_alpha : 0x30300030 ); - alpha ^= 0x0F0F000F; - - // 2. Walk the submodel tree once per lod band. The submodel-local matrix - // stack starts at identity; the per-instance camera transform comes from - // instance_modelview[gl_BaseInstance + gl_InstanceID] in the shader. - for( std::size_t band = 0; band < bandcount; ++band ) { - - std::size_t const band_start = m_instance_bandstarts[ band * distanceslots ]; - std::size_t const band_end = m_instance_bandstarts[ ( band + 1 ) * distanceslots ]; - if( band_start == band_end ) { continue; } - - // 2a. Push identity onto the matrix stack so submodel transforms - // accumulate from identity, not from camera/instance space. - ::glPushMatrix(); - ::glLoadIdentity(); - - // 2b. Configure the existing TModel3d/TSubModel render path. Setting - // m_current_instance_count routes draw(handle) calls to draw_instanced. - m_current_instance_count = band_end - band_start; - m_current_instance_base = baseinstance + static_cast( band_start ); - - Model->Root->fSquareDist = m_instance_banddistances[ band ]; // shared global, used by submodel LOD - Model->Root->ReplacableSet( ( batch_material != nullptr ? batch_material->replacable_skins : nullptr ), alpha ); - Model->Root->pRoot = Model; - - Render( Model->Root ); - - m_current_instance_count = 0; - m_current_instance_base = 0; - - ::glPopMatrix(); - - ++m_renderpass.draw_stats.instanced_drawcalls; - } - - m_renderpass.draw_stats.instances += static_cast( m_instance_survivors.size() ); - m_renderpass.draw_stats.models += static_cast( m_instance_survivors.size() ); -} // how the depth prepass treats the material. the colour program of the material decides: the ones which test the alpha of the // diffuse texture against the opacity (mat_default.frag and the like) get the same test, the ones which never discard a @@ -6541,187 +5208,287 @@ void opengl46_renderer::Queue_Sleepers( TTrack *Track ) m_passcull->add_sleepers( records.key, records.bucket, records.area ); } -void opengl46_renderer::Render_Sleepers( TTrack *Track ) +// 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) { - if( Track == nullptr ) { return; } - if( false == Track->m_sleeper_enabled ) { return; } - if( Track->m_sleeper_model == nullptr ) { return; } - if( Track->m_sleeper_local_transforms.empty() ) { return; } - if( Global.SleeperDistance <= 0.f ) { return; } + Bind_Material(Material, Submodel, Lighting); +} - // only the color and reflection passes draw sleepers; shadow/pick skip them on purpose - // (sleeper shadows would mostly fall back under the trackbed and pick already operates on - // the track itself). - switch( m_renderpass.draw_mode ) { - case rendermode::color: - case rendermode::reflections: - break; - default: - return; - } +void opengl46_renderer::walk_shadow_material(material_handle const Material) +{ + Bind_Material_Shadow(Material); +} - // distance gate -- compare against Globals.SleeperDistance squared to avoid the sqrt - auto const camerapos = m_renderpass.pass_camera.position(); - auto const trackpos = Track->location(); - auto const distsq = glm::length2( trackpos - camerapos ); - auto const cutoffsq = static_cast( Global.SleeperDistance ) * static_cast( Global.SleeperDistance ); - if( distsq > cutoffsq ) { return; } +opengl_material const &opengl46_renderer::walk_material_data(material_handle const Material) const +{ + return m_materials.material(Material); +} - // build camera-space modelview matrices. - // each sleeper's stored matrix is in track-local space (relative to Track->m_origin). - // Render_Sleepers is called from inside the per-cell origin push -- the cell's center - // already equals Track->m_origin (see basic_cell::insert), so the current GL_MODELVIEW - // is already view * translate(m_origin - camera). We just need to compose with each - // per-sleeper local transform to get the final modelview. - glm::mat4 const origin_mv = OpenGLMatrices.data( GL_MODELVIEW ); +opengl_material const &opengl46_renderer::walk_material_data(TSubModel const *Submodel) const +{ + return Material(Submodel); +} - // per-sleeper frustum cull + LOD selection. The whole-track SleeperDistance - // gate above keeps or drops the track as a unit; everything from here on - // operates on individual sleepers. - // - // Each m_sleeper_local_transforms entry is - // translate(world_pos - m_origin) * rotate(...) * translate(local_offset) - // so its translation column is the sleeper position relative to m_origin; - // adding m_origin back yields the sleeper's world-space position. The model's - // bounding radius (floored to a small minimum, in case it was never measured) - // serves both as the frustum test sphere and -- via the distance to that - // world position -- as the per-sleeper LOD distance. - auto const sleeperradius = std::max( Track->m_sleeper_model->bounding_radius(), 2.0f ); - - // Phase 1 -- frustum cull. For every sleeper that survives, store its - // camera-space modelview paired with the squared distance from the camera - // to ITS OWN world position (not the track / segment origin). That - // per-sleeper distance is what drives LOD selection below. A sleeper whose - // origin sits just off screen while its geometry still reaches into view is - // kept rather than wrongly culled. - std::vector> survivors; - survivors.reserve( Track->m_sleeper_local_transforms.size() ); - for( auto const &local : Track->m_sleeper_local_transforms ) { - glm::dvec3 const sleeperworldpos { - Track->m_origin + glm::dvec3( local[ 3 ].x, local[ 3 ].y, local[ 3 ].z ) }; - if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ sleeperworldpos, sleeperradius } ) ) { - continue; - } - auto const sleeperdistancesquared = static_cast( glm::length2( sleeperworldpos - camerapos ) ); - survivors.emplace_back( sleeperdistancesquared, origin_mv * local ); - } - - // every sleeper of this track was frustum-culled -- nothing left to draw - if( survivors.empty() ) { return; } - - // Phase 2 -- sort survivors near-to-far. A submodel is drawn only while - // fSquareDist lies inside its [fSquareMinDist, fSquareMaxDist) range, so - // every distance between two consecutive range bounds selects an identical - // set of submodels -- i.e. the same LOD. Sorting turns each such LOD band - // into a contiguous run, letting the draw loop emit one instanced batch per - // band instead of one track-wide batch at a single distance. - std::sort( survivors.begin(), survivors.end(), - []( std::pair const &Left, std::pair const &Right ) { - return Left.first < Right.first; } ); - - // contiguous copy of the sorted modelview matrices, for the upload. - // Per-sleeper distances stay available as survivors[i].first, in lockstep. - std::vector instance_modelviews; - instance_modelviews.reserve( survivors.size() ); - for( auto const &survivor : survivors ) { - instance_modelviews.emplace_back( survivor.second ); - } - - // collect the model's distinct LOD distance bounds. The sorted, de-duplicated - // set of every submodel's fSquareMinDist / fSquareMaxDist partitions distance - // into bands within which the selected LOD is constant. A model with no LOD - // yields a single band, and the draw loop below then behaves exactly like a - // single plain batched draw. - std::vector lodbounds; +void opengl46_renderer::walk_program(walk_shader const Program) +{ + switch (Program) { - std::vector pending; - if( Track->m_sleeper_model->Root != nullptr ) { - pending.push_back( Track->m_sleeper_model->Root ); + case walk_shader::pick: + m_pick_shader->bind(); + break; + case walk_shader::pick_surface: + m_pick_surface_shader->bind(); + break; + case walk_shader::line: + m_line_shader->bind(); + break; + case walk_shader::billboard: + m_billboard_shader->bind(); + break; + case walk_shader::freespot: + m_freespot_shader->bind(); + break; + } +} + +void opengl46_renderer::walk_texture(std::size_t const Unit, texture_handle const Texture) +{ + Bind_Texture(Unit, Texture); +} + +void opengl46_renderer::walk_blending(bool const Alpha) +{ + setup_drawing(Alpha); +} + +void opengl46_renderer::walk_sunlight(float const Factor) +{ + setup_sunlight_intensity(Factor); +} + +void opengl46_renderer::walk_environment(TEnvironmentType const Environment) +{ + setup_environment_light(Environment); +} + +void opengl46_renderer::walk_ambient_save() +{ + m_cabambient = light_data.ambient; +} + +void opengl46_renderer::walk_ambient_add(glm::vec3 const &Addition) +{ + light_data.ambient += packed_vec3( Addition ); + upload_light_header(); +} + +void opengl46_renderer::walk_ambient_restore() +{ + light_data.ambient = m_cabambient; +} + +glm::vec4 opengl46_renderer::walk_sun_ambient() const +{ + return m_sunlight.ambient; +} + +void opengl46_renderer::walk_draw(gfx::geometry_handle const &Geometry) +{ + draw(Geometry); +} + +void opengl46_renderer::walk_draw(std::vector::iterator First, std::vector::iterator Last) +{ + draw(First, Last); +} + +std::uint32_t opengl46_renderer::walk_instances(glm::mat4 const *Modelviews, std::size_t const Count) +{ + return Upload_Instance_Modelviews(Modelviews, Count); +} + +void opengl46_renderer::walk_cull_face(bool const Enabled) +{ + if (Enabled) + ::glEnable(GL_CULL_FACE); + else + glDisable(GL_CULL_FACE); +} + +void opengl46_renderer::walk_glare(bool const Enabled) +{ + if (Enabled) + { + glDisable(GL_DEPTH_TEST); + glBlendFunc(GL_SRC_ALPHA, GL_ONE); + } + else + { + glEnable(GL_DEPTH_TEST); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } +} + +void opengl46_renderer::walk_glare_draw(TSubModel *Submodel, gfx::geometry_handle const &Geometry) +{ + if (Submodel->occlusion_query) { + glBeginConditionalRender(*Submodel->occlusion_query, GL_QUERY_WAIT); + draw(Geometry); + glEndConditionalRender(); + } + else + draw(Geometry); +} + +void opengl46_renderer::walk_light_query(TSubModel *Submodel, bool const Begin) +{ + if (false == gl::vao::use_vao) + return; + if (Begin) { + if (!Submodel->occlusion_query) + Submodel->occlusion_query.emplace(gl::query::ANY_SAMPLES_PASSED); + Submodel->occlusion_query->begin(); + } + else + Submodel->occlusion_query->end(); +} + +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; + 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; } - while( false == pending.empty() ) { - auto const *submodel = pending.back(); - pending.pop_back(); - lodbounds.emplace_back( submodel->fSquareMinDist ); - lodbounds.emplace_back( submodel->fSquareMaxDist ); - if( submodel->Child != nullptr ) { pending.push_back( submodel->Child ); } - if( submodel->Next != nullptr ) { pending.push_back( submodel->Next ); } + 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; + m_shadowcasters.clear(); + } + else if( ( m_renderpass.draw_mode == rendermode::shadows ) + && ( Global.gfx_gpucull_shadows ) + && ( m_shadowcascade >= 0 ) + && ( m_shadowcascade < static_cast( m_shadowcull.size() ) ) + && ( false == m_cubeshadows ) ) { + // the cascades of the sun + m_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; } } - std::sort( lodbounds.begin(), lodbounds.end() ); - lodbounds.erase( std::unique( lodbounds.begin(), lodbounds.end() ), lodbounds.end() ); - - // optional replacable skin: build a transient material_data so we can drive ReplacableSet - // the same way Render_Instanced does. when no skin is set we fall back to the model defaults. - material_data sleeper_material {}; - bool const has_skin = ( Track->m_sleeper_skin != null_handle ); - if( has_skin ) { - sleeper_material.replacable_skins[ 1 ] = Track->m_sleeper_skin; + 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 ); } - - // Phase 3 -- draw. Walk the sorted survivors one LOD band at a time. Each - // band is submitted as one instanced draw from its own base instance; every - // draw uses an in-band fSquareDist, so each sleeper renders at the LOD its own - // distance selects while instancing stays fully in effect. - auto *Model = Track->m_sleeper_model; - std::size_t const total = instance_modelviews.size(); - auto const baseinstance { Upload_Instance_Modelviews( instance_modelviews.data(), total ) }; - std::size_t band_start = 0; - while( band_start < total ) { - // the band ends at the first sleeper distance that reaches the next LOD - // bound above the band's starting distance. - auto const upperbound = std::upper_bound( - lodbounds.begin(), lodbounds.end(), survivors[ band_start ].first ); - float const band_limit = ( upperbound == lodbounds.end() - ? std::numeric_limits::max() - : *upperbound ); - std::size_t band_end = band_start; - while( ( band_end < total ) && ( survivors[ band_end ].first < band_limit ) ) { - ++band_end; + // 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(); } - // every distance in the band selects the same LOD; the band's nearest - // sleeper is used as the representative fSquareDist. - float const band_distancesquared = survivors[ band_start ].first; - - ::glPushMatrix(); - ::glLoadIdentity(); - - m_current_instance_count = band_end - band_start; - m_current_instance_base = baseinstance + static_cast( band_start ); - - Model->Root->fSquareDist = band_distancesquared; - auto alpha = ( has_skin ? sleeper_material.textures_alpha : 0x30300030 ); - alpha ^= 0x0F0F000F; - Model->Root->ReplacableSet( ( has_skin ? sleeper_material.replacable_skins : nullptr ), alpha ); - Model->Root->pRoot = Model; - - Render( Model->Root ); - - m_current_instance_count = 0; - m_current_instance_base = 0; - - ::glPopMatrix(); - - ++m_renderpass.draw_stats.instanced_drawcalls; - - band_start = band_end; } + // 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 ); +} - m_renderpass.draw_stats.instances += static_cast( total ); - m_renderpass.draw_stats.models += static_cast( total ); +void opengl46_renderer::walk_editor_cursor() +{ + if (m_current_viewport->main && (false == m_scenedepthdrawn)) + { + // (with the depth pass of the whole scene it's done there, at the same stage) + if (m_scenerecording) + { + // the depth of what the walk recorded so far + Render_Scene_Prepass(); + } + Update_Mouse_Position(); + } +} +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 }; + 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 : survivors ) { + for( auto const &survivor : Drawn ) { std::size_t band { 0 }; - for( auto const bound : lodbounds ) { + for( auto const bound : Lodbounds ) { if( counted( bound ) && ( bound <= survivor.first ) ) { ++band; } } ++counts[ band ]; @@ -6729,1042 +5496,60 @@ void opengl46_renderer::Render_Sleepers( TTrack *Track ) } } -// Renders the sleepers and switch timbers of a run of tracks of a cell. Each beam is made of instances of the parts of a model or -// of a whole model, kept by its track relative to the origin of the track. The runs of beams in range and in sight are gathered -// for the whole run of tracks by the model and its part or its level of detail, and each is drawn in one instanced batch. -// In the shadow pass only those near the camera are drawn, their shadows falling where they can be seen. -void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last ) +bool opengl46_renderer::walk_instances_begin() { - if( Global.SleeperDistance <= 0.f ) { return; } - - auto const shadows = ( m_renderpass.draw_mode == rendermode::shadows ); - // the distance is taken from the camera of the view, the shadow pass culling against its own frustum - auto const camera = ( shadows ? m_renderpass.viewport_camera.position() : m_renderpass.pass_camera.position() ); - auto const range = static_cast( shadows ? std::min( Global.SleeperDistance, 50.f ) : Global.SleeperDistance ); - if( m_renderpass.draw_mode == rendermode::color && m_timbersweepframe != m_framestamp ) { - // once a frame the beams of the tracks well past where they're laid and worked out ahead are let go - m_timbersweepframe = m_framestamp; - track_sleepers::release_far( camera, range + 600.0 ); + 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(); } - glm::mat4 const originmodelview = OpenGLMatrices.data( GL_MODELVIEW ); - for( auto &bucket : m_timberbuckets ) { - bucket.instances.clear(); - } - auto const bucket_for = [&]( track_sleepers::beam_model const *Model, int const Part, float const Band ) -> std::vector & { - for( auto &bucket : m_timberbuckets ) { - if( bucket.model == Model && bucket.part == Part && bucket.band == Band ) { - return bucket.instances; + if( m_instancecullpass ) { + if( m_passcull->drawing() ) { + // the buckets of the pass weren't queued for the cpu, which leaves the loop below empty + if( m_passcull == &m_instancecull ) { + Render_Instances_Indirect(); + } + else { + Render_Instances_Indirect_Pass(); } } - m_timberbuckets.push_back( { Model, Part, Band, {} } ); - return m_timberbuckets.back().instances; }; - // the level of detail of the whole model a beam is seen at, from its distance - auto const band_of = []( track_sleepers::beam_model const &Model, double const Distance ) { - auto const squaredistance { static_cast( Distance * Distance ) }; - auto band { Model.bands.empty() ? 0.f : Model.bands.front() }; - for( auto const start : Model.bands ) { - if( squaredistance >= start ) { - band = start; - } - } - return band; }; - // the beams of a run made of a whole model, each at the level of detail of its own distance; the run is culled as one - auto const put_by_band = [&]( track_sleepers::beam_model const &Model, std::vector const &Transforms, std::uint32_t const From, std::uint32_t const To, glm::dvec3 const &Origin ) { - std::vector *instances { nullptr }; - auto current { 0.f }; - for( auto index = From; index < To; ++index ) { - auto const &local = Transforms[ index ]; - auto const band = band_of( Model, glm::length( Origin + glm::dvec3( glm::vec3( local[ 3 ] ) ) - camera ) ); - if( instances == nullptr || band != current ) { - // looked up again only as the band changes; a bucket added on the way moves the others, so the pointer goes with it - instances = &bucket_for( &Model, -1, band ); - current = band; - } - instances->emplace_back( originmodelview * local ); - } - }; - for( auto first { First }; first != Last; ++first ) { - auto *track = *first; - if( false == track->m_visible || false == track->m_sleepers3d.active ) { continue; } - if( glm::length( track->location() - camera ) > range + track->radius() ) { - // worked out ahead, in the background, before it comes in range - if( false == shadows && track->m_sleepers3d.dirty && glm::length( track->location() - camera ) < range + track->radius() + 300.0 ) { - track_sleepers::prefetch( *track ); - } - continue; - } - if( track->m_sleepers3d.dirty ) { - track_sleepers::lay( *track ); - } - auto const &laid = track->m_sleepers3d; - if( laid.timbers == nullptr ) { continue; } - // the transforms of the beams are relative to the origin of the track, which the modelview is translated to already - auto const &trackmodelview = originmodelview; - for( auto const &chunk : laid.chunks ) { - auto const centre = track->m_origin + glm::dvec3( chunk.centre ); - auto const distance = glm::length( centre - camera ); - if( distance > range + chunk.radius ) { continue; } - if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ centre, chunk.radius } ) ) { continue; } - if( laid.timbers->stretchable ) { - for( int part = 0; part < track_sleepers::part_count; ++part ) { - auto &instances = bucket_for( laid.timbers, part, 0.f ); - for( auto index = chunk.first[ part ]; index < chunk.last[ part ]; ++index ) { - instances.emplace_back( trackmodelview * laid.instances[ part ][ index ] ); - } - } - } - if( chunk.wholelast > chunk.wholefirst ) { - put_by_band( *laid.timbers, laid.whole, chunk.wholefirst, chunk.wholelast, track->m_origin ); - } - if( laid.plains != nullptr && chunk.plainlast > chunk.plainfirst ) { - put_by_band( *laid.plains, laid.plain, chunk.plainfirst, chunk.plainlast, track->m_origin ); - } - } - } - - auto const alpha = ( 0x30300030 ^ 0x0F0F000F ); - std::size_t total { 0 }; - for( auto &bucket : m_timberbuckets ) { - auto const &instances = bucket.instances; - if( instances.empty() ) { continue; } - total += instances.size(); - auto const &beammodel = *bucket.model; - auto *model = beammodel.model; - if( bucket.part >= 0 ) { - // only the part of the model is visible, its siblings are skipped as the model is walked - for( int part = 0; part < track_sleepers::part_count; ++part ) { - beammodel.parts[ part ]->iVisible = ( part == bucket.part ? 1 : 0 ); - } - } - material_data skins {}; - if( beammodel.skin != null_handle ) { - skins.replacable_skins[ 1 ] = beammodel.skin; - } - // the instances go to the instance stream of the frame, as in Render_Sleepers(), which the recorded walk of the depth - // pass keeps for its replay as well - auto const baseinstance { Upload_Instance_Modelviews( instances.data(), instances.size() ) }; - ::glPushMatrix(); - ::glLoadIdentity(); - m_current_instance_count = instances.size(); - m_current_instance_base = baseinstance; - // inside the range of the level of detail the beams are at - model->Root->fSquareDist = ( bucket.part >= 0 ? 0.f : bucket.band + 1.f ); - model->Root->ReplacableSet( ( beammodel.skin != null_handle ? skins.replacable_skins : nullptr ), alpha ); - model->Root->pRoot = model; - Render( model->Root ); - m_current_instance_count = 0; - m_current_instance_base = 0; - ::glPopMatrix(); - ++m_renderpass.draw_stats.instanced_drawcalls; - if( bucket.part >= 0 ) { - for( int part = 0; part < track_sleepers::part_count; ++part ) { - beammodel.parts[ part ]->iVisible = 1; - } - } - } - m_renderpass.draw_stats.instances += static_cast( total ); -} - -bool opengl46_renderer::Render(TDynamicObject *Dynamic) -{ - glDebug("Render TDynamicObject"); - - if (!Global.render_cab && Global.pCamera.m_owner == Dynamic) - return false; - - Dynamic->renderme = ( - ( Global.pCamera.m_owner == Dynamic && !FreeFlyModeFlag ) - || ( m_renderpass.pass_camera.visible( Dynamic ) ) ); - - if (false == Dynamic->renderme) { return false; } - - // setup - TSubModel::iInstance = reinterpret_cast(Dynamic); //żeby nie robić cudzych animacji - TSubModel::iVariantSeed = Dynamic->variant_seed(); - glm::dvec3 const originoffset = Dynamic->vPosition - m_renderpass.pass_camera.position(); - // lod visibility ranges are defined for base (x 1.0) viewing distance. for render we adjust them for actual range multiplier and zoom - float squaredistance; - switch (m_renderpass.draw_mode) - { - case rendermode::shadows: - { - squaredistance = glm::length2(glm::vec3{glm::dvec3{Dynamic->vPosition - m_renderpass.viewport_camera.position()}} / Global.ZoomFactor); - if( false == FreeFlyModeFlag ) { - // filter out small details if we're in vehicle cab - squaredistance = std::max( 100.f * 100.f, squaredistance ); - } - break; - } - case rendermode::reflections: - { - // reflection mode draws simplified version of the shapes, by artificially increasing view range - // it also ignores zoom settings and distance multipliers - squaredistance = - std::max( - 100.f * 100.f, - // TBD: take into account distance multipliers? - glm::length2( glm::vec3{ originoffset } ) /* / Global.fDistanceFactor */ ); - // NOTE: arbitrary draw range limit - if( squaredistance > Global.reflectiontune.range_vehicles * Global.reflectiontune.range_vehicles ) { - return false; - } - // TBD, TODO: bind offset value with setting variable? - // NOTE: combined 'squared' distance doesn't equal actual squared (distance + offset) but, eh - squaredistance += sq(EU07_REFLECTIONFIDELITYOFFSET); - break; - } - default: - { - squaredistance = glm::length2(glm::vec3{originoffset} / Global.ZoomFactor); - // TODO: filter out small details based on fidelity setting - break; - } - } - // crude way to reject early items too far to affect the output (mostly relevant for shadow and reflection passes) - auto const drawdistancethreshold{ m_renderpass.draw_range + 250 }; - if( squaredistance > sq(drawdistancethreshold) ) { - return false; - } - // second stage visibility cull, reject vehicles too far away to be noticeable - auto const squareradius{ Dynamic->radius() * Dynamic->radius() }; - Dynamic->renderme = ( squareradius * Global.ZoomFactor / squaredistance > 0.003 * 0.003 ); - if( false == Dynamic->renderme ) { - return false; - } - - // debug data - ++m_renderpass.draw_stats.dynamics; - - Dynamic->ABuLittleUpdate(squaredistance); // ustawianie zmiennych submodeli dla wspólnego modelu - - glm::mat4 future_stack = model_ubs.future; - - glm::mat4 mv = OpenGLMatrices.data(GL_MODELVIEW); - model_ubs.future *= glm::translate(mv, glm::vec3(Dynamic->get_future_movement())) * glm::inverse(mv); - // the velocity it leaves in the colour pass keeps the motion blur on (Render_pass()) - if ((m_renderpass.draw_mode == rendermode::color) && (Dynamic->get_future_movement() != glm::dvec3{0.0})) - m_vehiclesmoving = true; - - ::glPushMatrix(); - ::glTranslated(originoffset.x, originoffset.y, originoffset.z); - ::glMultMatrixd(glm::value_ptr(Dynamic->mMatrix)); - - switch (m_renderpass.draw_mode) - { - - case rendermode::color: - case rendermode::reflections: - { - if (Dynamic->fShade > 0.0f) - { - // change light level based on light level of the occupied track - setup_sunlight_intensity(Dynamic->fShade); - } - - // render - if( Dynamic->mdLowPolyInt ) { - // low poly interior - Render_lowpoly( Dynamic, squaredistance, false ); - } - else { - // HACK: if the model lacks low poly interior, we presume the load is placed outside - if( Dynamic->mdLoad ) { - // renderowanie nieprzezroczystego ładunku - Render( Dynamic->mdLoad, Dynamic->Material(), squaredistance, { 0.f, Dynamic->LoadOffset, 0.f }, {} ); - } - } - if( Dynamic->mdModel ) { - // main model - Render( Dynamic->mdModel, Dynamic->Material(), squaredistance ); - } - // optional attached models - for( auto *attachment : Dynamic->mdAttachments ) { - Render( attachment, Dynamic->Material(), squaredistance ); - } - // optional coupling adapters - Render_coupler_adapter( Dynamic, squaredistance, end::front ); - Render_coupler_adapter( Dynamic, squaredistance, end::rear ); - - // post-render cleanup - if (Dynamic->fShade > 0.0f) - { - // restore regular light level - setup_sunlight_intensity(); - } - break; - } - case rendermode::shadows: - { - if( Dynamic->mdLowPolyInt ) { - // low poly interior - Render( Dynamic->mdLowPolyInt, Dynamic->Material(), squaredistance ); - } - if( Dynamic->mdModel ) { - // main model - Render( Dynamic->mdModel, Dynamic->Material(), squaredistance ); - } - // optional attached models - for( auto *attachment : Dynamic->mdAttachments ) { - Render( attachment, Dynamic->Material(), squaredistance ); - } - // optional coupling adapters - Render_coupler_adapter( Dynamic, squaredistance, end::front ); - Render_coupler_adapter( Dynamic, squaredistance, end::rear ); - if( Dynamic->mdLoad ) { - // renderowanie nieprzezroczystego ładunku - Render( Dynamic->mdLoad, Dynamic->Material(), squaredistance, { 0.f, Dynamic->LoadOffset, 0.f }, {} ); - } - // post-render cleanup - break; - } - case rendermode::pickcontrols: - { - if (Dynamic->mdLowPolyInt) { - // low poly interior - Render(Dynamic->mdLowPolyInt, Dynamic->Material(), squaredistance); - } - break; - } - case rendermode::pickscenery: - default: - { - break; - } - } - - ::glPopMatrix(); - - model_ubs.future = future_stack; - - // TODO: check if this reset is needed. In theory each object should render all parts based on its own instance data anyway? - if (Dynamic->btnOn) - Dynamic->TurnOff(); // przywrócenie domyślnych pozycji submodeli - - return true; -} - -// rendering kabiny gdy jest oddzielnym modelem i ma byc wyswietlana -bool opengl46_renderer::Render_cab(TDynamicObject const *Dynamic, float const Lightlevel, bool const Alpha) -{ - - if (Dynamic == nullptr) - { - - TSubModel::iInstance = 0; - TSubModel::iVariantSeed = 0; - return false; - } - - TSubModel::iInstance = reinterpret_cast(Dynamic); - TSubModel::iVariantSeed = Dynamic->variant_seed(); - - if ((true == FreeFlyModeFlag) || (false == Dynamic->bDisplayCab) || (Dynamic->mdKabina == Dynamic->mdModel)) - { - // ABu: Rendering kabiny jako ostatniej, zeby bylo widac przez szyby, tylko w widoku ze srodka - return false; - } - - if (Dynamic->mdKabina) - { // bo mogła zniknąć przy przechodzeniu do innego pojazdu - // setup shared by all render paths - ::glPushMatrix(); - - auto const originoffset = Dynamic->GetPosition() - m_renderpass.pass_camera.position(); - ::glTranslated(originoffset.x, originoffset.y, originoffset.z); - ::glMultMatrixd(glm::value_ptr(Dynamic->mMatrix)); - - switch (m_renderpass.draw_mode) - { - case rendermode::color: - { - // render path specific setup: - if (Dynamic->fShade > 0.0f) - { - // change light level based on light level of the occupied track - setup_sunlight_intensity(Dynamic->fShade); - } - - auto const old_ambient { light_data.ambient }; - auto const luminance { Global.fLuminance * ( Dynamic->fShade > 0.0f ? Dynamic->fShade : 1.0f ) }; - if( Lightlevel > 0.f ) { - // crude way to light the cabin, until we have something more complete in place. with the lamp of the cab among the - // lights a share of it is kept for the light which bounces around the cab - auto const share { m_cablamplit ? Global.gfx_lights_cab_bounce : 1.f }; - light_data.ambient += packed_vec3( ( Dynamic->InteriorLight * Lightlevel ) * std::clamp( 1.25f - (float)luminance, 0.f, 1.f ) * share ); - upload_light_header(); - } - - // render - if (true == Alpha) - { - // translucent parts - Render_Alpha(Dynamic->mdKabina, Dynamic->Material(), 0.0); - } - else - { - // opaque parts - Render(Dynamic->mdKabina, Dynamic->Material(), 0.0); - } - // post-render restore - light_data.ambient = old_ambient; - setup_sunlight_intensity(); - - break; - } - case rendermode::shadows: - { - if( true == Alpha ) { - // translucent parts - Render_Alpha( Dynamic->mdKabina, Dynamic->Material(), 0.0 ); - } - else { - // opaque parts - Render( Dynamic->mdKabina, Dynamic->Material(), 0.0 ); - } - break; - } - case rendermode::pickcontrols: - { - Render(Dynamic->mdKabina, Dynamic->Material(), 0.0); - break; - } - default: - { - break; - } - } - // post-render restore - ::glPopMatrix(); - } - - return true; -} - -bool opengl46_renderer::Render(TModel3d *Model, material_data const *Material, float const Squaredistance) -{ - - auto alpha = (Material != nullptr ? Material->textures_alpha : 0x30300030); - alpha ^= 0x0F0F000F; // odwrócenie flag tekstur, aby wyłapać nieprzezroczyste - if (0 == (alpha & Model->iFlags & 0x1F1F001F)) - { - // czy w ogóle jest co robić w tym cyklu? - return false; - } - - Model->Root->fSquareDist = Squaredistance; // zmienna globalna! - - // setup - Model->Root->ReplacableSet((Material != nullptr ? Material->replacable_skins : nullptr), alpha); - - Model->Root->pRoot = Model; - - // render - Render(Model->Root); - - // post-render cleanup - - return true; -} - -bool opengl46_renderer::Render(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle) -{ - ::glPushMatrix(); - ::glTranslated(Position.x, Position.y, Position.z); - if (Angle.y != 0.0) - ::glRotated(Angle.y, 0.f, 1.f, 0.f); - if (Angle.x != 0.0) - ::glRotated(Angle.x, 1.f, 0.f, 0.f); - if (Angle.z != 0.0) - ::glRotated(Angle.z, 0.f, 0.f, 1.f); - - auto const result = Render(Model, Material, Squaredistance); - - ::glPopMatrix(); - - return result; -} - -void opengl46_renderer::Render(TSubModel *Submodel) -{ - glDebug("Render TSubModel"); - - if ((Submodel->iVisible) && (TSubModel::fSquareDist >= Submodel->fSquareMinDist) && (TSubModel::fSquareDist < Submodel->fSquareMaxDist) && (false == m_previewing || Submodel->fVisible > 0.f)) - { - glm::mat4 future_stack = model_ubs.future; - - if (Submodel->iFlags & 0xC000) - { - ::glPushMatrix(); - if (Submodel->fMatrix) - ::glMultMatrixf(Submodel->fMatrix->readArray()); - if (Submodel->b_aAnim != TAnimType::at_None) - { - Submodel->RaAnimation(Submodel->b_aAnim); - - glm::mat4 mv = OpenGLMatrices.data(GL_MODELVIEW); - model_ubs.future *= (mv * Submodel->future_transform) * glm::inverse(mv); - // the placement may be different in the next frame: the walk isn't kept (Record_Indirect_Draws()) - m_indirectchanging = true; - } - } - - if (Submodel->eType < TP_ROTATOR) - { - // renderowanie obiektów OpenGL - if (Submodel->iAlpha & Submodel->iFlags & 0x1F) - { - // rysuj gdy element nieprzezroczysty - - // debug data - ++m_renderpass.draw_stats.submodels; - ++m_renderpass.draw_stats.drawcalls; - - switch (m_renderpass.draw_mode) - { - case rendermode::color: - case rendermode::reflections: - { - // material configuration: - // transparency hack - if (Submodel->fVisible < 1.0f) - setup_drawing(true); - - // textures... - if (Submodel->m_material < 0) - { // zmienialne skóry - Bind_Material(submodel_material(Submodel), Submodel); - } - else - { - // również 0 - Bind_Material(Submodel->m_material, Submodel); - } - - // ...luminance - auto const isemissive { ( Submodel->f4Emision.a > 0.f ) && ( Global.fLuminance < Submodel->fLight ) }; - if( ( Submodel->f4Emision.a > 0.f ) && ( Submodel->fLight > -1.f ) ) - { - // lit or not by the luminance of the moment: the walk isn't kept (Record_Indirect_Draws()) - m_indirectchanging = true; - } - if (isemissive) - model_ubs.emission = Submodel->f4Emision.a; - - // main draw call - draw(Submodel->m_geometry.handle); - - // post-draw reset - model_ubs.emission = 0.0f; - if (Submodel->fVisible < 1.0f) - setup_drawing(false); - - break; - } - case rendermode::shadows: - { - // skip if the shadow caster rank is too low for currently set threshold - // or if the submodel is hidden (colour pass only fades it out, e.g. inactive seasonal variant) - if( ( Material( Submodel ).shadow_rank > Global.gfx_shadow_rank_cutoff ) - || ( Submodel->fVisible <= 0.f ) ) - { - --m_renderpass.draw_stats.submodels; - --m_renderpass.draw_stats.drawcalls; - break; - } - - if (Submodel->m_material < 0) - { // zmienialne skóry - Bind_Material_Shadow(submodel_material(Submodel)); - } - else - { - // również 0 - Bind_Material_Shadow(Submodel->m_material); - } - draw(Submodel->m_geometry.handle); - break; - } - case rendermode::pickscenery: - { - m_pick_shader->bind(); - draw(Submodel->m_geometry.handle); - break; - } - case rendermode::pickcontrols: - { - if (Submodel->screen_touch_list) { - // touch screen gradient - m_pick_surface_shader->bind(); - model_ubs.param[0] = glm::vec4(1.0f - m_picksurfaceitems.size() / 255.0f, 0.0f, 0.0f, 1.0f); - m_picksurfaceitems.emplace_back(Submodel); - } - else { - // control picking applies individual colour for each submodel - m_pick_shader->bind(); - m_pickcontrolsitems.emplace_back(Submodel); - model_ubs.param[0] = glm::vec4(pick_color(m_pickcontrolsitems.size()), 1.0f); - } - - draw(Submodel->m_geometry.handle); - break; - } - default: - { - break; - } - } - } - } - else if (Submodel->eType == TP_STARS) - { - - switch (m_renderpass.draw_mode) - { - // colour points are only rendered in colour mode(s) - case rendermode::color: - case rendermode::reflections: - { - m_indirectchanging = true; - if (Global.fLuminance < Submodel->fLight) - { - Bind_Material(Submodel->m_material, Submodel); - - // main draw call - model_ubs.param[1].x = 8.0f; - - draw(Submodel->m_geometry.handle); - } - break; - } - default: - { - break; - } - } - } - if (Submodel->Child != nullptr) - if (Submodel->iAlpha & Submodel->iFlags & 0x001F0000) - Render(Submodel->Child); - - if (Submodel->iFlags & 0xC000) - { - model_ubs.future = future_stack; - ::glPopMatrix(); - } - } - /* - if( Submodel->b_Anim < at_SecondsJump ) - Submodel->b_Anim = at_None; // wyłączenie animacji dla kolejnego użycia subm - */ - if (Submodel->Next) - if (Submodel->iAlpha & Submodel->iFlags & 0x1F000000) - Render(Submodel->Next); // dalsze rekurencyjnie -} - -void opengl46_renderer::Render(TTrack *Track) -{ - if ((Track->m_material1 == 0) && (Track->m_material2 == 0) && (Track->eType != tt_Switch || Track->SwitchExtension->m_material3 == 0)) - { - return; - } - if (false == Track->m_visible) - { - return; - } - - ++m_renderpass.draw_stats.paths; - ++m_renderpass.draw_stats.drawcalls; - - switch (m_renderpass.draw_mode) - { - // single path pieces are rendererd in pick scenery mode only - case rendermode::pickscenery: - { - m_pick_shader->bind(); - m_picksceneryitems.emplace_back( Track ); - - draw(std::begin(Track->Geometry1), std::end(Track->Geometry1)); - draw(std::begin(Track->Geometry2), std::end(Track->Geometry2)); - for (auto &tread : Track->GeometryTreads) - draw(std::begin(tread.geometry), std::end(tread.geometry)); - if (Track->eType == tt_Switch) - draw(Track->SwitchExtension->Geometry3); - break; - } - default: - { - break; - } - } -} - -// experimental, does track rendering in two passes, to take advantage of reduced texture switching -void opengl46_renderer::Render(scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last) -{ - - // setup - switch (m_renderpass.draw_mode) - { - case rendermode::shadows: - { - // NOTE: roads-based platforms tend to miss parts of shadows if rendered with either back or front culling - glDisable(GL_CULL_FACE); - break; - } - default: - { - break; - } - } - - // geometry of neighbouring tracks which share the material is gathered and drawn together. - // only neighbours are combined, the tracks are still drawn in their order in the cell - material_handle batchmaterial{null_handle}; - auto const flush_batch = [&]() - { - if (false == m_pathbatch.empty()) - { - draw(std::begin(m_pathbatch), std::end(m_pathbatch)); - m_pathbatch.clear(); - } - batchmaterial = null_handle; - }; - // returns: true if the material starts a new batch, false if it continues the current one - auto const begin_batch = [&](material_handle const Material) - { - if (Material == batchmaterial) - { - return false; - } - flush_batch(); - Bind_Material(Material); - batchmaterial = Material; - return true; - }; - - // TODO: render auto generated trackbeds together with regular trackbeds in pass 1, and all rails in pass 2 - // first pass, material 1 - for (auto first{First}; first != Last; ++first) - { - - auto const track{*first}; - - if (track->m_material1 == 0) - { - continue; - } - if (false == track->m_visible) - { - continue; - } - - ++m_renderpass.draw_stats.paths; - ++m_renderpass.draw_stats.drawcalls; - - switch (m_renderpass.draw_mode) - { - case rendermode::color: - case rendermode::reflections: - { - if (track->eEnvironment != e_flat) - { - // track with light level of its own is drawn on its own - flush_batch(); - setup_environment_light(track->eEnvironment); - Bind_Material(track->m_blendmaterial1 != null_handle ? track->m_blendmaterial1 : track->m_material1); - draw(std::begin(track->Geometry1), std::end(track->Geometry1)); - // restore default lighting - setup_environment_light(); - break; - } - if (false == begin_batch(track->m_blendmaterial1 != null_handle ? track->m_blendmaterial1 : track->m_material1)) - { - // the track is drawn with the call of its predecessor - --m_renderpass.draw_stats.drawcalls; - } - m_pathbatch.insert(std::end(m_pathbatch), std::begin(track->Geometry1), std::end(track->Geometry1)); - break; - } - case rendermode::shadows: - { - if ((std::abs(track->fTexHeight1) < 0.35f) || (track->iCategoryFlag != 2)) - { - // shadows are only calculated for high enough roads, typically meaning track platforms - --m_renderpass.draw_stats.paths; - --m_renderpass.draw_stats.drawcalls; - continue; - } - if( Material( track->m_material1 ).shadow_rank > Global.gfx_shadow_rank_cutoff ) { - // skip if the shadow caster rank is too low for currently set threshold - --m_renderpass.draw_stats.paths; - --m_renderpass.draw_stats.drawcalls; - continue; - } - Bind_Material_Shadow(track->m_material1); - draw(std::begin(track->Geometry1), std::end(track->Geometry1)); - break; - } - case rendermode::pickscenery: // pick scenery mode uses piece-by-piece approach - case rendermode::pickcontrols: - default: - { - break; - } - } - } - flush_batch(); - // second pass, material 2 - for (auto first{First}; first != Last; ++first) - { - auto const track{*first}; - - if (track->m_material2 == 0) - { - continue; - } - if (false == track->m_visible) - { - continue; - } - if (track->eType == tt_Normal && editor_ballast_hidden(track, m_renderpass.draw_mode == rendermode::color)) - { - continue; - } - - switch (m_renderpass.draw_mode) - { - case rendermode::color: - case rendermode::reflections: - { - if (track->eEnvironment != e_flat) - { - flush_batch(); - setup_environment_light(track->eEnvironment); - Bind_Material(track->m_blendmaterial2 != null_handle ? track->m_blendmaterial2 : track->m_material2); - draw(std::begin(track->Geometry2), std::end(track->Geometry2)); - // restore default lighting - setup_environment_light(); - break; - } - begin_batch(track->m_blendmaterial2 != null_handle ? track->m_blendmaterial2 : track->m_material2); - m_pathbatch.insert(std::end(m_pathbatch), std::begin(track->Geometry2), std::end(track->Geometry2)); - break; - } - case rendermode::shadows: - { - if ((std::abs(track->fTexHeight1) < 0.35f) || ((track->iCategoryFlag == 1) && (track->eType != tt_Normal))) - { - // shadows are only calculated for high enough trackbeds - continue; - } - if( Material( track->m_material2 ).shadow_rank > Global.gfx_shadow_rank_cutoff ) { - // skip if the shadow caster rank is too low for currently set threshold - continue; - } - Bind_Material_Shadow(track->m_material2); - draw(std::begin(track->Geometry2), std::end(track->Geometry2)); - break; - } - case rendermode::pickscenery: // pick scenery mode uses piece-by-piece approach - case rendermode::pickcontrols: - default: - { - break; - } - } - } - flush_batch(); - - // third pass, material 3 - for (auto first{First}; first != Last; ++first) - { - - auto const track{*first}; - - if (track->eType != tt_Switch) - { - continue; - } - if (track->SwitchExtension->m_material3 == 0) - { - continue; - } - if (false == track->m_visible) - { - continue; - } - if (editor_ballast_hidden(track, m_renderpass.draw_mode == rendermode::color)) - { - continue; - } - - switch (m_renderpass.draw_mode) - { - case rendermode::color: - case rendermode::reflections: - { - if (track->eEnvironment != e_flat) - { - flush_batch(); - setup_environment_light(track->eEnvironment); - Bind_Material(track->m_blendmaterial3 != null_handle ? track->m_blendmaterial3 : track->SwitchExtension->m_material3); - draw(track->SwitchExtension->Geometry3); - // restore default lighting - setup_environment_light(); - break; - } - begin_batch(track->m_blendmaterial3 != null_handle ? track->m_blendmaterial3 : track->SwitchExtension->m_material3); - m_pathbatch.emplace_back(track->SwitchExtension->Geometry3); - break; - } - case rendermode::shadows: - { - if ((std::abs(track->fTexHeight1) < 0.35f) || ((track->iCategoryFlag == 1) && (track->eType != tt_Normal))) - { - // shadows are only calculated for high enough trackbeds - continue; - } - if( Material( track->m_material2 ).shadow_rank > Global.gfx_shadow_rank_cutoff ) { - // skip if the shadow caster rank is too low for currently set threshold - continue; - } - Bind_Material_Shadow(track->SwitchExtension->m_material3); - draw(track->SwitchExtension->Geometry3); - break; - } - case rendermode::pickscenery: // pick scenery mode uses piece-by-piece approach - case rendermode::pickcontrols: - default: - { - break; - } - } - } - flush_batch(); - - // cut faces of rails, in matte variants of the rail materials - if ((m_renderpass.draw_mode == rendermode::color) || (m_renderpass.draw_mode == rendermode::reflections)) - { - for (auto first{First}; first != Last; ++first) - { - auto const track{*first}; - - if (false == track->m_visible) - { - continue; - } - std::vector> parts{{track->m_materialcut1, &track->GeometryCut1}, {track->m_materialcut2, &track->GeometryCut2}}; - for (auto &tread : track->GeometryTreads) - { - parts.emplace_back(tread.material, &tread.geometry); - parts.emplace_back(tread.matte, &tread.cuts); - } - for (auto const &cuts : parts) - { - if ((cuts.first == null_handle) || cuts.second->empty()) - { - continue; - } - if (track->eEnvironment != e_flat) - { - flush_batch(); - setup_environment_light(track->eEnvironment); - Bind_Material(cuts.first); - draw(std::begin(*cuts.second), std::end(*cuts.second)); - setup_environment_light(); - continue; - } - begin_batch(cuts.first); - m_pathbatch.insert(std::end(m_pathbatch), std::begin(*cuts.second), std::end(*cuts.second)); - } - } - flush_batch(); - } - - // fourth pass: per-track sleeper models (sleepermodel optional directive). - // drawn after rails/trackbeds so depth pre-pass culling is favourable, and only in passes - // where Render_Sleepers actually does work (it gates itself on draw mode / distance). - switch( m_renderpass.draw_mode ) { - case rendermode::color: - case rendermode::reflections: { - for( auto first { First }; first != Last; ++first ) { - auto *track = *first; - if( false == track->m_visible ) { continue; } - if( false == track->m_sleeper_enabled ) { continue; } - // a track with the sleepers and timbers of eu07.ini lays its sleeper model there - if( track->m_sleepers3d.active ) { continue; } + 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 ) { - // 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() ) { continue; } + for( auto const &counts : m_sleeperbandcounts ) { + if( false == counts.second.empty() ) { + m_passcull->cpu_result( counts.first, counts.second, 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 - continue; + // and the models drawn on their own + for( auto const &model : m_modelcpudistances ) { + m_passcull->cpu_instance( model.first, model.second ); } - Render_Sleepers( track ); } - if( m_renderpass.draw_mode == rendermode::color ) { - Render_Timbers( First, Last ); - } - break; - } - case rendermode::shadows: { - Render_Timbers( First, Last ); - break; - } - default: - break; - } - - // post-render reset - switch (m_renderpass.draw_mode) - { - case rendermode::shadows: - { - // restore standard face cull mode - ::glEnable(GL_CULL_FACE); - break; - } - default: - { - break; - } } + return m_instancecullpass; } -void opengl46_renderer::Render(TMemCell *Memcell) +void opengl46_renderer::walk_instances_compare(scene::basic_cell::instance_bucket_key const &Key, std::vector const &Bandcounts) { - - ::glPushMatrix(); - auto const position = Memcell->location() - m_renderpass.pass_camera.position(); - ::glTranslated(position.x, position.y + 0.5, position.z); - - switch (m_renderpass.draw_mode) - { - case rendermode::color: - { - break; - } - case rendermode::shadows: - case rendermode::pickscenery: - { - break; - } - case rendermode::reflections: - case rendermode::pickcontrols: - { - break; - } - default: - { - break; - } - } - - ::glPopMatrix(); + 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; +} + + + void opengl46_renderer::Render_grass(glm::mat4 const &Future) { if (false == Global.gfx_grass || m_grass.empty()) @@ -7927,206 +5712,6 @@ void opengl46_renderer::Render_precipitation() ::glPopMatrix(); } -void opengl46_renderer::Render_Alpha(scene::basic_region *Region) -{ - - // sort the nodes based on their distance to viewer - std::sort(std::begin(m_cellqueue), std::end(m_cellqueue), [](distancecell_pair const &Left, distancecell_pair const &Right) { return (Left.first) < (Right.first); }); - - Render_Alpha(std::rbegin(m_cellqueue), std::rend(m_cellqueue)); -} - -void opengl46_renderer::Render_Alpha(cell_sequence::reverse_iterator First, cell_sequence::reverse_iterator Last) -{ - - // NOTE: this method is launched only during color pass therefore we don't bother with mode test here - // first pass draws elements which we know are located in section banks, to reduce vbo switching - { - auto first{First}; - while (first != Last) - { - - auto const *cell = first->second; - - if (false == cell->m_shapestranslucent.empty()) - { - // since all shapes of the cell share center point we can optimize out a few calls here - ::glPushMatrix(); - auto const originoffset{cell->m_area.center - m_renderpass.pass_camera.position()}; - ::glTranslated(originoffset.x, originoffset.y, originoffset.z); - // render - // NOTE: we can reuse the method used to draw opaque geometry - for (auto const &shape : cell->m_shapestranslucent) - { - Render(shape, false); - } - // post-render cleanup - ::glPopMatrix(); - } - - ++first; - } - } - // second pass draws elements with their own vbos - { - auto first{First}; - while (first != Last) - { - - auto const *cell = first->second; - - // translucent parts of instanced models - for (auto *instance : cell->m_instancetranslucent) - { - Render_Alpha(instance); - } - // translucent parts of vehicles - for (auto *path : cell->m_paths) - { - for (auto *dynamic : path->Dynamics) - { - Render_Alpha(dynamic); - } - } - - ++first; - } - } - // third pass draws the wires; - // wires use section vbos, but for the time being we want to draw them at the very end - { - auto first{First}; - while (first != Last) - { - - auto const *cell = first->second; - - if ((false == cell->m_traction.empty() || (false == cell->m_lines.empty()))) - { - // since all shapes of the cell share center point we can optimize out a few calls here - ::glPushMatrix(); - auto const originoffset{cell->m_area.center - m_renderpass.pass_camera.position()}; - ::glTranslated(originoffset.x, originoffset.y, originoffset.z); - Bind_Material(null_handle); - // render - for (auto *traction : cell->m_traction) - { - Render_Alpha(traction); - } - for (auto &lines : cell->m_lines) - { - Render_Alpha(lines); - } - // post-render cleanup - ::glPopMatrix(); - } - - ++first; - } - } -} - -void opengl46_renderer::Render_Alpha(TAnimModel *Instance) -{ - - if (false == Instance->m_visible) { return; } - if( false == m_renderpass.pass_camera.visible( Instance->m_area ) ) { return; } - - double distancesquared; - switch (m_renderpass.draw_mode) - { - case rendermode::shadows: - { - // 'camera' for the light pass is the light source, but we need to draw what the 'real' camera sees - distancesquared = glm::length2((Instance->location() - m_renderpass.viewport_camera.position()) / (double)Global.ZoomFactor) / Global.fDistanceFactor; - break; - } - default: - { - distancesquared = glm::length2((Instance->location() - m_renderpass.pass_camera.position()) / (double)Global.ZoomFactor) / Global.fDistanceFactor; - break; - } - } - if ((distancesquared < Instance->m_rangesquaredmin) || (distancesquared >= Instance->m_rangesquaredmax)) - { - return; - } - // crude way to reject early items too far to affect the output (mostly relevant for shadow passes) - auto const drawdistancethreshold{ m_renderpass.draw_range + 250 }; - if( distancesquared > sq(drawdistancethreshold) ) { - return; - } - // second stage visibility cull, reject modelstoo far away to be noticeable - auto const radiussquared { Instance->radius() * Instance->radius() }; - if( radiussquared * Global.ZoomFactor / distancesquared < 0.003 * 0.003 ) { - return; - } - - Instance->RaPrepare(); - if (Instance->pModel) - { - auto const &scale = Instance->Scale(); - bool const scaled = ( scale.x != 1.0f || scale.y != 1.0f || scale.z != 1.0f ); - if( scaled ) { - // scaled instance: build the per-instance transform locally and call - // the distance-only Render_Alpha so we can inject glScalef before - // the submodel walk. Mirror of the opaque path in Render(TAnimModel*). - ::glPushMatrix(); - auto const Position = Instance->location() - m_renderpass.pass_camera.position(); - auto const Angle = Instance->vAngle; - ::glTranslated( Position.x, Position.y, Position.z ); - if( Angle.y != 0.0 ) ::glRotated( Angle.y, 0.f, 1.f, 0.f ); - if( Angle.x != 0.0 ) ::glRotated( Angle.x, 1.f, 0.f, 0.f ); - if( Angle.z != 0.0 ) ::glRotated( Angle.z, 0.f, 0.f, 1.f ); - ::glScalef( scale.x, scale.y, scale.z ); - Render_Alpha( Instance->pModel, Instance->Material(), distancesquared ); - ::glPopMatrix(); - } - else { - Render_Alpha(Instance->pModel, Instance->Material(), distancesquared, Instance->location() - m_renderpass.pass_camera.position(), Instance->vAngle); - } - } -} - -void opengl46_renderer::Render_Alpha(TTraction *Traction) -{ - glDebug("Render_Alpha TTraction"); - - auto const distancesquared { glm::length2( ( Traction->location() - m_renderpass.pass_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor }; - if ((distancesquared < Traction->m_rangesquaredmin) || (distancesquared >= Traction->m_rangesquaredmax)) - { - return; - } - - if (false == Traction->m_visible) - { - return; - } - // rysuj jesli sa druty i nie zerwana - if ((Traction->Wires == 0) || (true == TestFlag(Traction->DamageFlag, 128))) - { - return; - } - // setup - auto const distance{static_cast(std::sqrt(distancesquared))}; - auto const linealpha = 20.f * Traction->WireThickness / std::max(0.5f * Traction->radius() + 1.f, distance - (0.5f * Traction->radius())); - - // render - - // McZapkie-261102: kolor zalezy od materialu i zasniedzenia - model_ubs.param[0] = glm::vec4(Traction->wire_color(), glm::min(1.0f, linealpha)); - - if (m_renderpass.draw_mode == rendermode::shadows) - Bind_Material_Shadow(null_handle); - else - m_line_shader->bind(); - - draw(Traction->m_geometry); - - // debug data - ++m_renderpass.draw_stats.traction; - ++m_renderpass.draw_stats.drawcalls; -} void opengl46_renderer::Render_editor_overlay() { @@ -8177,418 +5762,6 @@ void opengl46_renderer::Render_editor_overlay() ::glPopMatrix(); } -void opengl46_renderer::Render_Alpha(scene::lines_node const &Lines) -{ - glDebug("Render_Alpha scene::lines_node"); - - auto const &data{Lines.data()}; - - auto const distancesquared { glm::length2( ( data.area.center - m_renderpass.pass_camera.position() ) / (double)Global.ZoomFactor ) / Global.fDistanceFactor }; - if ((distancesquared < data.rangesquared_min) || (distancesquared >= data.rangesquared_max)) - { - return; - } - // setup - auto const distance{static_cast(std::sqrt(distancesquared))}; - auto const linealpha = - (data.line_width > 0.f ? 10.f * data.line_width / std::max(0.5f * data.area.radius + 1.f, distance - (0.5f * data.area.radius)) : 1.f); // negative width means the lines are always opague - - model_ubs.param[0] = glm::vec4(glm::vec3(data.lighting.diffuse * m_sunlight.ambient), glm::min(1.0f, linealpha)); - - if (m_renderpass.draw_mode == rendermode::shadows) - Bind_Material_Shadow(null_handle); - else - m_line_shader->bind(); - draw(data.geometry); - - ++m_renderpass.draw_stats.lines; - ++m_renderpass.draw_stats.drawcalls; -} - -bool opengl46_renderer::Render_Alpha(TDynamicObject *Dynamic) -{ - if (!Global.render_cab && Global.pCamera.m_owner == Dynamic) - return false; - - if (false == Dynamic->renderme) - { - return false; - } - - // setup - TSubModel::iInstance = (size_t)Dynamic; //żeby nie robić cudzych animacji - TSubModel::iVariantSeed = Dynamic->variant_seed(); - glm::dvec3 const originoffset = Dynamic->vPosition - m_renderpass.pass_camera.position(); - // lod visibility ranges are defined for base (x 1.0) viewing distance. for render we adjust them for actual range multiplier and zoom - float squaredistance; - switch (m_renderpass.draw_mode) - { - case rendermode::shadows: - default: - { - squaredistance = glm::length2(glm::vec3{originoffset} / Global.ZoomFactor); - break; - } - } - Dynamic->ABuLittleUpdate(squaredistance); // ustawianie zmiennych submodeli dla wspólnego modelu - - glm::mat4 future_stack = model_ubs.future; - - glm::mat4 mv = OpenGLMatrices.data(GL_MODELVIEW); - model_ubs.future *= glm::translate(mv, glm::vec3(Dynamic->get_future_movement())) * glm::inverse(mv); - // the velocity it leaves in the colour pass keeps the motion blur on (Render_pass()) - if ((m_renderpass.draw_mode == rendermode::color) && (Dynamic->get_future_movement() != glm::dvec3{0.0})) - m_vehiclesmoving = true; - - ::glPushMatrix(); - - ::glTranslated(originoffset.x, originoffset.y, originoffset.z); - ::glMultMatrixd(glm::value_ptr(Dynamic->mMatrix)); - - if (Dynamic->fShade > 0.0f) - { - // change light level based on light level of the occupied track - setup_sunlight_intensity(Dynamic->fShade); - } - - // render - if( Dynamic->mdLowPolyInt ) { - // low poly interior - Render_Alpha( Dynamic->mdLowPolyInt, Dynamic->Material(), squaredistance ); - } - if( Dynamic->mdModel ) { - // main model - Render_Alpha( Dynamic->mdModel, Dynamic->Material(), squaredistance ); - } - // optional attached models - for( auto *attachment : Dynamic->mdAttachments ) { - Render_Alpha( attachment, Dynamic->Material(), squaredistance ); - } - // optional coupling adapters - Render_coupler_adapter( Dynamic, squaredistance, end::front, true ); - Render_coupler_adapter( Dynamic, squaredistance, end::rear, true ); - if( Dynamic->mdLoad ) { - // renderowanie nieprzezroczystego ładunku - Render_Alpha( Dynamic->mdLoad, Dynamic->Material(), squaredistance, { 0.f, Dynamic->LoadOffset, 0.f }, {} ); - } - - // post-render cleanup - if (Dynamic->fShade > 0.0f) - { - // restore regular light level - setup_sunlight_intensity(); - } - - ::glPopMatrix(); - - model_ubs.future = future_stack; - - if (Dynamic->btnOn) - Dynamic->TurnOff(); // przywrócenie domyślnych pozycji submodeli - - return true; -} - -bool opengl46_renderer::Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance) -{ - - auto alpha = (Material != nullptr ? Material->textures_alpha : 0x30300030); - - if (0 == (alpha & Model->iFlags & 0x2F2F002F)) - { - // nothing to render - return false; - } - - Model->Root->fSquareDist = Squaredistance; // zmienna globalna! - - // setup - Model->Root->ReplacableSet((Material != nullptr ? Material->replacable_skins : nullptr), alpha); - - Model->Root->pRoot = Model; - - // render - Render_Alpha(Model->Root); - - // post-render cleanup - - return true; -} - -bool opengl46_renderer::Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle) -{ - ::glPushMatrix(); - ::glTranslated(Position.x, Position.y, Position.z); - if (Angle.y != 0.0) - ::glRotated(Angle.y, 0.f, 1.f, 0.f); - if (Angle.x != 0.0) - ::glRotated(Angle.x, 1.f, 0.f, 0.f); - if (Angle.z != 0.0) - ::glRotated(Angle.z, 0.f, 0.f, 1.f); - - auto const result = Render_Alpha(Model, Material, Squaredistance); // position is effectively camera offset - - ::glPopMatrix(); - - return result; -} - -// characters of a text display, laid out in a row. the text can be encoded in utf-8 or in windows-1250 -void opengl46_renderer::Render_Alpha_text(TSubModel *Submodel) -{ - if (TSubModel::pasText == nullptr) - { - return; - } - for (auto *p : Submodel->text_letters(*TSubModel::pasText)) - { // translucent only, for the time being - Render_Alpha(p); - if (p->fMatrix) - ::glMultMatrixf(p->fMatrix->readArray()); // move on to the place of the next character - } -} - -void opengl46_renderer::Render_Alpha(TSubModel *Submodel) -{ - // renderowanie przezroczystych przez DL - if ((Submodel->iVisible) && (TSubModel::fSquareDist >= Submodel->fSquareMinDist) && (TSubModel::fSquareDist < Submodel->fSquareMaxDist) && (false == m_previewing || Submodel->fVisible > 0.f)) - { - glm::mat4 future_stack = model_ubs.future; - - if (Submodel->iFlags & 0xC000) - { - ::glPushMatrix(); - if (Submodel->fMatrix) - ::glMultMatrixf(Submodel->fMatrix->readArray()); - if (Submodel->b_aAnim != TAnimType::at_None) - { - Submodel->RaAnimation(Submodel->b_aAnim); - - glm::mat4 mv = OpenGLMatrices.data(GL_MODELVIEW); - model_ubs.future *= (mv * Submodel->future_transform) * glm::inverse(mv); - } - } - - if (Submodel->eType < TP_ROTATOR) - { - // renderowanie obiektów OpenGL - if (Submodel->iAlpha & Submodel->iFlags & 0x2F) - { - // rysuj gdy element przezroczysty - - // debug data - ++m_renderpass.draw_stats.submodels; - ++m_renderpass.draw_stats.drawcalls; - - switch (m_renderpass.draw_mode) - { - case rendermode::color: - { - // material configuration: - // textures... - if (Submodel->m_material < 0) - { // zmienialne skóry - Bind_Material(submodel_material(Submodel), Submodel); - } - else - { - Bind_Material(Submodel->m_material, Submodel); - } - // ...luminance - - auto const isemissive { ( Submodel->f4Emision.a > 0.f ) && ( Global.fLuminance < Submodel->fLight ) }; - if (isemissive) - model_ubs.emission = Submodel->f4Emision.a; - - // main draw call - draw(Submodel->m_geometry.handle); - - model_ubs.emission = 0.0f; - break; - } - case rendermode::shadows: - { - // skip if the shadow caster rank is too low for currently set threshold - // or if the submodel is hidden (colour pass only fades it out, e.g. inactive seasonal variant) - if( ( Material( Submodel ).shadow_rank > Global.gfx_shadow_rank_cutoff ) - || ( Submodel->fVisible <= 0.f ) ) - { - --m_renderpass.draw_stats.submodels; - --m_renderpass.draw_stats.drawcalls; - break; - } - - if (Submodel->m_material < 0) - { // zmienialne skóry - Bind_Material_Shadow(submodel_material(Submodel)); - } - else - { - Bind_Material_Shadow(Submodel->m_material); - } - draw(Submodel->m_geometry.handle); - break; - } - default: - { - break; - } - } - } - } - else if (Submodel->eType == TP_FREESPOTLIGHT) - { - // NOTE: we're forced here to redo view angle calculations etc, because this data isn't instanced but stored along with the single mesh - // TODO: separate instance data from reusable geometry - auto const &modelview = OpenGLMatrices.data(GL_MODELVIEW); - auto const lightcenter = modelview * glm::mix(glm::vec4(0.f, 0.f, -0.05f, 1.f), glm::vec4(0.f, 0.f, -0.25f, 1.f), - static_cast(TSubModel::fSquareDist / Submodel->fSquareMaxDist)); // pozycja punktu świecącego względem kamery - Submodel->fCosViewAngle = glm::dot(glm::normalize(modelview * glm::vec4(0.f, 0.f, -1.f, 1.f) - lightcenter), glm::normalize(-lightcenter)); - - if (Global.fLuminance < Submodel->fLight || Global.Overcast > 1.0f) - { - if (Submodel->fCosViewAngle > Submodel->fCosFalloffAngle) - { - // only bother if the viewer is inside the visibility cone - // luminosity at night is at level of ~0.1, so the overall resulting transparency in clear conditions is ~0.5 at full 'brightness' - auto glarelevel{std::clamp(std::max(0.6f - Global.fLuminance, // reduce the glare in bright daylight - Global.Overcast - 1.f), // ensure some glare in rainy/foggy conditions - 0.f, 1.f)}; - // view angle attenuation - float const anglefactor{std::clamp((Submodel->fCosViewAngle - Submodel->fCosFalloffAngle) / (Submodel->fCosHotspotAngle - Submodel->fCosFalloffAngle), 0.f, 1.f)}; - - glarelevel *= anglefactor; - - if (glarelevel > 0.0f) - { - glDisable(GL_DEPTH_TEST); - glBlendFunc(GL_SRC_ALPHA, GL_ONE); - - ::glPushMatrix(); - ::glLoadIdentity(); // macierz jedynkowa - ::glTranslatef(lightcenter.x, lightcenter.y, lightcenter.z); // początek układu zostaje bez zmian - ::glRotated(std::atan2(lightcenter.x, lightcenter.z) * 180.0 / M_PI, 0.0, 1.0, 0.0); // jedynie obracamy w pionie o kąt - - auto const lightcolor = glm::vec3(Submodel->DiffuseOverride.r < 0.f ? // -1 indicates no override - Submodel->f4Diffuse : - Submodel->DiffuseOverride); - - m_billboard_shader->bind(); - Bind_Texture(0, m_glaretexture); - model_ubs.param[0] = glm::vec4(glm::vec3(lightcolor), Submodel->fVisible * glarelevel); - - // main draw call - if (Submodel->occlusion_query) { - glBeginConditionalRender(*Submodel->occlusion_query, GL_QUERY_WAIT); - draw(m_billboardgeometry); - glEndConditionalRender(); - } - else - draw(m_billboardgeometry); - - glEnable(GL_DEPTH_TEST); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - ::glPopMatrix(); - } - } - } - - if (Submodel->fCosViewAngle > Submodel->fCosFalloffAngle) - { - // kąt większy niż maksymalny stożek swiatła - float lightlevel = 1.f; // TODO, TBD: parameter to control light strength - // view angle attenuation - float const anglefactor = std::clamp((Submodel->fCosViewAngle - Submodel->fCosFalloffAngle) / (Submodel->fCosHotspotAngle - Submodel->fCosFalloffAngle), 0.f, 1.f); - lightlevel *= anglefactor; - - // distance attenuation. NOTE: since it's fixed pipeline with built-in gamma correction we're using linear attenuation - // we're capping how much effect the distance attenuation can have, otherwise the lights get too tiny at regular distances - float const distancefactor{std::max(0.5f, (Submodel->fSquareMaxDist - TSubModel::fSquareDist) / Submodel->fSquareMaxDist)}; - auto const pointsize{std::max(3.f, 5.f * distancefactor * anglefactor)}; - // additionally reduce light strength for farther sources in rain or snow - if (Global.Overcast > 0.75f) - { - float const precipitationfactor{std::lerp(std::lerp(1.f, 0.25f, std::clamp(Global.Overcast * 0.75f - 0.5f, 0.f, 1.f)), 1.f, distancefactor)}; - lightlevel *= precipitationfactor; - } - - if (lightlevel > 0.f) - { - ::glPushMatrix(); - ::glLoadIdentity(); - ::glTranslatef(lightcenter.x, lightcenter.y, lightcenter.z); // początek układu zostaje bez zmian - - // material configuration: - // limit impact of dense fog on the lights - auto const lightrange { std::max( 500, m_fogrange * 2 ) }; // arbitrary, visibility at least 500m - model_ubs.fog_density = 1.0 / lightrange; - - // main draw call - model_ubs.emission = 1.0f; - - auto lightcolor = glm::vec3(Submodel->DiffuseOverride.r < 0.f ? // -1 indicates no override - Submodel->f4Diffuse : - Submodel->DiffuseOverride); - - m_freespot_shader->bind(); - - if (Global.Overcast > 1.0f) - { - // fake fog halo - float const fogfactor{std::lerp(1.5f, 1.f, std::clamp(Global.fFogEnd / 2000, 0.f, 1.f)) * std::max(1.f, Global.Overcast)}; - model_ubs.param[1].x = pointsize * fogfactor * 4.0f; - model_ubs.param[0] = glm::vec4(glm::vec3(lightcolor), Submodel->fVisible * std::min(1.f, lightlevel) * 0.5f); - - draw(Submodel->m_geometry.handle); - } - model_ubs.param[1].x = pointsize * 4.0f; - model_ubs.param[0] = glm::vec4(glm::vec3(lightcolor), Submodel->fVisible * std::min(1.f, lightlevel)); - - if (gl::vao::use_vao) { - if (!Submodel->occlusion_query) - Submodel->occlusion_query.emplace(gl::query::ANY_SAMPLES_PASSED); - Submodel->occlusion_query->begin(); - } - - draw(Submodel->m_geometry.handle); - - if (gl::vao::use_vao) - Submodel->occlusion_query->end(); - - // post-draw reset - model_ubs.emission = 0.0f; - model_ubs.fog_density = 1.0f / m_fogrange; - - ::glPopMatrix(); - } - } - } - - if (Submodel->Child != nullptr) - { - if (Submodel->eType == TP_TEXT) - { // tekst renderujemy w specjalny sposób, zamiast submodeli z łańcucha Child - Render_Alpha_text(Submodel); - } - else if (Submodel->iAlpha & Submodel->iFlags & 0x002F0000) - Render_Alpha(Submodel->Child); - } - - if (Submodel->iFlags & 0xC000) - { - model_ubs.future = future_stack; - ::glPopMatrix(); - } - } - /* - if( Submodel->b_aAnim < at_SecondsJump ) - Submodel->b_aAnim = at_None; // wyłączenie animacji dla kolejnego użycia submodelu - */ - if (Submodel->Next != nullptr) - if (Submodel->iAlpha & Submodel->iFlags & 0x2F000000) - Render_Alpha(Submodel->Next); -}; bool opengl46_renderer::Debug_Ui_State(std::optional param) { @@ -9501,7 +6674,7 @@ void opengl46_renderer::Update_Lights(light_array &Lights, bool const Headlights light_ssbo->update(&light_data, 0, offsetof(gl::light_data, lights) + sizeof(gl::light_element) * light_i); } -void opengl46_renderer::Write_Local_Light(gl::light_element &Light, opengl46_local_lights::light const &Source, glm::dvec3 const &Camera, glm::mat4 const &View) +void opengl46_renderer::Write_Local_Light(gl::light_element &Light, gfx::local_lights::light const &Source, glm::dvec3 const &Camera, glm::mat4 const &View) { // a cone wider than a half of the space is no limit for the culling, but the shading still fades the light out of it Light.type = gl::light_element::SPOT; @@ -10206,15 +7379,6 @@ bool opengl46_renderer::Init_caps() return true; } -glm::vec3 opengl46_renderer::pick_color(std::size_t const Index) -{ - return glm::vec3{((Index & 0xff0000) >> 16) / 255.0f, ((Index & 0x00ff00) >> 8) / 255.0f, (Index & 0x0000ff) / 255.0f}; -} - -std::size_t opengl46_renderer::pick_index(glm::ivec3 const &Color) -{ - return Color.b + (Color.g * 256) + (Color.r * 256 * 256); -} std::unique_ptr opengl46_renderer::create_func() { diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index 41317772..acd864ea 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -40,14 +40,15 @@ http://mozilla.org/MPL/2.0/. #include "rendering/opengl46/opengl46weather.h" #include "rendering/opengl46/opengl46exposure.h" #include "rendering/opengl46/opengl46ao.h" -#include "rendering/opengl46/opengl46locallights.h" +#include "rendering/common/locallights.h" #include "rendering/opengl46/opengl46localshadows.h" #include "rendering/opengl46/opengl46instancecull.h" #include "rendering/opengl46/opengl46hiz.h" #include "rendering/opengl46/opengl46shadowfit.h" +#include "rendering/common/scenewalk.h" // bare-bones render controller, in lack of anything better yet -class opengl46_renderer : public gfx_renderer { +class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { public: // constructors opengl46_renderer() = default; @@ -189,64 +190,42 @@ class opengl46_renderer : public gfx_renderer { static gfx_backend_desc backend_desc(); private: + using gfx::scene_walk::Render; + using gfx::scene_walk::Render_Alpha; + // the draws of the shared walk of the scene + void walk_material(material_handle const Material, TSubModel const *Submodel = nullptr, lighting_data const *Lighting = nullptr) override; + void walk_shadow_material(material_handle const Material) override; + opengl_material const &walk_material_data(material_handle const Material) const override; + opengl_material const &walk_material_data(TSubModel const *Submodel) const override; + void walk_program(walk_shader const Program) override; + void walk_texture(std::size_t const Unit, texture_handle const Texture) override; + void walk_blending(bool const Alpha) override; + void walk_sunlight(float const Factor = 1.f) override; + void walk_environment(TEnvironmentType const Environment = e_flat) override; + void walk_ambient_save() override; + void walk_ambient_add(glm::vec3 const &Addition) override; + void walk_ambient_restore() override; + glm::vec4 walk_sun_ambient() const override; + void walk_draw(gfx::geometry_handle const &Geometry) override; + void walk_draw(std::vector::iterator First, std::vector::iterator Last) override; + std::uint32_t walk_instances(glm::mat4 const *Modelviews, std::size_t const Count) override; + void walk_cull_face(bool const Enabled) override; + void walk_glare(bool const Enabled) override; + void walk_glare_draw(TSubModel *Submodel, gfx::geometry_handle const &Geometry) override; + void walk_light_query(TSubModel *Submodel, bool const Begin) override; + // 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; + // types - enum class rendermode - { - none, - color, - shadows, - reflections, - pickcontrols, - pickscenery - }; - - struct debug_stats - { - int dynamics{0}; - int models{0}; - int submodels{0}; - int paths{0}; - int traction{0}; - int shapes{0}; - int lines{0}; - int particles{0}; - int drawcalls{0}; - int instanced_drawcalls{0}; // drawcalls issued via instanced render path (one per TModel3d batch) - int instances{0}; // total instances drawn via instanced render path - int triangles{0}; - - debug_stats& operator+=( const debug_stats& Right ) { - dynamics += Right.dynamics; - models += Right.models; - submodels += Right.submodels; - paths += Right.paths; - traction += Right.traction; - shapes += Right.shapes; - lines += Right.lines; - particles += Right.particles; - drawcalls += Right.drawcalls; - instanced_drawcalls += Right.instanced_drawcalls; - instances += Right.instances; - return *this; } - }; - - using section_sequence = std::vector; - using distancecell_pair = std::pair; - using cell_sequence = std::vector; - - struct renderpass_config - { - opengl_camera pass_camera; - opengl_camera viewport_camera; - rendermode draw_mode{rendermode::none}; - float draw_range{0.0f}; - // depth range of the projection of a color pass; the far plane is only the end of the range which is drawn, - // as the projection matrix itself has none - float znear{0.0f}; - float zfar{0.0f}; - debug_stats draw_stats; - }; - struct viewport_config { int width; int height; @@ -349,33 +328,6 @@ class opengl46_renderer : public gfx_renderer { // Position in world space, Rotation from world axes to its axes; Near is the distance of the near plane from the projector void Headlight_Projector(light_array::light_record const &Light, glm::dvec3 &Position, glm::mat3 &Rotation, glm::mat4 &Projection, float &Near, float &Fieldofview); bool Render(world_environment *Environment); - void Render(scene::basic_region *Region); - void Render(section_sequence::iterator First, section_sequence::iterator Last); - void Render(cell_sequence::iterator First, cell_sequence::iterator Last); - void Render(scene::shape_node const &Shape, bool const Ignorerange); - void Render(TAnimModel *Instance); - // an instance of a bucket of a scene cell, queued to be drawn by Render_Instanced() - struct queued_instance { - scene::basic_cell::instance_bucket const *bucket; - std::size_t index; // of the instance and its record in the bucket - }; - // batched render path for many TAnimModel instances that share the same TModel3d. - // Caller guarantees every instance was instanceable when bucketed (no per-instance lights, - // no submodel animation, same replacable skins); the records of the instances tell which - // ones still are. Instances are individually frustum/distance culled by their records, the - // TAnimModel itself is read only for the first instance drawn. Each lod band drawn counts as - // one instanced_drawcall in draw_stats; the instances drawn are added to the instances counter. - // Bandcounts, when given, gets the number of instances drawn in each lod band. - void Render_Instanced( TModel3d *Model, std::vector const &Instances, std::vector *Bandcounts = nullptr ); - // adds the bucketed instances of a scene cell to the frame-level buckets drawn by Render_Instanced(), - // less the ones their records show to be out of sight in the current render pass - void Queue_Instances( scene::basic_cell const *Cell ); - // makes the records of the instances held by a bucket of a scene cell, with locations relative to Origin - void Update_Instance_Records( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket, glm::dvec3 const &Origin ); - // whether none of the instances of a bucket can pass the culling of the current pass, by the bounds of their records - bool Bucket_Culled( scene::basic_cell::instance_bucket const &Bucket ) const; - // the sphere around the locations of the visible instances of a bucket, with a margin for the precision of the records - static scene::bounding_area Bucket_Area( scene::basic_cell::instance_bucket const &Bucket ); // places supplied modelview matrices in the instance buffer of the frame. returns: base instance of the first of them std::uint32_t Upload_Instance_Modelviews( glm::mat4 const *Modelviews, std::size_t const Count ); // binds the instance buffer of the frame for the vertex shaders (INSTANCE_SSBO_BINDING) @@ -479,39 +431,20 @@ class opengl46_renderer : public gfx_renderer { 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; - bool Render(TDynamicObject *Dynamic); - bool Render(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle); - bool Render(TModel3d *Model, material_data const *Material, float const Squaredistance); - void Render(TSubModel *Submodel); - void Render(TTrack *Track); - void Render(scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last); - // renders the per-track sleeper instances (TTrack::m_sleeper_local_transforms) via GPU instancing. - // caller must already have the camera-relative world-space transform set on the matrix stack. - // no-op if the track has no sleepermodel, Global.SleeperDistance is 0, or the camera is beyond - // Global.SleeperDistance from the track origin. - void Render_Sleepers( TTrack *Track ); // 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 ); - // renders the sleepers and switch timbers of the tracks in range (TTrack::m_sleepers3d), laying them first where they're out of date. - // the parts of the model are drawn in instanced batches for the whole run of tracks, the runs of beams out of sight left out - void Render_Timbers( scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last ); // 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 ); - bool Render_cab(TDynamicObject const *Dynamic, float const Lightlevel, bool const Alpha = false); - bool Render_interior( bool const Alpha = false ); - bool Render_lowpoly( TDynamicObject *Dynamic, float const Squaredistance, bool const Setup, bool const Alpha = false ); - bool Render_coupler_adapter( TDynamicObject *Dynamic, float const Squaredistance, int const End, bool const Alpha = false ); // 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 std::vector const &Model_Textures( TModel3d *Model ); // has the data of the textures read ahead of the camera, with gfx.textures.streaming void Update_Texture_Prefetch(); - void Render(TMemCell *Memcell); void Render_particles(); void Render_precipitation(); // the view of the colour pass being drawn, for the particles of the gpu @@ -519,24 +452,14 @@ class opengl46_renderer : public gfx_renderer { // the grass of the heightmap terrain around the camera of the main view; Future: the matrix of the motion vectors void Render_grass(glm::mat4 const &Future); void Render_vr_models(); - void Render_Alpha(scene::basic_region *Region); - void Render_Alpha(cell_sequence::reverse_iterator First, cell_sequence::reverse_iterator Last); - void Render_Alpha(TAnimModel *Instance); - void Render_Alpha(TTraction *Traction); - void Render_Alpha(scene::lines_node const &Lines); void Render_editor_overlay(); - bool Render_Alpha(TDynamicObject *Dynamic); - bool Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle); - bool Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance); - void Render_Alpha(TSubModel *Submodel); - void Render_Alpha_text(TSubModel *Submodel); // characters of a text display // writes the lighting of the pass about to be drawn to the light buffer: the sun, the ambient light and the fog, and the headlights of Lights // unless Headlights is false, as in the pass which draws the environment map, where lights of vehicles would only get baked into the map. // Clustered tells the shading to take the headlights from the lists of the clusters (see Cull_Lights()) and not to go through the first few. // with Headlights and Clustered the cab lamp (first) and the lamps of models and vehicles (after the headlights) are written as well void Update_Lights(light_array &Lights, bool const Headlights = true, bool const Clustered = false); // writes a light of a model or a vehicle, or the cab lamp, to the slot of the light buffer, in the view space of the pass - void Write_Local_Light(gl::light_element &Light, opengl46_local_lights::light const &Source, glm::dvec3 const &Camera, glm::mat4 const &View); + void Write_Local_Light(gl::light_element &Light, gfx::local_lights::light const &Source, glm::dvec3 const &Camera, glm::mat4 const &View); // brings the light cluster grid of the viewport up to date with the projection of the color pass being drawn. // Targetsize is the size in pixels of what the pass draws to, which gl_FragCoord of the shading counts in void Update_Clusters(viewport_config &vp, glm::ivec2 const &Targetsize); @@ -547,8 +470,6 @@ class opengl46_renderer : public gfx_renderer { void Render_cluster_heatmap(viewport_config &vp, gl::framebuffer *Target, glm::ivec2 const &Targetsize); // ambient occlusion from the resolved depth (vp.main_texd), multiplied into the image of Target. Mode: Global.gfx_postfx_ao void Render_ambient_occlusion(viewport_config &vp, gl::framebuffer &Target, int const Mode); - glm::vec3 pick_color(std::size_t const Index); - std::size_t pick_index(glm::ivec3 const &Color); bool init_viewport(viewport_config &vp); @@ -590,8 +511,6 @@ class opengl46_renderer : public gfx_renderer { /* float m_sunandviewangle; // cached dot product of sunlight and camera vectors */ - gfx::geometry_handle m_billboardgeometry{0, 0}; - texture_handle m_glaretexture{-1}; texture_handle m_suntexture{-1}; texture_handle m_moontexture{-1}; texture_handle m_smoketexture{-1}; @@ -612,8 +531,6 @@ class opengl46_renderer : public gfx_renderer { opengl46_gpu_particles m_gpuparticles; // the smoke, rain and snow of the gpu (gfx.smoke.gpu, gfx.precipitation.gpu) bool m_particlesoft{false}; // the depth for the soft particles is bound in the colour pass being drawn - unsigned int m_framestamp; // id of currently rendered gfx frame - unsigned int m_timbersweepframe { 0 }; // the frame the beams of the tracks far away were last let go in float m_framerate; double m_updateaccumulator{0.0}; std::string m_debugtimestext; @@ -638,29 +555,12 @@ class opengl46_renderer : public gfx_renderer { float m_specularopaquescalefactor{1.f}; float m_speculartranslucentscalefactor{1.f}; - float m_fogrange = 2000.0f; - - renderpass_config m_renderpass; // parameters for current render pass - section_sequence m_sectionqueue; // list of sections in current render pass - cell_sequence m_cellqueue; - // frame-level accumulation of per-cell opaque instance buckets. Each visited - // cell's buckets are merged here keyed by (TModel3d*, skins), so that - // Render_Instanced() runs once per unique model across the whole pass instead - // of once per cell -- collapsing many tiny instanced draws into a few large - // batches. Reused every pass; the vectors are emptied (not freed) at the top of Render(scene::basic_region*). - std::unordered_map, scene::basic_cell::instance_bucket_key_hash> m_frame_instance_buckets; // textures used by vehicle models, for the release of far textures std::unordered_map> m_modeltextures; // the read ahead of textures (Update_Texture_Prefetch()): the cell it goes on with std::size_t m_prefetchcursor { 0 }; std::chrono::steady_clock::time_point m_prefetchvehicletime; std::chrono::steady_clock::time_point m_prefetchdroptime; - // geometry of neighbouring tracks of a cell which share the material, gathered to be drawn together. reused by each Render(path_sequence) - std::vector m_pathbatch; - // instance records kept by the buckets of scene cells are good as long as they carry this number. - // it changes with each frame drawn with the scenery editor active, as the editor moves and resizes instances in place, - // and whenever scene::node_state_version does, as instances are shown, hidden or drawn on their own - unsigned int m_instancerecordsversion { 1 }; unsigned int m_nodestateversion { 0 }; // scene::node_state_version which m_instancerecordsversion was last checked against // culling of instances in compute passes, run along the first color pass of the main viewport of a frame // when Global.gfx_gpucull is set: the instances of the pass are drawn from its result (1), or the result is compared @@ -683,7 +583,6 @@ class opengl46_renderer : public gfx_renderer { 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; - std::vector m_instance_bandcounts; // reported by Render_Instanced() for the comparison // 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 @@ -710,10 +609,6 @@ class opengl46_renderer : public gfx_renderer { 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 - // comparison (Global.gfx_gpucull 2): the key of the model Render( TAnimModel * ) is called for, and the models it drew in - // the pass with their scaled squared distances, reported once the culling ran - scene::basic_cell::instance_bucket_key const *m_modelcomparekey { nullptr }; - std::vector> m_modelcpudistances; 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 @@ -786,7 +681,6 @@ class opengl46_renderer : public gfx_renderer { }; std::unordered_map m_walkrecords; int m_walkrankcutoff { 0 }; // Global.gfx_shadow_rank_cutoff the walks were made with - bool m_indirectchanging { false }; // the walk under way met a submodel which may look different in the next frame // 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 { @@ -949,14 +843,10 @@ class opengl46_renderer : public gfx_renderer { bool m_contactshadowslast { false }; // a vehicle drawn in the colour pass of the last frame moved, and one drawn in this frame did (motion blur, Render_pass()) bool m_vehiclesmoved { true }; - bool m_vehiclesmoving { false }; gl::gpu_timer m_contactshadowstimer; // resolve of the depth renderpass_config m_colorpass; // parametrs of most recent color pass std::array m_shadowpass; // parametrs of most recent shadowmap pass for each of csm stages - std::vector m_pickcontrolsitems; - std::vector m_picksurfaceitems; TSubModel const *m_pickcontrolitem{nullptr}; - std::vector m_picksceneryitems; scene::basic_node *m_picksceneryitem{nullptr}; glm::vec3 m_worldmousecoordinates { 0.f }; bool m_mousehit { false }; @@ -997,21 +887,16 @@ class opengl46_renderer : public gfx_renderer { uint32_t m_lights_lit = 0; uint32_t m_lights_dropped = 0; // lights of models and vehicles and the cab lamp, gathered once per frame for the light clusters - opengl46_local_lights m_locallights; - // the last Update_Lights() placed the cab lamp in the light buffer: the cab is lit by it, not by an addition to the ambient light - bool m_cablamplit = false; + gfx::local_lights m_locallights; + // the ambient light of the light buffer while the cab is lit (walk_ambient_save()) + packed_vec3 m_cabambient { 0.f }; // cube shadow maps of the lamps opengl46_local_shadows m_localshadows; // the shadow pass draws to a cube shadow map, with the programs of m_localshadows bool m_cubeshadows = false; - // the shadow pass draws the kept scenery part of the map of a lamp: no vehicles, and not the model of the lamp itself - bool m_cubescenery = false; - TAnimModel const *m_cubeskipinstance = nullptr; // 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; - // the shadow pass leaves this vehicle out (the one the headlights of the map being drawn belong to) - TDynamicObject const *m_shadowskipvehicle = nullptr; // headlights which may get shadow maps in this frame, and their importance std::vector> m_headlightshadowcandidates; std::vector m_headlightshadowpicked; @@ -1041,44 +926,7 @@ class opengl46_renderer : public gfx_renderer { // matrices of their instances past the ones placed earlier in the frame, and draw them from that base instance. // persistently mapped with Global.gfx_bufferstreaming std::unique_ptr m_instancestream; - // when > 0, the renderer is inside a Render_Instanced() call. The draw(handle) - // method routes to glDrawElementsInstanced* with this many instances from - // m_current_instance_base onwards rather than a single regular draw. The vertex shader reads - // per-instance modelview from instance_ssbo[gl_BaseInstance + gl_InstanceID]. Reset to 0 on exit. - std::size_t m_current_instance_count { 0 }; - std::uint32_t m_current_instance_base { 0 }; - // camera-space transforms of the sleepers and timbers drawn by Render_Timbers, by the model and its part or its level of - // detail; kept from frame to frame to reuse their allocations - struct timber_bucket { - track_sleepers::beam_model const *model { nullptr }; - int part { -1 }; // of a model put together from parts; -1: the whole model - float band { 0.f }; // the squared distance the level of detail of the whole model is seen from - std::vector instances; - }; - std::vector m_timberbuckets; - // persistent scratch buffer for Render_Instanced(): holds the per-instance - // camera-space root modelview matrices for the batch currently being built. - // Kept as a member rather than a function-local so its heap allocation is - // reused across calls -- clear() retains capacity, so once it has grown to - // the largest batch seen, steady-state frames perform no allocation here. - std::vector m_instance_modelviews; - // further persistent scratch buffers for Render_Instanced(): culled instances paired with their - // squared distance, and the lod bounds of the drawn model which split them into lod bands - struct instance_survivor { - glm::mat4 modelview; - float distancesquared; - std::uint32_t band; - }; - std::vector m_instance_survivors; - std::vector m_instance_lodbounds; - std::vector m_instance_lodpending; - // per sort key (lod band and distance slot): first index in m_instance_modelviews (with end sentinel) - // and fill cursor; per lod band: nearest distance - std::vector m_instance_bandstarts; - std::vector m_instance_bandcursors; - std::vector m_instance_banddistances; gl::scene_ubs scene_ubs; - gl::model_ubs model_ubs; gl::light_data light_data; std::unordered_map> m_shaders; @@ -1167,8 +1015,6 @@ class opengl46_renderer : public gfx_renderer { std::unique_ptr ldr_tex; std::unique_ptr ldr_fb; } m_preview; - // set while a preview is drawn: submodels faded out entirely are skipped, instead of being drawn invisible over the stencil - bool m_previewing { false }; static bool renderer_register; diff --git a/rendering/vulkan14/vulkan14bindings.h b/rendering/vulkan14/vulkan14bindings.h index dff38364..ebca9d2a 100644 --- a/rendering/vulkan14/vulkan14bindings.h +++ b/rendering/vulkan14/vulkan14bindings.h @@ -12,9 +12,9 @@ http://mozilla.org/MPL/2.0/. // same values for the SPIR-V binaries, which glslc compiles without the renderer (the renderer checks they are the same). // // the descriptor sets (decision K6 of the plan): set 0 holds the resources of a pass, made when the renderer starts and bound -// with a dynamic offset in the ring of the frame; set 1 is pushed with the draws (vkCmdPushDescriptorSet2): the textures of -// the material and what changes between the draws. a layout can have only one set pushed, so the passes without a set of -// their own (the user interface, the compute passes) push set 0 +// with dynamic offsets in the ring of the frame; set 1 is pushed with the draws (vkCmdPushDescriptorSet2): the textures of +// the material. a layout can have only one set pushed, so the passes without a set of their own (the user interface, the +// compute passes, the post-processing) push set 0 #pragma once @@ -28,27 +28,63 @@ namespace vk14::bindings constexpr std::uint32_t UI_SET = 0; constexpr std::uint32_t UI_TEXTURE = 0; -// the sets of the passes of the scene (from the stage D1 of the plan) +// the sets of the passes of the scene constexpr std::uint32_t PASS_SET = 0; constexpr std::uint32_t MATERIAL_SET = 1; +// set 0 of the scene: the block of the scene (gl::scene_ubs, dynamic offset in the ring of the frame) +constexpr std::uint32_t SCENE_UBO = 0; +// the blocks of the draws of the frame (gl::model_ubs each), the one of a draw picked by the push constant draw +constexpr std::uint32_t DRAW_SSBO = 1; +// the modelview matrices of the instances of the frame, element 0 the identity (draws which aren't instanced) +constexpr std::uint32_t INSTANCE_SSBO = 2; +// the light buffer of the pass (gl::light_data, dynamic offset) +constexpr std::uint32_t LIGHT_SSBO = 3; +// the light clusters: the grid of the pass (gl::cluster_ubs, dynamic offset), the lists of the lights of the clusters +constexpr std::uint32_t CLUSTER_UBO = 4; +constexpr std::uint32_t CLUSTER_RANGE_SSBO = 5; +constexpr std::uint32_t CLUSTER_INDEX_SSBO = 6; +// the cascades of the sun shadows (depth compared), the environment map, the beams of the headlights +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 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; +// the compute passes of the light clusters, set 0 pushed +constexpr std::uint32_t CL_CLUSTER_UBO = 0; +constexpr std::uint32_t CL_AABB_SSBO = 1; +constexpr std::uint32_t CL_RANGE_SSBO = 2; +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 +constexpr std::uint32_t POST_SET = 0; +constexpr std::uint32_t POST_TEXTURE_FIRST = 0; + +// 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) +constexpr std::uint32_t PUSH_CONSTANTS_SIZE = 112; + // the specialization constants of the shaders // 1: the colour written to the window is encoded by the shader (a window without an srgb format, gfx.shadergamma yes) constexpr std::uint32_t SPEC_OUTPUT_GAMMA = 0; +// the settings of the game (gfx.* in eu07.ini) the shaders of the scene depend on, as in shaders/ogl46/settings.glsl +constexpr std::uint32_t SPEC_SHADOWMAP_ENABLED = 1; +constexpr std::uint32_t SPEC_ENVMAP_ENABLED = 2; +constexpr std::uint32_t SPEC_MOTIONBLUR_ENABLED = 3; +constexpr std::uint32_t SPEC_POSTFX_ENABLED = 4; +constexpr std::uint32_t SPEC_EXTRAEFFECTS_ENABLED = 5; +constexpr std::uint32_t SPEC_COUNT = 6; } // namespace vk14::bindings namespace vk14 { -// the bindings above as #define lines, with MASZYNA_CONSTANTS, which tells shaders/vk14/common.glsl not to write its own -inline std::string binding_preamble() -{ - auto const define = [](char const *Name, std::uint32_t const Value) { return std::string("#define ") + Name + " " + std::to_string(Value) + "\n"; }; - return define("MASZYNA_CONSTANTS", 1) + define("UI_SET", bindings::UI_SET) + define("UI_TEXTURE", bindings::UI_TEXTURE) + define("PASS_SET", bindings::PASS_SET) + define("MATERIAL_SET", bindings::MATERIAL_SET) + - define("MATERIAL_TEXTURE_FIRST", bindings::MATERIAL_TEXTURE_FIRST) + define("SPEC_OUTPUT_GAMMA", bindings::SPEC_OUTPUT_GAMMA); -} +// the bindings above as #define lines, with the constants of the game the shaders share with the opengl renderer (the sizes of +// the blocks, the cluster grid), and MASZYNA_CONSTANTS, which tells shaders/vk14/common.glsl not to write its own +std::string binding_preamble(); } // namespace vk14 diff --git a/rendering/vulkan14/vulkan14context.h b/rendering/vulkan14/vulkan14context.h new file mode 100644 index 00000000..e1271234 --- /dev/null +++ b/rendering/vulkan14/vulkan14context.h @@ -0,0 +1,322 @@ +/* +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 objects of the vulkan api of the vulkan renderer (vulkan14_renderer::context), for the files of the renderer only: +// the core (vulkan14renderer.cpp) and the scene (vulkan14scene.cpp) + +#pragma once + +#include "rendering/vulkan14/vulkan14renderer.h" +#include "rendering/vulkan14/vulkan14common.h" +#include "rendering/vulkan14/vulkan14instance.h" +#include "rendering/vulkan14/vulkan14device.h" +#include "rendering/vulkan14/vulkan14swapchain.h" +#include "rendering/vulkan14/vulkan14memory.h" +#include "rendering/vulkan14/vulkan14shaders.h" +#include "rendering/vulkan14/vulkan14descriptors.h" +#include "rendering/vulkan14/vulkan14passes.h" +#include "rendering/vulkan14/vulkan14texture.h" +#include "rendering/vulkan14/vulkan14imgui.h" +#include "rendering/vulkan14/vulkan14bindings.h" +#include "rendering/vulkan14/vulkan14geometry.h" +#include "model/material.h" +#include "gl/ubo.h" + +#include +#include +#include +#include + +namespace vk14 +{ + +// how the vertices of a program of the scene come +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 + particle, // position, colour and texture coordinates in one stream, the smoke + position_uv // position and texture coordinates in two streams, the precipitation +}; + +// the push constants of the pipelines of the scene and of the post-processing (shaders/vk14/common.glsl, draw_constants_block) +struct draw_constants +{ + std::uint32_t draw{0}; + std::uint32_t flags{0}; + std::uint32_t pad0{0}; + std::uint32_t pad1{0}; + glm::vec4 data[6]{}; +}; +static_assert(sizeof(draw_constants) == bindings::PUSH_CONSTANTS_SIZE, "the push constants don't match the shaders"); + +// a program of the scene: the shaders and the pipelines made of them for the targets and the states they were drawn with +struct scene_program : public gfx::shader_program +{ + std::shared_ptr vertex; + std::shared_ptr fragment; + // 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; + vertex_kind vertices{vertex_kind::scene}; + std::unordered_map pipelines; +}; + +// what a pass of the scene draws to; the pipelines are made for these +struct target_formats +{ + std::array colour{VK_FORMAT_UNDEFINED, VK_FORMAT_UNDEFINED}; + std::uint32_t colours{0}; + VkFormat depth{VK_FORMAT_UNDEFINED}; + VkSampleCountFlagBits samples{VK_SAMPLE_COUNT_1_BIT}; + // the number the pipelines of the targets are known by + std::uint32_t id{0}; +}; + +// the images a viewport draws its scene to, made for its size and the settings +struct viewport_targets +{ + VkExtent2D extent{0, 0}; + VkSampleCountFlagBits samples{VK_SAMPLE_COUNT_1_BIT}; + bool motion{false}; // the velocity is drawn, for the motion blur + bool aberration{false}; + // with more than one sample: the images drawn to, resolved to the ones below + std::shared_ptr msaa_colour; + std::shared_ptr msaa_motion; + std::shared_ptr msaa_depth; + // the colour of the scene (hdr), the velocity and the depth: the resolved ones, or the ones drawn to with one sample + std::shared_ptr colour; + std::shared_ptr motion_image; + std::shared_ptr depth; + // the colour after the motion blur (hdr) + std::shared_ptr blurred; + // the tonemapped image, before the chromatic aberration, and the one which goes to the window + std::shared_ptr tonemapped; + 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; + target_formats formats; + target_formats output_formats; + bool srgb{true}; // the output encodes the colour (gfx.shadergamma no) +}; + +// the ring of a frame some data of the draws goes through, the descriptor of which reads a window of it: the window starts again +// further on when it's full, and the pass set is bound again with the new offset +struct window_ring +{ + frame_ring ring; + VkDeviceSize window{0}; // the size of the window, bytes + VkDeviceSize stride{0}; + VkDeviceSize base{0}; // where the current window starts + bool open{false}; +}; + +// the light clusters (gl::cluster_grid), of the colour pass of each viewport in turn +struct cluster_state +{ + buffer aabbs; + buffer ranges; + buffer indices; + buffer counters; + gl::cluster_ubs params{}; + 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}; +}; + +// 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 +struct readback +{ + std::shared_ptr data; + std::uint64_t value{0}; // of the timeline + bool pending{false}; + glm::ivec2 position{0}; +}; + +// what the scene is drawn with +struct scene_state +{ + // the layouts: of the pass set (set 0), the pushed set of the materials (set 1), of the pipelines of the scene, the + // post-processing and the compute passes of the clusters + VkDescriptorSetLayout pass_layout{VK_NULL_HANDLE}; + push_set material_set; + VkPipelineLayout scene_layout{VK_NULL_HANDLE}; + push_set post_set; + VkPipelineLayout post_layout{VK_NULL_HANDLE}; + push_set cluster_set; + VkPipelineLayout cluster_layout{VK_NULL_HANDLE}; + // the pass sets: made with the pool when the images they read are made anew + VkDescriptorPool pass_pool{VK_NULL_HANDLE}; + enum pass_set_kind + { + set_colour, // the shadow map and the environment map + set_reflections, // the shadow map and the empty environment map + set_plain, // the empty maps (shadows, picking) + set_count + }; + std::array pass_sets{}; + bool pass_sets_stale{true}; + // 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; + // the images every pass may read: the stand-ins, the shadow maps, the environment map + std::shared_ptr white; // a texture of a material which isn't there + std::shared_ptr shadow_empty; + std::shared_ptr shadow_map; + std::vector shadow_layers; // a view of each cascade + std::uint32_t shadow_size{0}; + std::shared_ptr env_empty; + std::shared_ptr env_map; + std::shared_ptr env_depth; + std::array env_faces{}; + std::shared_ptr pick_colour; + std::shared_ptr pick_depth; + VkImageView headlights_view{VK_NULL_HANDLE}; // the one the pass sets were made with + cluster_state clusters; + // the targets of the viewports, [0] the main window + std::vector targets; + // the formats of the passes which don't depend on the viewport + target_formats shadow_formats; + target_formats reflection_formats; + target_formats pick_formats; + std::uint32_t formats_count{0}; + // 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 post-processing, by the fragment shader + std::unordered_map> post_fragments; + std::shared_ptr quad; + VkPipeline cluster_build{VK_NULL_HANDLE}; + VkPipeline cluster_cull{VK_NULL_HANDLE}; + // the reading of the colour under the cursor of the picking passes and of the depth under the cursor of the editor + readback pick_controls; + readback pick_scenery; + readback cursor; + // the screenshot asked for, copied with the frame + std::shared_ptr screenshot; + std::uint64_t screenshot_value{0}; + VkExtent2D screenshot_extent{0, 0}; + bool screenshot_pending{false}; + bool screenshot_wanted{false}; + // the occlusion queries of the points of light of the lamps, a pool for each frame in flight with the submodels they were made + // for; the glare of a lamp is drawn where its point was seen in the frame the gpu was done with last + std::vector occlusion_pools; + std::vector> occlusion_submodels; + std::unordered_map glare_visible; + static constexpr std::uint32_t occlusion_queries{1024}; + // the images of the scene were put in their layouts (the stand-ins cleared) + bool images_ready{false}; +}; + +} // namespace vk14 + +// the objects of the vulkan api. destroyed in the reverse order: the swapchains and the frames, then the device, then the instance +struct vulkan14_renderer::context +{ + struct frame + { + 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 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 + std::uint64_t number{0}; + }; + + vk14::instance instance; + vk14::device device; + vk14::allocator memory; + vk14::deletion_queue deletions; + vk14::uploader uploads; + vk14::image_table images; + vk14::samplers sampling; + vk14::frame_ring ring; + vk14::shader_library shaders; + vk14::pipeline_library pipelines; + vk14::pass_list passes; + vk14::pending_textures pending; + vk14::resources resources; + vk14::imgui_backend imgui; + vk14::scene_state scene; + // the textures of the files; made after the objects above and gone before them + std::unique_ptr textures; + // [0]: the main window, then the windows of the extra viewports + std::vector> windows; + std::vector frames; + // one semaphore counts the frames done by the gpu; the binary ones are only between the swapchains and the queue + VkSemaphore timeline{VK_NULL_HANDLE}; + std::uint64_t timelinevalue{0}; + // frames recorded since the start + std::uint64_t framecount{0}; + // the windows with an image acquired in the frame being made + std::vector acquired; + 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}; + + ~context() + { + destroy(); + } + + bool create_frames(std::uint32_t const Count, std::string &Reason); + // 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 + std::uint64_t completed() const; + // waits until the gpu reached the value of the timeline; false: the device is lost + bool wait(std::uint64_t const Value) const; + void destroy(); +}; + +// 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::target_formats const *targets{nullptr}; + VkExtent2D extent{0, 0}; + vk14::scene_state::pass_set_kind set{vk14::scene_state::set_plain}; + bool rendering{false}; // inside a rendering of a pass of the scene + bool shadows{false}; // a pass of the sun shadows: the back faces are drawn + // the offsets of the pass set in the rings of the frame + std::uint32_t scene_offset{0}; + std::uint32_t light_offset{0}; + std::uint32_t cluster_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}; + std::array textures{}; + bool textures_dirty{true}; + // the block of the draw last pushed, for the chunks drawn together + std::uint32_t block{0}; + // what is bound + VkPipeline pipeline{VK_NULL_HANDLE}; + VkBuffer vertices{VK_NULL_HANDLE}; + VkBuffer userdata{VK_NULL_HANDLE}; + VkBuffer indices{VK_NULL_HANDLE}; + VkPrimitiveTopology topology{VK_PRIMITIVE_TOPOLOGY_MAX_ENUM}; + VkCullModeFlags cull{VK_CULL_MODE_FLAG_BITS_MAX_ENUM}; + int depthtest{-1}; + int depthwrite{-1}; + std::uint32_t pushed_draw{~0u}; + // the state of the walk (setup_drawing() and the glare of the opengl renderer) + bool blending{false}; + bool cullface{true}; + bool glare{false}; + bool depthmask_off{false}; // the editor overlay: no depth written whatever the blending + // the occlusion queries of the glare of the lamps in the frame: the submodels, by the index of their query + std::vector queries; + std::uint32_t query{~0u}; // the query under way + // the draws made in the pass, for the debug panel + std::uint32_t draws{0}; +}; diff --git a/rendering/vulkan14/vulkan14descriptors.cpp b/rendering/vulkan14/vulkan14descriptors.cpp index 1a5a21ea..f0238e01 100644 --- a/rendering/vulkan14/vulkan14descriptors.cpp +++ b/rendering/vulkan14/vulkan14descriptors.cpp @@ -37,9 +37,10 @@ bool samplers::create(device const &Device, float Anisotropy, std::string &Reaso } if (Compare) { - // reverse z: the fragment is lit where it's nearer the light than the map, its depth greater + // as GL_LEQUAL of the opengl renderer: 1 (in the shadow) where the reference isn't nearer the light than the map (reverse + // z: its depth isn't greater), the shaders take the result as the amount of the shadow info.compareEnable = VK_TRUE; - info.compareOp = VK_COMPARE_OP_GREATER_OR_EQUAL; + info.compareOp = VK_COMPARE_OP_LESS_OR_EQUAL; info.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK; } if (!VK14_VERIFY(vkCreateSampler(handle, &info, nullptr, &Sampler))) @@ -53,7 +54,7 @@ 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_BORDER, false, 1.f, true, "shadow"); + make(shadow, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, false, 1.f, true, "shadow"); } void samplers::destroy(VkDevice Device) diff --git a/rendering/vulkan14/vulkan14frame.cpp b/rendering/vulkan14/vulkan14frame.cpp new file mode 100644 index 00000000..068aa49f --- /dev/null +++ b/rendering/vulkan14/vulkan14frame.cpp @@ -0,0 +1,2839 @@ +/* +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 scene of the vulkan renderer, part two (the stages D3-D7 of the plan): the frame and its passes, in the order of +// opengl46_renderer::Render_pass(): the cascades of the sun shadows, the faces of the environment map, the light clusters, the +// colour of the scene (the sky, the opaque scene with the cab, the translucent parts, the particles, the precipitation, the cab), +// the post-processing (the motion blur, the tonemapping, the chromatic aberration), the output to the windows with the user +// interface; the picking, the position of the cursor of the editor, the screenshots, the previews of the models + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14context.h" +#include "rendering/screenshot.h" +#include "rendering/editoroverlay.h" +#include "application/application.h" +#include "simulation/simulation.h" +#include "simulation/simulationenvironment.h" +#include "simulation/simulationtime.h" +#include "vehicle/Train.h" +#include "vehicle/DynObj.h" +#include "model/Model3d.h" +#include "model/AnimModel.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/Timer.h" +#include "utilities/utilities.h" +#include "utilities/color.h" + +namespace +{ + +// the size of the (square) images of the picking passes +int constexpr pick_buffer_size{1024}; + +// the projections of the opengl renderer (opengl46_renderer::perspective_projection() and the others): reverse z with the +// range 1..0, an infinite far plane for the perspective ones +glm::mat4 perspective_projection(viewport_proj_config const &c, float n, float f, glm::mat4 &frustum) +{ + glm::vec3 vr, vu, vn; + vr = glm::normalize(c.pb - c.pa); + vu = glm::normalize(c.pc - c.pa); + vn = glm::normalize(glm::cross(vr, vu)); + + glm::mat4 M; + M[0] = glm::vec4(vr, 0.0f); + M[1] = glm::vec4(vu, 0.0f); + M[2] = glm::vec4(vn, 0.0f); + M = glm::transpose(M); + + glm::vec3 va, vb, vc; + va = c.pa - c.pe; + vb = c.pb - c.pe; + vc = c.pc - c.pe; + + float l, r, b, t, d; + d = -glm::dot(va, vn); + + l = glm::dot(vr, va) * n / d; + r = glm::dot(vr, vb) * n / d; + b = glm::dot(vu, va) * n / d; + t = glm::dot(vu, vc) * n / d; + + frustum = glm::frustum(l, r, b, t, n, f); + glm::mat4 R = frustum; + frustum = glm::translate(frustum * M, -c.pe); + + R[2][2] = -1.0f; + R[3][2] = -2.0f * n; + R = glm::mat4(1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, -0.5f, 0.0f, 0.0f, 0.0f, 0.5f, 1.0f) * R; + + return glm::translate(R * M, -c.pe); +} + +glm::mat4 perspective_projection_raw(float fovy, float aspect, float znear, float zfar) +{ + const float f = 1.0f / std::tan(fovy / 2.0f); + return glm::mat4(f / aspect, 0.0f, 0.0f, 0.0f, 0.0f, f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, znear, 0.0f); +} + +glm::mat4 ortho_projection(float l, float r, float b, float t, float znear, float zfar) +{ + glm::mat4 proj = glm::ortho(l, r, b, t, znear, zfar); + return glm::mat4(1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, -0.5f, 0.0f, 0.0f, 0.0f, 0.5f, 1.0f) * proj; +} + +glm::mat4 ortho_frustumtest_projection(float l, float r, float b, float t, float znear, float zfar) +{ + return glm::ortho(l, r, b, t, znear, zfar); +} + +// transform 1..-1 NDC xy coordinates to 1..0 (the z is 1..0 already) +glm::mat4 const coordmove{0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.5, 0.5, 0.0, 1.0}; + +// the vertices of a particle of the smoke (opengl46_particles) +struct particle_vertex +{ + glm::vec3 position; + glm::vec4 color; + glm::vec2 texture; +}; +static_assert(sizeof(particle_vertex) == 36, "the layout of the vertices of the particles"); + +std::vector> const billboard_vertices{ + {{-0.5f, -0.5f, 0.f}, {0.f, 0.f}}, {{0.5f, -0.5f, 0.f}, {1.f, 0.f}}, {{0.5f, 0.5f, 0.f}, {1.f, 1.f}}, {{-0.5f, -0.5f, 0.f}, {0.f, 0.f}}, {{0.5f, 0.5f, 0.f}, {1.f, 1.f}}, {{-0.5f, 0.5f, 0.f}, {0.f, 1.f}}, +}; + +// the cone of the precipitation around the camera (opengl46_precipitation::create()) +struct precipitation_mesh +{ + std::vector vertices; + std::vector uvs; + std::vector indices; + + void create(int const Tesselation) + { + auto const heightfactor{10.f}; + auto const verticaltexturestretchfactor{1.5f}; + auto const latitudes{3}; + auto const longitudes{Tesselation}; + auto const longitudehalfstep{0.5f * static_cast(2.0 * M_PI * 1.f / longitudes)}; + std::uint16_t index = 0; + std::vector radii{25.f, 10.f, 5.f, 1.f}; + for (auto radius : radii) + { + for (int i = 0; i <= latitudes; ++i) + { + auto const latitude{static_cast(M_PI * (-0.5f + (float)(i) / latitudes))}; + auto const z{std::sin(latitude)}; + auto const zr{std::cos(latitude)}; + for (int j = 0; j <= longitudes; ++j) + { + auto const longitude{static_cast(2.0 * M_PI * (float)(j) / longitudes)}; + auto const x{std::cos(longitude)}; + auto const y{std::sin(longitude)}; + vertices.emplace_back(glm::vec3(-x * zr, -z * heightfactor, -y * zr) * radius); + auto u = (i == 0 ? longitude + longitudehalfstep : i == latitudes ? longitude - longitudehalfstep : longitude); + uvs.emplace_back(u / (2.0 * M_PI) * radius, 1.f - (float)(i) / latitudes * radius * heightfactor * 0.5f / verticaltexturestretchfactor); + if ((i == 0) || (j == 0)) + { + ++index; + } + else + { + if (i < latitudes) + { + indices.emplace_back(index - 1 - (longitudes + 1)); + indices.emplace_back(index - 1); + indices.emplace_back(index); + } + if (i > 1) + { + indices.emplace_back(index); + indices.emplace_back(index - (longitudes + 1)); + indices.emplace_back(index - 1 - (longitudes + 1)); + } + ++index; + } + } + } + } + } +}; + +precipitation_mesh &precipitation() +{ + static precipitation_mesh mesh; + if (mesh.vertices.empty()) + { + mesh.create(18); + } + return mesh; +} + +VkPipelineStageFlags2 const colour_stage{VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT}; +VkAccessFlags2 const colour_access{VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT}; +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}; +// a depth resolved by a rendering: written in the stage of the colour output +VkPipelineStageFlags2 const resolve_depth_stage{VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT}; +VkAccessFlags2 const resolve_depth_access{VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT | 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}; +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) +{ + VkMemoryBarrier2 barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; + barrier.srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT; + barrier.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT; + barrier.dstStageMask = VK_PIPELINE_STAGE_2_HOST_BIT; + barrier.dstAccessMask = VK_ACCESS_2_HOST_READ_BIT; + VkDependencyInfo dependency{}; + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency.memoryBarrierCount = 1; + dependency.pMemoryBarriers = &barrier; + vkCmdPipelineBarrier2(Commands, &dependency); +} + +// the images the colour of a viewport is drawn to, as its rendering uses them +std::vector colour_target_uses(vk14::viewport_targets &targets) +{ + auto const msaa{targets.samples != VK_SAMPLE_COUNT_1_BIT}; + std::vector uses; + auto &colourimage{msaa ? *targets.msaa_colour : *targets.colour}; + auto &depthimage{msaa ? *targets.msaa_depth : *targets.depth}; + uses.push_back(vk14::use_of(colourimage, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, colour_stage, colour_access)); + uses.push_back(vk14::use_of(depthimage, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)); + if (msaa) + { + uses.push_back(vk14::use_of(*targets.colour, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, colour_stage, colour_access)); + uses.push_back(vk14::use_of(*targets.depth, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, resolve_depth_stage, resolve_depth_access)); + } + if (targets.motion) + { + uses.push_back(vk14::use_of(msaa ? *targets.msaa_motion : *targets.motion_image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, colour_stage, colour_access)); + if (msaa) + { + uses.push_back(vk14::use_of(*targets.motion_image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, colour_stage, colour_access)); + } + } + return uses; +} + +VkRenderingAttachmentInfo attachment_info(VkImageView View, VkImageLayout Layout, bool const Clear, VkClearValue const &Value) +{ + VkRenderingAttachmentInfo info{}; + info.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; + info.imageView = View; + info.imageLayout = Layout; + info.loadOp = (Clear ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD); + info.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + info.clearValue = Value; + return info; +} + +} // namespace + +// the data of the frames, prepared once the renderer is there + +void vulkan14_renderer::init_frame_data() +{ + auto &ctx{*m_context}; + // rgb value for 5780 kelvin + Global.DayLight.diffuse[0] = 1.0f; + Global.DayLight.diffuse[1] = 0.9490196f; + Global.DayLight.diffuse[2] = 0.9058824f; + Global.DayLight.is_directional = true; + + auto const texturesize{static_cast(ctx.device.caps().limits.maxImageDimension2D)}; + Global.iMaxTextureSize = std::min(Global.iMaxTextureSize, texturesize); + Global.iMaxCabTextureSize = std::min(Global.iMaxCabTextureSize, texturesize); + Global.CurrentMaxTextureSize = Global.iMaxTextureSize; + WriteLog("texture sizes capped at " + std::to_string(Global.iMaxTextureSize) + "p (" + std::to_string(Global.iMaxCabTextureSize) + "p for cab textures)"); + if (Global.iMultisampling) + { + WriteLog("using multisampling x" + std::to_string(1 << Global.iMultisampling)); + } + if (Global.gfx_framebuffer_width == -1) + { + Global.gfx_framebuffer_width = Global.fb_size.x; + } + if (Global.gfx_framebuffer_height == -1) + { + Global.gfx_framebuffer_height = Global.fb_size.y; + } + WriteLog("main window size: " + std::to_string(Global.gfx_framebuffer_width) + "x" + std::to_string(Global.gfx_framebuffer_height)); + + m_viewports.clear(); + auto &main{m_viewports.emplace_back()}; + main.width = Global.gfx_framebuffer_width; + main.height = Global.gfx_framebuffer_height; + main.main = true; + main.shadow = true; + main.window = 0; + main.draw_range = 1.0f; + + // preload some common textures + WriteLog("Loading common gfx data..."); + m_glaretexture = Fetch_Texture("fx/lightglare"); + m_suntexture = Fetch_Texture("fx/sun"); + m_moontexture = Fetch_Texture("fx/moon"); + m_smoketexture = Fetch_Texture("fx/smoke"); + m_headlightstexture = Fetch_Texture("fx/headlights:st"); + WriteLog("...gfx data pre-loading done"); + + // prepare basic geometry chunks + float const size = 2.5f / 2.0f; + auto const geometrybank = Create_Bank(); + gfx::vertex_array billboard_array{ + {{-size, size, 0.f}, glm::vec3(), {1.f, 1.f}}, {{size, size, 0.f}, glm::vec3(), {0.f, 1.f}}, {{-size, -size, 0.f}, glm::vec3(), {1.f, 0.f}}, {{size, -size, 0.f}, glm::vec3(), {0.f, 0.f}}}; + gfx::userdata_array userdata{}; + m_billboardgeometry = m_geometry.create_chunk(billboard_array, userdata, geometrybank, GL_TRIANGLE_STRIP); + + m_invalid_material = Fetch_Material("invalid"); + m_fogrange = 2000.f; +} + +// the images of a viewport for its size and the settings +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) +{ + // 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))}; + while ((samples > VK_SAMPLE_COUNT_1_BIT) && (0 == (Samplecounts & samples))) + { + samples = static_cast(samples >> 1); + } + auto const motion{Global.gfx_postfx_motionblur_enabled}; + auto const aberration{Global.gfx_postfx_chromaticaberration_enabled}; + VkExtent2D const extent{static_cast(std::max(Width, 1)), static_cast(std::max(Height, 1))}; + if ((Targets.extent.width == extent.width) && (Targets.extent.height == extent.height) && (Targets.samples == samples) && (Targets.motion == motion) && (Targets.aberration == aberration) && (Targets.srgb == Srgb) && + Targets.output) + { + return true; + } + Deletions.push(Value, [old = std::move(Targets)]() {}); + auto const oldformats{Targets.formats}; + auto const oldoutput{Targets.output_formats}; + Targets = vk14::viewport_targets{}; + Targets.extent = extent; + Targets.samples = samples; + Targets.motion = motion; + Targets.aberration = aberration; + Targets.srgb = Srgb; + + auto make = [&](std::shared_ptr &Image, VkFormat Format, VkImageUsageFlags Usage, VkSampleCountFlagBits Samples, char const *Name) { + vk14::image_desc desc; + desc.format = Format; + desc.extent = {extent.width, extent.height, 1}; + desc.samples = Samples; + desc.usage = Usage; + Image = std::make_shared(); + if (false == Image->create(Memory, desc, Name)) + { + Image.reset(); + return false; + } + return true; + }; + auto const hdr{VK_FORMAT_R16G16B16A16_SFLOAT}; + auto const velocity{VK_FORMAT_R16G16_SFLOAT}; + auto const depth{VK_FORMAT_D32_SFLOAT}; + auto const sampledcolour{VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT}; + auto ok{true}; + if (samples != VK_SAMPLE_COUNT_1_BIT) + { + ok = ok && make(Targets.msaa_colour, hdr, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, samples, "msaa colour") && make(Targets.msaa_depth, depth, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, samples, "msaa depth"); + if (motion) + { + ok = ok && make(Targets.msaa_motion, velocity, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, samples, "msaa velocity"); + } + } + ok = ok && make(Targets.colour, hdr, sampledcolour, VK_SAMPLE_COUNT_1_BIT, "scene colour") && + make(Targets.depth, depth, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, VK_SAMPLE_COUNT_1_BIT, "scene depth"); + if (motion) + { + ok = ok && make(Targets.motion_image, velocity, sampledcolour, VK_SAMPLE_COUNT_1_BIT, "scene velocity") && make(Targets.blurred, hdr, sampledcolour, VK_SAMPLE_COUNT_1_BIT, "motion blurred"); + } + auto const output{Srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM}; + if (aberration) + { + 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"); + 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"); + Targets.output.reset(); + return false; + } + Targets.formats.colour = {hdr, motion ? velocity : VK_FORMAT_UNDEFINED}; + Targets.formats.colours = (motion ? 2 : 1); + Targets.formats.depth = depth; + Targets.formats.samples = samples; + Targets.output_formats.colour = {output, VK_FORMAT_UNDEFINED}; + Targets.output_formats.colours = 1; + // 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); + return true; +} + +} // namespace + +// the frame + +bool vulkan14_renderer::Render() +{ + if (!m_context || m_context->devicelost) + { + return false; + } + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto const device{ctx.device.handle()}; + auto const slot{static_cast(ctx.framecount % ctx.frames.size())}; + auto &frame{ctx.frames[slot]}; + + Timer::subsystem.gfx_total.start(); + + // the gpu has to be done with the frame which used the slot before + if (frame.done > 0) + { + if (false == ctx.wait(frame.done)) + { + report_device_lost("waiting for a frame"); + return false; + } + ctx.passes.collect(slot); + } + // what the frames done used last goes + ctx.deletions.collect(std::max(ctx.completed(), frame.done)); + VK14_VERIFY(vkResetCommandPool(device, frame.pool, 0)); + ctx.ring.begin_frame(slot); + scene.draws.ring.begin_frame(slot); + scene.instances.ring.begin_frame(slot); + scene.draws.open = false; + scene.instances.open = false; + // the draws which aren't instanced read the identity at the start of the window from the first draw of the frame on + open_instance_window(); + // the occlusion queries of the lamps the slot made the last time + { + auto &submodels{scene.occlusion_submodels[slot]}; + if (false == submodels.empty()) + { + std::vector results(submodels.size(), 1); + if (vkGetQueryPoolResults(device, scene.occlusion_pools[slot], 0, static_cast(submodels.size()), results.size() * sizeof(std::uint32_t), results.data(), sizeof(std::uint32_t), 0) == VK_SUCCESS) + { + for (std::size_t i = 0; i < submodels.size(); ++i) + { + scene.glare_visible[submodels[i]] = (results[i] > 0); + } + } + vkResetQueryPool(device, scene.occlusion_pools[slot], 0, static_cast(submodels.size())); + submodels.clear(); + } + } + + 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)); + auto const value{ctx.timelinevalue + 1}; + ctx.uploads.begin_frame(frame.uploads, value); + ctx.slot = slot; + ctx.commands = frame.commands; + // 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) + { + std::vector pending; + { + std::lock_guard lock(ctx.pending.mutex); + pending.swap(ctx.pending.textures); + } + for (auto *texture : pending) + { + texture->apply_pending(); + } + } + + ctx.acquired.clear(); + for (auto &window : ctx.windows) + { + switch (window->acquire(slot)) + { + case vk14::swapchain::acquire_result::ready: + ctx.acquired.push_back(window.get()); + break; + case vk14::swapchain::acquire_result::skipped: + break; + default: + if (window != ctx.windows.front()) + { + // a window of an extra viewport which can't be shown doesn't stop the others + break; + } + ctx.acquired.clear(); + report_device_lost("acquiring an image"); + return false; + } + } + + frame.number = ctx.framecount; + ctx.passes.begin_frame(frame.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)}); + 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)}; + // the maps which aren't drawn yet are read before they are (the pass sets hold them) + if (scene.shadow_map) + { + uses.push_back(vk14::use_of(*scene.shadow_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)); + } + if (scene.env_map) + { + 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); + // no light is assigned to any cluster until the culling pass runs + vkCmdFillBuffer(frame.commands, 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; + barrier.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT; + barrier.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + barrier.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT; + VkDependencyInfo dependency{}; + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency.memoryBarrierCount = 1; + dependency.pMemoryBarriers = &barrier; + vkCmdPipelineBarrier2(frame.commands, &dependency); + ctx.passes.end_pass(); + } + + // fetch simulation data + if (simulation::is_ready) + { + static_cast(m_sunlight) = Global.DayLight; + // quantize sun angle to reduce shadow crawl + auto const quantizationstep{0.004f}; + m_sunlight.direction = glm::normalize(quantizationstep * glm::roundEven(m_sunlight.direction * (1.f / quantizationstep))); + m_fogcolor = Global.FogColor; + // 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)) + { + m_locallights.gather(Global.pCamera.Pos, *simulation::Region, m_geometry, m_framestamp); + } + } + else + { + // a scenery is being loaded: the cells, instances and models the lights were taken from may be gone + m_locallights.reset(); + scene.glare_visible.clear(); + } + if ((EditorModeFlag) || (m_nodestateversion != scene::node_state_version)) + { + // the editor moves and resizes model instances in place, events show and hide them: the instance records are out of date + ++m_instancerecordsversion; + m_nodestateversion = scene::node_state_version; + } + m_renderpass.draw_mode = rendermode::none; + // the vehicles which moved in the last frame (motion blur) + m_vehiclesmoved = m_vehiclesmoving; + m_vehiclesmoving = false; + m_renderpass.draw_stats = debug_stats(); + m_draw->draws = 0; + + auto const drawscene{simulation::is_ready && (false == Global.gfx_skiprendering)}; + if (drawscene) + { + render_scene(); + if (m_pickcontrolswanted && simulation::Train) + { + render_picking(rendermode::pickcontrols); + } + if (m_picknodewanted) + { + render_picking(rendermode::pickscenery); + } + } + m_pickcontrolswanted = false; + m_picknodewanted = false; + + // 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(); + Application.render_ui(); + Timer::subsystem.gfx_gui.stop(); + + // the windows: the image of their viewport, the user interface over the main one + bool uidrawn{false}; + for (auto *window : ctx.acquired) + { + std::size_t viewport{m_viewports.size()}; + for (std::size_t i = 0; i < m_viewports.size(); ++i) + { + if (ctx.windows[m_viewports[i].window].get() == window) + { + viewport = i; + break; + } + } + auto const ismain{window == ctx.windows.front().get()}; + ctx.passes.begin_pass(ismain ? "ui" : "other viewports", {window->use(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, colour_stage, colour_access)}); + VkClearValue clear{}; + clear.color = {{0.f, 0.f, 0.f, 1.f}}; + auto attachment{attachment_info(window->view(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, true, clear)}; + VkRenderingInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + rendering.renderArea = {{0, 0}, window->extent()}; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &attachment; + vkCmdBeginRendering(frame.commands, &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()); + uidrawn = true; + } + vkCmdEndRendering(frame.commands); + 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)}); + } + if (false == uidrawn) + { + // the main window is minimized: the frame of imgui has nowhere to go + ctx.imgui.skip(); + } + // the screenshot asked for: the output of the main viewport, read back once the gpu is done with the frame + if (scene.screenshot_wanted && drawscene && (false == scene.targets.empty()) && scene.targets.front().output && (false == scene.screenshot_pending)) + { + scene.screenshot_wanted = false; + auto &output{*scene.targets.front().output}; + auto const extent{scene.targets.front().extent}; + 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)}); + VkBufferImageCopy2 region{}; + region.sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageExtent = {extent.width, extent.height, 1}; + VkCopyImageToBufferInfo2 copy{}; + copy.sType = VK_STRUCTURE_TYPE_COPY_IMAGE_TO_BUFFER_INFO_2; + copy.srcImage = output.handle(); + copy.srcImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + copy.dstBuffer = scene.screenshot->handle(); + copy.regionCount = 1; + copy.pRegions = ®ion; + vkCmdCopyImageToBuffer2(frame.commands, ©); + host_read_barrier(frame.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)); + 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); + + 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); + } + 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); + + // the uploads first, in the same submit: their barriers make them done before the frame uses what they copied + std::vector commands; + if (uploaded) + { + VkCommandBufferSubmitInfo info{}; + info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; + info.commandBuffer = frame.uploads; + commands.push_back(info); + } + { + VkCommandBufferSubmitInfo info{}; + info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; + info.commandBuffer = frame.commands; + commands.push_back(info); + } + + 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(); + return true; +} + +void vulkan14_renderer::render_scene() +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + if (scene.targets.size() < m_viewports.size()) + { + scene.targets.resize(m_viewports.size()); + } + for (std::size_t i = 0; i < m_viewports.size(); ++i) + { + 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)) + { + continue; + } + render_viewport(i); + } + m_current_viewport = (m_viewports.empty() ? nullptr : &m_viewports.front()); +} + +// the passes of the walk + +void vulkan14_renderer::start_draws(vk14::target_formats const &Targets, int const Width, int const Height, int const Set, bool const Shadows) +{ + auto &draw{*m_draw}; + draw.commands = m_context->commands; + draw.targets = &Targets; + draw.extent = {static_cast(Width), static_cast(Height)}; + draw.set = static_cast(Set); + draw.shadows = Shadows; + draw.rendering = true; + draw.set_dirty = true; + draw.textures_dirty = true; + draw.pipeline = VK_NULL_HANDLE; + draw.vertices = VK_NULL_HANDLE; + draw.userdata = VK_NULL_HANDLE; + draw.indices = VK_NULL_HANDLE; + draw.topology = VK_PRIMITIVE_TOPOLOGY_MAX_ENUM; + draw.cull = VK_CULL_MODE_FLAG_BITS_MAX_ENUM; + draw.depthtest = -1; + draw.depthwrite = -1; + draw.pushed_draw = ~0u; + draw.block = ~0u; + draw.cullface = true; + draw.glare = false; + draw.depthmask_off = false; + 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); +} + +void vulkan14_renderer::end_draws() +{ + auto &draw{*m_draw}; + draw.rendering = false; + draw.program = nullptr; +} + +void vulkan14_renderer::upload_scene_block() +{ + auto &ctx{*m_context}; + auto const slice{ctx.ring.allocate(sizeof(gl::scene_ubs), ctx.device.caps().limits.minUniformBufferOffsetAlignment)}; + if (false == bool(slice)) + { + return; + } + std::memcpy(slice.data, &scene_ubs, sizeof(gl::scene_ubs)); + m_draw->scene_offset = static_cast(slice.offset); + m_draw->set_dirty = true; +} + +void vulkan14_renderer::upload_light_header() +{ + // the header with the lights in use: what changes between the draws (the sun in tunnels, the ambient light of the cab) goes + // with a copy of the lights, the shaders read the buffer whole + auto &ctx{*m_context}; + // the lights stay the same between the calls of update_lights(): a header with the sun given already goes again + auto const *header{reinterpret_cast(&light_data)}; + for (auto const &[copy, offset] : m_lightheaders) + { + if (0 == std::memcmp(copy.data(), header, copy.size())) + { + if (m_draw && (m_draw->light_offset != offset)) + { + m_draw->light_offset = offset; + m_draw->set_dirty = true; + } + return; + } + } + auto const size{offsetof(gl::light_data, lights) + sizeof(gl::light_element) * std::max(m_lightcount, 1)}; + auto const slice{ctx.ring.allocate(size, ctx.device.caps().limits.minStorageBufferOffsetAlignment)}; + if (false == bool(slice)) + { + return; + } + std::memcpy(slice.data, &light_data, size); + if (m_lightheaders.size() < 32) + { + auto &entry{m_lightheaders.emplace_back()}; + std::memcpy(entry.first.data(), header, entry.first.size()); + entry.second = static_cast(slice.offset); + } + if (m_draw) + { + m_draw->light_offset = static_cast(slice.offset); + m_draw->set_dirty = true; + } +} + +void vulkan14_renderer::upload_lights() +{ + upload_light_header(); +} + +void vulkan14_renderer::setup_matrices() +{ + OpenGLMatrices.mode(GL_PROJECTION); + OpenGLMatrices.load_matrix(m_renderpass.pass_camera.projection()); + // trim modelview matrix just to rotation, since rendering is done in camera-centric world space + OpenGLMatrices.mode(GL_MODELVIEW); + OpenGLMatrices.load_matrix(glm::mat4{glm::mat3{m_renderpass.pass_camera.modelview()}}); +} + +void vulkan14_renderer::setup_shadow_matrices() +{ + if (false == Global.gfx_shadowmap_enabled) + { + return; + } + for (std::size_t idx = 0; idx < m_shadowpass.size(); ++idx) + { + glm::mat4 depthproj = m_shadowpass[idx].pass_camera.projection(); + // NOTE: we strip transformations from camera projections to remove jitter that occurs + // with large (and unneded as we only need the offset) transformations back and forth + auto const depthcam{glm::mat3{m_shadowpass[idx].pass_camera.modelview()}}; + auto const worldcam{glm::mat3{m_renderpass.pass_camera.modelview()}}; + scene_ubs.lightview[idx] = coordmove * depthproj * glm::translate(glm::mat4{depthcam}, glm::vec3{m_renderpass.pass_camera.position() - m_shadowpass[idx].pass_camera.position()}) * glm::mat4{glm::inverse(worldcam)}; + } +} + +// the camera of the pass, as opengl46_renderer::setup_pass() sets it up (without the vr and the cascades fitted to the depth) +void vulkan14_renderer::setup_pass(viewport_config &Viewport, renderpass_config &Config, rendermode const Mode, float const Znear, float const Zfar, bool const Ignoredebug) +{ + Config.draw_mode = Mode; + + if (false == simulation::is_ready) + { + return; + } + switch (Mode) + { + case rendermode::color: + Config.draw_range = Global.BaseDrawRange * Global.fDistanceFactor * Viewport.draw_range; + break; + case rendermode::shadows: + Config.draw_range = Global.shadowtune.range; + break; + case rendermode::reflections: + Config.draw_range = Global.BaseDrawRange * Viewport.draw_range; + break; + case rendermode::pickcontrols: + Config.draw_range = 50.f; + break; + case rendermode::pickscenery: + Config.draw_range = Global.BaseDrawRange * Viewport.draw_range * 0.5f; + break; + default: + Config.draw_range = 0.f; + break; + } + + auto &camera = Config.pass_camera; + glm::dmat4 viewmatrix(1.0); + glm::mat4 frustumtest_proj; + Config.viewport_camera.position() = Global.pCamera.Pos; + + auto const editor_ortho_projection = [](viewport_config const &Viewport, float const Zfar, glm::mat4 &Projection, glm::mat4 &Frustum) { + if (false == (EditorModeFlag && Global.EditorOrtho && Viewport.main)) + { + return; + } + auto const height{Global.EditorOrthoExtent}; + auto const width{height * Global.window_size.x / std::max(1.f, static_cast(Global.window_size.y))}; + Projection = ortho_projection(-width, width, -height, height, -Zfar, Zfar); + Frustum = ortho_frustumtest_projection(-width, width, -height, height, -Zfar, Zfar); + }; + + switch (Mode) + { + case rendermode::color: + { + if ((Viewport.proj_type == viewport_config::normal) && Viewport.main) + { + float const fovy = glm::radians(Global.FieldOfView / Global.ZoomFactor); + // setup virtual screen + glm::vec2 screen_h = glm::vec2(Global.window_size) / 2.0f; + float const dist = screen_h.y / glm::tan(fovy / 2.0f); + Viewport.projection.pa = glm::vec3(-screen_h.x, -screen_h.y, -dist); + Viewport.projection.pb = glm::vec3(screen_h.x, -screen_h.y, -dist); + Viewport.projection.pc = glm::vec3(-screen_h.x, screen_h.y, -dist); + Viewport.projection.pe = glm::vec3(0.0f, 0.0f, 0.0f); + } + if (Global.headtrack_conf.magic_window) + { + Viewport.projection.pe = Global.viewport_move; + } + TCamera *cam = ((false == DebugCameraFlag) || (true == Ignoredebug)) ? &Global.pCamera : &Global.pDebugCamera; + camera.position() = cam->Pos; + cam->SetMatrix(viewmatrix); + if (!Global.headtrack_conf.magic_window) + { + camera.position() += Global.viewport_move * glm::mat3(viewmatrix); + viewmatrix = glm::dmat4(glm::inverse(Global.viewport_rotate)) * viewmatrix; + } + // optimization: don't bother drawing things completely blended with the background + // but ensure at least 2 km range to account for signals and lights + Config.draw_range = std::min(Config.draw_range, m_fogrange * 2); + Config.draw_range = std::max(2000.f, Config.draw_range); + auto zfar = (Zfar > 1.f ? Zfar : Config.draw_range * Zfar); + auto const znear = (Znear > 1.f ? Znear : Znear > 0.f ? Znear * zfar : 0.1f * Global.ZoomFactor); + if (EditorModeFlag && Viewport.main) + { + zfar = editor_track_view_range(zfar, true); + } + Config.znear = znear; + Config.zfar = zfar; + camera.projection() = perspective_projection(Viewport.projection, znear, zfar, frustumtest_proj); + editor_ortho_projection(Viewport, zfar, camera.projection(), frustumtest_proj); + break; + } + case rendermode::shadows: + { + auto const zfar = (Zfar > 1.f ? Zfar : Zfar * Global.shadowtune.range * std::max(Zfar, Global.ZoomFactor * 0.5f)); + std::size_t bounding_viewports_count = 0; + std::vector frustumchunkshapepoints; + glm::dvec3 pass_camera{0.0}; + for (auto &conf : m_viewports) + { + if (!conf.shadow) + { + continue; + } + renderpass_config worldview; + setup_pass(conf, worldview, rendermode::color, Znear, zfar, true); + auto &points = worldview.pass_camera.frustum_points(); + frustumchunkshapepoints.insert(frustumchunkshapepoints.end(), points.begin(), points.end()); + pass_camera += worldview.pass_camera.position(); + bounding_viewports_count++; + } + pass_camera /= std::max(bounding_viewports_count, 1); + // ...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})}; + // ...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}); + // ...projection matrix: calculate boundaries of the frustum chunk in light space... + auto const lightviewmatrix = glm::translate(glm::mat4{glm::mat3{viewmatrix}}, -frustumchunkcentre); + for (auto &point : frustumchunkshapepoints) + { + point = lightviewmatrix * point; + } + 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)}; + frustumchunkmin = quantizationstep * glm::floor(frustumchunkmin * (1.f / quantizationstep)); + frustumchunkmax = quantizationstep * glm::ceil(frustumchunkmax * (1.f / quantizationstep)); + // ...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); + // ... and adjust the projection to sample complete shadow map texels + auto const shadowbuffersize{static_cast(m_context->scene.shadow_size)}; + auto shadowmaptexel = glm::vec2{camera.projection() * glm::mat4{viewmatrix} * glm::vec4{0.f, 0.f, 0.f, 1.f}}; + shadowmaptexel *= shadowbuffersize * 0.5f; + auto shadowmapadjustment = glm::round(shadowmaptexel) - shadowmaptexel; + shadowmapadjustment /= shadowbuffersize * 0.5f; + camera.projection() = glm::translate(glm::mat4{1.f}, glm::vec3{shadowmapadjustment, 0.f}) * camera.projection(); + break; + } + case rendermode::pickcontrols: + case rendermode::pickscenery: + { + camera.position() = Global.pCamera.Pos; + Global.pCamera.SetMatrix(viewmatrix); + viewport_proj_config proj = Viewport.projection; + float znear = 0.1f * Global.ZoomFactor; + float zfar = Config.draw_range * Global.fDistanceFactor; + if (EditorModeFlag && Viewport.main && Mode == rendermode::pickscenery) + { + zfar = editor_track_view_range(zfar, true); + } + camera.projection() = perspective_projection(proj, znear, zfar, frustumtest_proj); + editor_ortho_projection(Viewport, zfar, camera.projection(), frustumtest_proj); + break; + } + case rendermode::reflections: + { + // the faces of the map are drawn over a few frames. each is drawn from the place the camera was at + // when the update began, so they fit together along the edges when the camera moves in the meantime + camera.position() = m_environmentupdatelocation; + glm::dvec3 const cubefacetargetvectors[6] = {{1.0, 0.0, 0.0}, {-1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, -1.0, 0.0}, {0.0, 0.0, 1.0}, {0.0, 0.0, -1.0}}; + glm::dvec3 const cubefaceupvectors[6] = {{0.0, -1.0, 0.0}, {0.0, -1.0, 0.0}, {0.0, 0.0, 1.0}, {0.0, 0.0, -1.0}, {0.0, -1.0, 0.0}, {0.0, -1.0, 0.0}}; + auto const cubefaceindex = m_environmentcubetextureface; + viewmatrix *= glm::lookAt(camera.position(), camera.position() + cubefacetargetvectors[cubefaceindex], cubefaceupvectors[cubefaceindex]); + float znear = 0.1f * Global.ZoomFactor; + float zfar = Config.draw_range * Global.fDistanceFactor; + viewport_proj_config proj({{-0.5f, -0.5f, -0.5f}, {0.5f, -0.5f, -0.5f}, {-0.5f, 0.5f, -0.5f}, {0.0f, 0.0f, 0.0f}}); + camera.projection() = perspective_projection(proj, znear, zfar, frustumtest_proj); + break; + } + default: + break; + } + camera.modelview() = viewmatrix; + camera.update_frustum(frustumtest_proj); +} + +// the colour pass of a viewport (opengl46_renderer::Render_pass(), rendermode::color) +void vulkan14_renderer::render_viewport(std::size_t const Index) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &vp{m_viewports[Index]}; + auto &targets{scene.targets[Index]}; + auto const commands{ctx.commands}; + m_current_viewport = &vp; + + setup_pass(vp, m_renderpass, rendermode::color); + m_colorpass = m_renderpass; + + scene_ubs.time = Timer::GetTime(); + { + float percipitation_intensity = glm::saturate(Global.Overcast - 1.); + scene_ubs.rain_params.x = percipitation_intensity; + scene_ubs.rain_params.y = glm::mix(15., 1., percipitation_intensity); + if (TDynamicObject const *owner = Global.pCamera.m_owner; owner && !!owner->MoverParameters->CabActive) + { + for (int i = 0; i < 4; ++i) + { + if (i < owner->dWiperPos.size()) + { + int index = owner->MoverParameters->CabActive > 0 ? i : static_cast(owner->dWiperPos.size() - 1) - i; + scene_ubs.wiper_pos[i] = owner->dWiperPos[index]; + if (owner->dWiperPos[index] > 0. && owner->wiperDirection[index]) + { + scene_ubs.wiper_pos[i] += 1.; + } + if (owner->dWiperPos[index] < .025) + { + scene_ubs.wiper_timer_out[i] = scene_ubs.time; + } + if (owner->dWiperPos[index] > .975) + { + scene_ubs.wiper_timer_return[i] = scene_ubs.time; + } + } + else + { + scene_ubs.wiper_pos[i] = 0.; + scene_ubs.wiper_timer_out[i] = -1000.; + scene_ubs.wiper_timer_return[i] = -1000.; + } + } + } + else + { + scene_ubs.wiper_pos = glm::vec4{0.}; + scene_ubs.wiper_timer_out = glm::vec4{-1000.}; + scene_ubs.wiper_timer_return = glm::vec4{-1000.}; + } + } + // the stages after D: no contact shadows, no cascades fitted to the depth, no shadow maps of headlights + scene_ubs.contact_shadow_params = glm::vec4{0.f}; + scene_ubs.contact_shadow_lamps = glm::vec4{0.f}; + scene_ubs.shadow_fit = glm::vec4{0.f}; + + if (Global.gfx_shadowmap_enabled && vp.main && scene.shadow_map) + { + Timer::subsystem.gfx_shadows.start(); + m_renderpass.draw_stats = {}; + render_shadows(vp); + m_renderpass = m_colorpass; + Timer::subsystem.gfx_shadows.stop(); + } + if (Global.gfx_envmap_enabled && vp.main && scene.env_map) + { + m_renderpass.draw_stats = {}; + Timer::subsystem.gfx_reflections.start(); + if (render_reflections(vp)) + { + m_renderpass = m_colorpass; + } + Timer::subsystem.gfx_reflections.stop(); + } + + // 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})))}; + + setup_matrices(); + m_renderpass.draw_stats = {}; + model_ubs.future = glm::mat4(); + glm::mat4 future; + if (Global.pCamera.m_owner != nullptr) + { + auto const *vehicle = Global.pCamera.m_owner; + glm::mat4 mv = OpenGLMatrices.data(GL_MODELVIEW); + future = glm::translate(mv, -glm::vec3(vehicle->get_future_movement())) * glm::inverse(mv); + } + + // the light clusters of the viewport (the buffers of the grid are taken by each viewport in turn), and the lights of the pass + auto const clusters{Global.gfx_lights_clustered || Global.gfx_lights_clusterheatmap}; + ctx.passes.begin_pass("clear, clusters, lights"); + if (clusters) + { + update_clusters(glm::ivec2{vp.width, vp.height}); + } + auto const clustered{clusters && Global.gfx_lights_clustered && (scene.clusters.params.valid != 0)}; + update_lights(simulation::Lights, true, clustered); + + // the colour of the scene: drawn to the images with the samples, resolved to the single ones + auto const uses{colour_target_uses(targets)}; + ctx.passes.begin_pass("sky", uses); + begin_colour_rendering(Index, true); + // as in the opengl renderer, the shadow map and the environment map are those of the main view only + start_draws(targets.formats, static_cast(targets.extent.width), static_cast(targets.extent.height), vp.main ? vk14::scene_state::set_colour : vk14::scene_state::set_plain, false); + upload_light_header(); + if (clustered) + { + m_draw->cluster_offset = static_cast(scene.clusters.params_offset); + } + + // the sky + walk_blending(true); + scene_ubs.projection = OpenGLMatrices.data(GL_PROJECTION); + scene_ubs.inv_view = glm::inverse(glm::mat4{glm::mat3{m_colorpass.pass_camera.modelview()}}); + upload_scene_block(); + render_environment(&simulation::Environment); + + setup_shadow_matrices(); + upload_scene_block(); + + // opaque parts... + ctx.passes.begin_pass("colour, opaque"); + walk_blending(false); + // without rain/snow we can render the cab early to limit the overdraw + if (!FreeFlyModeFlag && Global.Overcast <= 1.0f && Global.render_cab && simulation::Train) + { + model_ubs.future = glm::mat4(); + auto const *vehicle{simulation::Train->Dynamic()}; + // the lamp lighting the shadows of the sun, unless it is a light of its own + if ((vehicle->InteriorLightLevel > 0.f) && (false == m_cablamplit)) + { + setup_shadow_color(glm::min(colors::white, m_shadowcolor + glm::vec4(vehicle->InteriorLight * vehicle->InteriorLightLevel, 1.f))); + } + Render_cab(vehicle, vehicle->InteriorLightLevel, false); + } + setup_shadow_color(m_shadowcolor); + model_ubs.future = future; + Render(simulation::Region); + + // ...translucent parts + ctx.passes.begin_pass("colour, translucent and cab"); + walk_blending(true); + Render_Alpha(simulation::Region); + render_editor_overlay(); + // particles + render_particles(); + // precipitation; done at end, only before cab render + render_precipitation(); + // cab render + if ((false == FreeFlyModeFlag) && Global.render_cab && simulation::Train) + { + model_ubs.future = glm::mat4(); + auto *vehicle{simulation::Train->Dynamic()}; + if ((vehicle->InteriorLightLevel > 0.f) && (false == m_cablamplit)) + { + setup_shadow_color(glm::min(colors::white, m_shadowcolor + glm::vec4(vehicle->InteriorLight * vehicle->InteriorLightLevel, 1.f))); + } + if (Global.Overcast > 1.0f) + { + // with active precipitation draw the opaque cab parts here to mask rain/snow placed 'inside' the cab + walk_blending(false); + Render_cab(vehicle, vehicle->InteriorLightLevel, false); + Render_interior(false); + walk_blending(true); + Render_interior(true); + } + Render_cab(vehicle, vehicle->InteriorLightLevel, true); + setup_shadow_color(m_shadowcolor); + if (Global.Overcast > 1.0f) + { + // with the cab in place we can (finally) safely draw translucent part of the occupied vehicle + Render_Alpha(vehicle); + } + } + vkCmdEndRendering(commands); + end_draws(); + // the images resolved and drawn are read by the post-processing + m_colorpass.draw_stats = m_renderpass.draw_stats; + + render_post(Index, motionblur); +} + +// 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) +{ + auto &ctx{*m_context}; + auto &targets{ctx.scene.targets[Index]}; + auto const msaa{targets.samples != VK_SAMPLE_COUNT_1_BIT}; + VkClearValue colourclear{}; + colourclear.color = {{0.f, 0.f, 0.f, 0.f}}; + VkClearValue depthclear{}; + depthclear.depthStencil = {0.f, 0}; + std::array colours{}; + colours[0] = attachment_info((msaa ? targets.msaa_colour : targets.colour)->view(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, Clear, colourclear); + if (msaa) + { + colours[0].resolveMode = VK_RESOLVE_MODE_AVERAGE_BIT; + colours[0].resolveImageView = targets.colour->view(); + colours[0].resolveImageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + } + if (targets.motion) + { + colours[1] = attachment_info((msaa ? targets.msaa_motion : targets.motion_image)->view(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, Clear, colourclear); + if (msaa) + { + colours[1].resolveMode = VK_RESOLVE_MODE_AVERAGE_BIT; + colours[1].resolveImageView = targets.motion_image->view(); + colours[1].resolveImageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + } + } + auto depth{attachment_info((msaa ? targets.msaa_depth : targets.depth)->view(), VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, Clear, depthclear)}; + if (msaa) + { + // the depth of the first sample, for the motion blur and the cursor of the editor + 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.colorAttachmentCount = targets.formats.colours; + rendering.pColorAttachments = colours.data(); + rendering.pDepthAttachment = &depth; + vkCmdBeginRendering(ctx.commands, &rendering); +} + +// the depth of the ground under the cursor of the editor: the rendering is ended (which resolves the depth), the depth under the +// cursor copied, and the rendering begun again where it was +void vulkan14_renderer::walk_editor_cursor() +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &draw{*m_draw}; + if ((m_current_viewport == nullptr) || (false == m_current_viewport->main) || (false == draw.rendering) || scene.cursor.pending || scene.targets.empty()) + { + return; + } + auto &targets{scene.targets.front()}; + auto const commands{ctx.commands}; + glm::vec2 mousepos = Global.cursor_pos; + // cursor coordinates are flipped compared to opengl, whose rows the images keep + mousepos.y = Global.window_size.y - mousepos.y; + 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); + auto readback{std::make_shared()}; + if (readback->create(ctx.memory, sizeof(float), VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::readback, "cursor depth")) + { + 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)}); + VkBufferImageCopy2 region{}; + region.sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2; + region.imageSubresource = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}; + region.imageOffset = {bufferpos.x, bufferpos.y, 0}; + region.imageExtent = {1, 1, 1}; + VkCopyImageToBufferInfo2 copy{}; + copy.sType = VK_STRUCTURE_TYPE_COPY_IMAGE_TO_BUFFER_INFO_2; + copy.srcImage = targets.depth->handle(); + copy.srcImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + copy.dstBuffer = readback->handle(); + copy.regionCount = 1; + copy.pRegions = ®ion; + vkCmdCopyImageToBuffer2(commands, ©); + host_read_barrier(commands); + scene.cursor.data = readback; + scene.cursor.value = ctx.timelinevalue + 1; + scene.cursor.pending = true; + scene.cursor.position = bufferpos; + vk14::barriers(commands, {vk14::use_of(*targets.depth, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, resolve_depth_stage, resolve_depth_access)}); + } + // what the rendering stored before is loaded again + vk14::barriers(commands, colour_target_uses(targets)); + begin_colour_rendering(0, false); + // the dynamic state starts anew with the rendering + auto const set{draw.set}; + auto const scene_offset{draw.scene_offset}; + auto const light_offset{draw.light_offset}; + auto const cluster_offset{draw.cluster_offset}; + auto const blending{draw.blending}; + auto *program{draw.program}; + auto const textures{draw.textures}; + start_draws(targets.formats, static_cast(targets.extent.width), static_cast(targets.extent.height), set, false); + draw.scene_offset = scene_offset; + draw.light_offset = light_offset; + draw.cluster_offset = cluster_offset; + draw.blending = blending; + draw.program = program; + draw.textures = textures; +} + +// the cascades of the sun shadows +void vulkan14_renderer::render_shadows(viewport_config &Viewport) +{ + auto &ctx{*m_context}; + 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; + } + } + 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)}); + setup_pass(Viewport, m_renderpass, rendermode::shadows, csmstageboundaries[idx], csmstageboundaries[idx + 1]); + m_shadowpass[idx] = m_renderpass; + setup_matrices(); + scene_ubs.projection = OpenGLMatrices.data(GL_PROJECTION); + auto const csmstagezfar{(csmstageboundaries[idx + 1]) + (m_shadowpass.size() - idx)}; + scene_ubs.cascade_end[idx] = csmstagezfar * csmstagezfar; + + VkClearValue clear{}; + clear.depthStencil = {0.f, 0}; + auto depth{attachment_info(scene.shadow_layers[idx], VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, true, clear)}; + VkRenderingInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + rendering.renderArea = {{0, 0}, {scene.shadow_size, scene.shadow_size}}; + rendering.layerCount = 1; + rendering.pDepthAttachment = &depth; + vkCmdBeginRendering(commands, &rendering); + start_draws(scene.shadow_formats, static_cast(scene.shadow_size), static_cast(scene.shadow_size), vk14::scene_state::set_plain, true); + walk_blending(false); + upload_scene_block(); + if (m_shadowcolor != colors::white) + { + Render(simulation::Region); + // render cab only in the closest csm stage + if ((idx == 0) && !FreeFlyModeFlag && Global.render_cab && simulation::Train) + { + Render_cab(simulation::Train->Dynamic(), 0.0f, false); + Render_cab(simulation::Train->Dynamic(), 0.0f, true); + } + } + vkCmdEndRendering(commands); + end_draws(); + ctx.passes.end_pass(); + } + vk14::barriers(commands, {vk14::use_of(*scene.shadow_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); +} + +// the environment map, a face of it per frame (opengl46_renderer::Render_reflections()) +bool vulkan14_renderer::render_reflections(viewport_config &Viewport) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + if (Global.reflectiontune.update_interval == 0) + { + return false; + } + if (m_environmentupdateface >= 6) + { + // no update in progress, check whether it's time for another one + auto const timestamp{Timer::GetRenderTime()}; + if ((timestamp - m_environmentupdatetime < Global.reflectiontune.update_interval) && (glm::length2(m_renderpass.pass_camera.position() - m_environmentupdatelocation) < sq(1000.0))) + { + return false; + } + m_environmentupdatetime = timestamp; + m_environmentupdatelocation = m_renderpass.pass_camera.position(); + m_environmentupdateface = 0; + } + // the first update is done in one go, as there's nothing to show in the meantime + auto const lastface{m_environmentready ? m_environmentupdateface : 5}; + ctx.passes.begin_pass("reflections", {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)}); + for (; m_environmentupdateface <= lastface; ++m_environmentupdateface) + { + // the depth of the faces is the same image: the clear of a face waits for the draws of the one before + vk14::barriers(ctx.commands, {vk14::use_of(*scene.env_depth, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)}); + m_environmentcubetextureface = m_environmentupdateface; + render_reflection_face(Viewport); + } + if (m_environmentupdateface >= 6) + { + // the mipmaps, each level from the one before it + auto const commands{ctx.commands}; + auto &image{*scene.env_map}; + auto const mips{image.desc().mips}; + auto const size{static_cast(image.desc().extent.width)}; + 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 = image.handle(); + entry.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, Level, Count, 0, 6}; + VkDependencyInfo dependency{}; + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency.imageMemoryBarrierCount = 1; + dependency.pImageMemoryBarriers = &entry; + vkCmdPipelineBarrier2(commands, &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); + if (mips > 1) + { + barrier(1, mips - 1, VK_IMAGE_LAYOUT_UNDEFINED, 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); + } + for (std::uint32_t level = 1; level < mips; ++level) + { + VkImageBlit2 blit{}; + blit.sType = VK_STRUCTURE_TYPE_IMAGE_BLIT_2; + blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level - 1, 0, 6}; + blit.srcOffsets[1] = {std::max(size >> (level - 1), 1), std::max(size >> (level - 1), 1), 1}; + blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, 6}; + blit.dstOffsets[1] = {std::max(size >> level, 1), std::max(size >> level, 1), 1}; + 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; + vkCmdBlitImage2(commands, &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; + } + else + { + vk14::barriers(ctx.commands, {vk14::use_of(*scene.env_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)}); + } + return true; +} + +void vulkan14_renderer::render_reflection_face(viewport_config &Viewport) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto const commands{ctx.commands}; + auto const size{scene.env_map->desc().extent.width}; + setup_pass(Viewport, m_renderpass, rendermode::reflections); + + VkClearValue colourclear{}; + colourclear.color = {{0.f, 0.f, 0.f, 1.f}}; + VkClearValue depthclear{}; + depthclear.depthStencil = {0.f, 0}; + auto colour{attachment_info(scene.env_faces[m_environmentcubetextureface], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, true, colourclear)}; + auto depth{attachment_info(scene.env_depth->view(), VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, true, depthclear)}; + VkRenderingInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + rendering.renderArea = {{0, 0}, {size, size}}; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &colour; + rendering.pDepthAttachment = &depth; + vkCmdBeginRendering(commands, &rendering); + start_draws(scene.reflection_formats, static_cast(size), static_cast(size), vk14::scene_state::set_reflections, false); + + setup_matrices(); + // the light buffer holds what the last color pass left in it; the lights go in anew, without the headlights + update_lights(simulation::Lights, false); + upload_light_header(); + // environment... + walk_blending(true); + scene_ubs.projection = OpenGLMatrices.data(GL_PROJECTION); + scene_ubs.inv_view = glm::inverse(glm::mat4{glm::mat3{m_renderpass.pass_camera.modelview()}}); + upload_scene_block(); + render_environment(&simulation::Environment); + // opaque parts... + walk_blending(false); + setup_shadow_matrices(); + upload_scene_block(); + Render(simulation::Region); + + vkCmdEndRendering(commands); + 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) +void vulkan14_renderer::render_environment(world_environment *Environment) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &draw{*m_draw}; + m_shadowcolor = colors::white; // prevent shadow from affecting sky + setup_shadow_color(m_shadowcolor); + if (Global.bWireFrame) + { + return; + } + walk_material(null_handle); + ::glPushMatrix(); + auto const fogdensity{model_ubs.fog_density}; + model_ubs.fog_density = 0.f; + + // skydome, drawn with 500m radius to blend in if the fog range is low + auto &skydome{simulation::Environment.skydome()}; + skydome.is_dirty() = false; + if (false == skydome.indices().empty()) + { + ::glPushMatrix(); + ::glScalef(500.0f, 500.0f, 500.0f); + model_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW)); + 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)) + { + std::memcpy(vertexslice.data, vertices.data(), vertexslice.size); + std::memcpy(colourslice.data, colours.data(), colourslice.size); + std::memcpy(indexslice.data, indices.data(), indexslice.size); + apply_draw_state(); + draw.block = push_draw_block(); + if (draw.block != ~0u) + { + if (draw.set_dirty) + { + bind_pass_set(); + } + if (draw.textures_dirty) + { + push_textures(); + } + vk14::draw_constants constants; + constants.draw = draw.block; + 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.vertices = VK_NULL_HANDLE; + draw.userdata = VK_NULL_HANDLE; + draw.indices = VK_NULL_HANDLE; + } + } + ::glPopMatrix(); + } + + auto const &modelview = OpenGLMatrices.data(GL_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)}; + 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); + 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) { + auto const refraction = [](float const Height) { + auto const height{std::max(Height, -2.f)}; + return 1.f / std::tan(glm::radians(height + 7.31f / (height + 4.4f))); + }; + auto const radius{glm::degrees(Radius)}; + return std::clamp(1.f - (refraction(Elevation - radius) - refraction(Elevation + radius)) / (2.f * radius * 60.f), 0.5f, 1.f); + }; + auto const mainview{(m_renderpass.draw_mode == rendermode::color) && (m_current_viewport != nullptr) && m_current_viewport->main}; + auto const draw_body = [&](std::array const &Body) { + if (false == bind_scene_pipeline(*scene.celestial, *draw.targets, vk14::blend_mode::premultiplied, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP)) + { + return; + } + apply_draw_state(); + 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 (std::size_t i = 0; i < Body.size(); ++i) + { + constants.data[i] = Body[i]; + } + push_constants(&constants); + vkCmdDraw(draw.commands, 4, 1, 0, 0); + }; + std::array body{}; + // sun + { + float const sunradius{glm::radians(0.2666f)}; + float const quadmargin{1.25f}; + glm::vec3 const limbdarkening{0.50f, 0.60f, 0.75f}; + float const limbpeak{1.f / (1.f - 0.75f / 3.f)}; + float const radiancemax{60000.f}; + auto const sunvector = sun.getDirection(); + glm::vec3 radiance{suncolor * overcastfactor}; + 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 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}; + if (peak > radiancemax) + { + radiance *= radiancemax / peak; + } + } + body[0] = glm::vec4{zenith, flattening(sun.getAngle(), sunradius), 1.f / quadmargin}; + body[1] = glm::vec4{0.f}; + body[2] = glm::vec4{limbdarkening, 0.f}; + model_ubs.param[0] = glm::vec4{radiance, 1.f}; + model_ubs.param[1] = glm::vec4(glm::vec3(modelview * glm::vec4(sunvector, 1.0f)), std::tan(sunradius) * quadmargin); + model_ubs.param[2] = glm::vec4(0.f, 0.f, 0.f, 1.f); + draw_body(body); + } + // moon + { + glm::vec2 const discposition{0.5f, 0.5f}; + float const discradius{0.1635f}; + float const quadmargin{1.1f}; + float const earthshine{0.005f}; + glm::vec3 const extinction{0.10f, 0.17f, 0.30f}; + walk_texture(0, m_moontexture); + glm::vec3 mooncolor(1.0f, 0.9490196f, 0.9058824f); + float visibility = std::max(0.f, 1.0 - 0.5 * Global.fLuminance - 0.65 * std::max(0.f, glm::dot(sun.getDirection(), moon.getDirection()))) * overcastfactor; + if (mainview) + { + visibility = 1.4f * overcastfactor; + } + auto const moonvector = moon.getDirection(); + auto const moonradius{moon.getAngularRadius()}; + auto const moonelevation{moon.getAngle()}; + auto const moonview{glm::vec3(modelview * glm::vec4(moonvector, 1.0f))}; + if (moon.getPhase() == 50) + { + model_ubs.param[0] = glm::vec4(mooncolor, visibility); + model_ubs.param[1] = glm::vec4(moonview, std::tan(moonradius) * (0.1665f / discradius)); + model_ubs.param[2] = glm::vec4(0.66f, 1.0f - 0.667f, 0.333f, 0.f); + } + else + { + auto const zenithangle{90.f - std::max(moonelevation, -1.f)}; + auto const airmass{1.f / (std::cos(glm::radians(zenithangle)) + 0.50572f * std::pow(std::max(96.07995f - zenithangle, 0.1f), -1.6364f))}; + auto const transmittance{glm::exp(-extinction * (airmass - 1.f))}; + auto const night{1.f - glm::smoothstep(-15.f, -9.f, sun.getAngle())}; + auto const earthphase{0.5f * (1.f - std::cos(glm::radians(moon.getPhaseAngle())))}; + body[0] = glm::vec4{zenith, flattening(moonelevation, moonradius), 1.f / quadmargin}; + body[1] = glm::vec4{glm::normalize(glm::mat3{modelview} * sun.getDirection()), earthshine * earthphase}; + body[2] = glm::vec4{0.f}; + model_ubs.param[0] = glm::vec4(mooncolor * transmittance * (visibility * Global.gfx_moon_disc), night * overcastfactor); + model_ubs.param[1] = glm::vec4(moonview, std::tan(moonradius) * quadmargin); + model_ubs.param[2] = glm::vec4(discposition, discradius, 2.f); + } + draw_body(body); + } + ::glPopMatrix(); + model_ubs.fog_density = fogdensity; + + // 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); + if ((sun.getAngle() < -18.f) && (moon.getAngle() > 0.f)) + { + // turn on moon shadows after astronomical twilight, if the moon is actually up + 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)); + setup_shadow_color(m_shadowcolor); +} + +void vulkan14_renderer::push_constants(void const *Constants) +{ + auto &draw{*m_draw}; + VkPushConstantsInfo info{}; + info.sType = VK_STRUCTURE_TYPE_PUSH_CONSTANTS_INFO; + info.layout = m_context->scene.scene_layout; + info.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + info.offset = 0; + info.size = sizeof(vk14::draw_constants); + info.pValues = Constants; + vkCmdPushConstants2(draw.commands, &info); + draw.pushed_draw = ~0u; +} + +// the smoke of the cpu (opengl46_particles) +void vulkan14_renderer::render_particles() +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + 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); + if (false == Global.Smoke) + { + return; + } + auto const &camera{m_renderpass.pass_camera}; + std::multimap sources; + for (auto const &source : simulation::Particles.sequence()) + { + if (false == camera.visible(source.area())) + { + continue; + } + sources.emplace(static_cast(glm::length(camera.position() - source.area().center) - source.area().radius), source); + } + if (sources.empty()) + { + return; + } + std::vector vertices; + auto const camerarotation{glm::mat3(camera.modelview())}; + particle_vertex vertex; + for (auto const &[sourcekey, sourcedata] : sources) + { + auto const particlecolor{glm::clamp(sourcedata.color() * (glm::vec3{Global.DayLight.ambient} + 0.35f * glm::vec3{Global.DayLight.diffuse}) * simulation::Environment.light_intensity(), glm::vec3{0.f}, glm::vec3{1.f})}; + for (auto const &particle : sourcedata.sequence()) + { + vertex.color[0] = particlecolor.r; + vertex.color[1] = particlecolor.g; + vertex.color[2] = particlecolor.b; + vertex.color.a = std::clamp(particle.opacity, 0.0f, 1.0f) * sourcedata.fade_in(particle.age); + auto const offset{glm::vec3{particle.position - camera.position()}}; + auto const rotation{glm::angleAxis(particle.rotation, glm::vec3{0.f, 0.f, 1.f})}; + for (auto const &[billboardoffset, billboardtexture] : billboard_vertices) + { + vertex.position = offset + (rotation * billboardoffset * particle.size) * camerarotation; + vertex.texture = billboardtexture; + vertices.emplace_back(vertex); + } + } + } + m_renderpass.draw_stats.particles = static_cast(vertices.size() / 6); + auto const slice{ctx.ring.allocate(vertices.size() * sizeof(particle_vertex), 16)}; + if ((false == bool(slice)) || (false == bind_scene_pipeline(*scene.smoke, *draw.targets, vk14::blend_mode::scene_alpha, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST))) + { + return; + } + std::memcpy(slice.data, vertices.data(), slice.size); + apply_draw_state(); + 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; + 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.vertices = VK_NULL_HANDLE; + draw.userdata = VK_NULL_HANDLE; +} + +// the rain and the snow of the cpu (opengl46_renderer::Render_precipitation(), opengl46_precipitation) +void vulkan14_renderer::render_precipitation() +{ + if (Global.Overcast <= 1.f) + { + return; + } + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &draw{*m_draw}; + if (m_precipitationovercast != Global.Overcast) + { + m_precipitationovercast = Global.Overcast; + std::string const densitysuffix{(m_precipitationovercast < 1.35 ? "_light" : "_medium")}; + if (Global.Weather == "rain:") + { + m_precipitationtexture = Fetch_Texture("fx/rain" + densitysuffix); + } + else if (Global.Weather == "snow:") + { + m_precipitationtexture = Fetch_Texture("fx/snow" + densitysuffix); + } + } + ::glPushMatrix(); + // tilt the precipitation cone against the velocity vector for crude motion blur, include current wind vector while at it + auto const velocity{simulation::Environment.precipitation().m_cameramove * -1.0 + glm::dvec3{simulation::Environment.wind()} * 0.5}; + if (glm::length2(velocity) > 0.0) + { + auto const forward{glm::normalize(velocity)}; + auto left{glm::cross(forward, {0.0, 1.0, 0.0})}; + auto const rotationangle{std::min(45.0, ((FreeFlyModeFlag || simulation::Train == nullptr) ? 5 * glm::length(velocity) : simulation::Train->Dynamic()->GetVelocity() * 0.2))}; + ::glRotated(rotationangle, left.x, 0.0, left.z); + } + if ((false == FreeFlyModeFlag) && (simulation::Train != nullptr)) + { + // counter potential vehicle roll + auto const roll{0.5 * glm::degrees(simulation::Train->Dynamic()->Roll())}; + if (roll != 0.0) + { + auto const forward{simulation::Train->Dynamic()->VectorFront()}; + ::glRotated(roll, forward.x, 0.0, forward.z); + } + } + if (!Global.iPause) + { + // oddly enough random streaks produce more natural looking rain than ones the eye can follow + m_precipitationrotation = (Global.Weather == "rain:" ? LocalRandom() * 360 : 0.0); + } + ::glRotated(m_precipitationrotation, 0.0, 1.0, 0.0); + model_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW)); + model_ubs.param[0] = glm::mix(0.5f * (Global.DayLight.diffuse + Global.DayLight.ambient), colors::white, 0.5f * std::clamp((float)Global.fLuminance, 0.f, 1.f)); + model_ubs.param[1].x = simulation::Environment.precipitation().get_textureoffset(); + walk_texture(0, m_precipitationtexture); + + auto const &mesh{precipitation()}; + auto const vertexslice{ctx.ring.allocate(mesh.vertices.size() * sizeof(glm::vec3), 16)}; + auto const uvslice{ctx.ring.allocate(mesh.uvs.size() * sizeof(glm::vec2), 16)}; + auto const indexslice{ctx.ring.allocate(mesh.indices.size() * sizeof(std::uint16_t), 16)}; + if (vertexslice && uvslice && indexslice && bind_scene_pipeline(*scene.precipitation, *draw.targets, draw.blending ? vk14::blend_mode::scene_alpha : vk14::blend_mode::none, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)) + { + std::memcpy(vertexslice.data, mesh.vertices.data(), vertexslice.size); + std::memcpy(uvslice.data, mesh.uvs.data(), uvslice.size); + std::memcpy(indexslice.data, mesh.indices.data(), indexslice.size); + apply_draw_state(); + draw.block = push_draw_block(); + if (draw.block != ~0u) + { + if (draw.set_dirty) + { + bind_pass_set(); + } + if (draw.textures_dirty) + { + push_textures(); + } + vk14::draw_constants constants; + constants.draw = draw.block; + 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.vertices = VK_NULL_HANDLE; + draw.userdata = VK_NULL_HANDLE; + draw.indices = VK_NULL_HANDLE; + } + } + ::glPopMatrix(); +} + +// the lines the editor draws over the scene (opengl46_renderer::Render_editor_overlay()) +void vulkan14_renderer::render_editor_overlay() +{ + if (false == EditorModeFlag || EditorOverlay.batches.empty()) + { + return; + } + if (m_editor_overlay_revision != EditorOverlay.revision) + { + m_editor_overlay_revision = EditorOverlay.revision; + if (m_editor_overlay_bank == null_handle) + { + m_editor_overlay_bank = Create_Bank(); + } + auto const &batches{EditorOverlay.batches}; + for (std::size_t i = 0; i < batches.size(); ++i) + { + gfx::vertex_array vertices; + vertices.reserve(batches[i].points.size()); + for (auto const &point : batches[i].points) + { + vertices.emplace_back(point, glm::vec3(0.f, 1.f, 0.f), glm::vec2()); + } + gfx::userdata_array userdata; + if (i >= m_editor_overlay_geometry.size()) + { + m_editor_overlay_geometry.emplace_back(); + } + auto &geometry{m_editor_overlay_geometry[i]}; + if (geometry != null_handle) + { + m_geometry.replace(vertices, userdata, geometry); + } + else if (false == vertices.empty()) + { + geometry = m_geometry.create_chunk(vertices, userdata, m_editor_overlay_bank, batches[i].type); + } + } + } + auto &draw{*m_draw}; + ::glPushMatrix(); + auto const origin{EditorOverlay.origin - m_renderpass.pass_camera.position()}; + ::glTranslated(origin.x, origin.y, origin.z); + draw.cullface = false; + draw.depthmask_off = true; + walk_program(walk_shader::line); + auto const &batches{EditorOverlay.batches}; + for (std::size_t i = 0; i < batches.size() && i < m_editor_overlay_geometry.size(); ++i) + { + if (batches[i].points.empty() || m_editor_overlay_geometry[i] == null_handle) + { + continue; + } + model_ubs.param[0] = batches[i].color; + walk_draw(m_editor_overlay_geometry[i]); + ++m_renderpass.draw_stats.drawcalls; + } + draw.depthmask_off = false; + draw.cullface = true; + ::glPopMatrix(); +} + +// the post-processing of a viewport: from the colour of the scene to its output, which the window shows +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); + }; + + 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"); + hdr = targets.blurred.get(); + } + // the exposure and the bloom come with the stage P1: the fixed exposure + vk14::draw_constants tonemap; + tonemap.data[0] = glm::vec4{0.f, 0.f, std::exp2(Global.gfx_postfx_autoexposure_compensation), 0.f}; + tonemap.data[1] = glm::vec4{targets.aberration ? 0.f : 1.f, 0.f, 0.f, 0.f}; + 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); + } + else + { + post("postfx_tonemapping.frag", *targets.output, {hdr, hdr}, tonemap, "tonemapping, output"); + } + vk14::barriers(commands, {vk14::use_of(*targets.output, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, sampled_access)}); +} + +// the output of a viewport drawn to its window turned upside down (decision K8), inside the rendering of the window +void vulkan14_renderer::present_viewport(std::size_t const Index) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &targets{scene.targets[Index]}; + auto const &window{*ctx.windows[m_viewports[Index].window]}; + auto const commands{ctx.commands}; + // the output encoded already goes as it is; an srgb output to a window which isn't (no srgb format of the surface) is encoded + auto const pipeline{post_pipeline("present.frag", static_cast(window.format()), (targets.srgb && (false == window.srgb())) ? 1 : 0)}; + if (pipeline == nullptr) + { + 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()); + vk14::draw_constants 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; + vkCmdPushConstants2(commands, &info); + vkCmdDraw(commands, 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 +// colour encoded already (the output of the viewport is srgb, or encoded by the tonemapping with gfx.shadergamma) +void *vulkan14_renderer::post_pipeline(char const *Fragment, int const Format, int const Encode) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto const lookup{scene.post_fragments.find(Fragment)}; + if (lookup == scene.post_fragments.end()) + { + return nullptr; + } + vk14::pipeline_desc desc; + desc.stages = {scene.quad, lookup->second}; + desc.layout = scene.post_layout; + desc.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + desc.colour_formats = {static_cast(Format)}; + auto const gamma{Encode >= 0 ? (Encode > 0) : Global.gfx_shadergamma}; + desc.specialization = {gamma ? 1u : 0u, Global.gfx_shadowmap_enabled ? 1u : 0u, Global.gfx_envmap_enabled ? 1u : 0u, Global.gfx_postfx_motionblur_enabled ? 1u : 0u, 1u, Global.gfx_extraeffects ? 1u : 0u}; + desc.name = Fragment; + return reinterpret_cast(ctx.pipelines.get(desc)); +} + +// the lights of the pass (opengl46_renderer::Update_Lights()) +void vulkan14_renderer::update_lights(light_array &Lights, bool const Headlights, bool const Clustered) +{ + // arrange the light array from closest to farthest from current position of the camera + auto const camera = m_colorpass.pass_camera.position(); + if (Headlights) + { + std::sort(std::begin(Lights.data), std::end(Lights.data), [&camera](light_array::light_record const &Left, light_array::light_record const &Right) { + if (Left.intensity == 0.f) + { + return false; + } + if (Right.intensity == 0.f) + { + return true; + } + return (glm::length2(camera - Left.position) / Left.intensity) < (glm::length2(camera - Right.position) / Right.intensity); + }); + } + 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}; + + sun_light renderlight; + renderlight.is_directional = false; + std::size_t light_i = 1; + m_lights_dropped = 0; + + // the lights other than the headlights are only taken from the lists of the clusters. the lamp of the occupied cab goes first + auto const locallights{Headlights && Clustered}; + m_cablamplit = false; + if (locallights) + { + if (auto const *lamp{m_locallights.cab_lamp()}) + { + auto &light{light_data.lights[light_i++]}; + write_local_light(light, *lamp, camera, mv); + m_cablamplit = true; + } + } + auto const headlightsstart{light_i}; + for (auto const &scenelight : Lights.data) + { + if (!Headlights || !Global.gfx_lights_headlights || scenelight.intensity == 0.f) + { + break; + } + auto const lightoffset = glm::vec3{scenelight.position - camera}; + if (glm::length2(lightoffset) > sq(1000.f)) + { + continue; + } + if (light_i >= gl::MAX_LIGHTS) + { + ++m_lights_dropped; + continue; + } + renderlight.position = lightoffset; + renderlight.direction = scenelight.direction; + auto luminance = static_cast(Global.fLuminance); + auto const environment = scenelight.owner->fShade; + if (environment > 0.f) + { + luminance *= environment; + } + renderlight.diffuse = glm::vec4{glm::max(glm::vec3{colors::none}, scenelight.color - glm::vec3{luminance}), renderlight.diffuse[3]}; + renderlight.ambient = glm::vec4{glm::max(glm::vec3{colors::none}, scenelight.color * glm::vec3{scenelight.intensity} - glm::vec3{luminance}), renderlight.ambient[3]}; + renderlight.factor = (scenelight.count * 0.5) * (scenelight.owner->DimHeadlights ? 0.5 : 1.0); + + gl::light_element *light = &light_data.lights[light_i]; + light->pos = mv * glm::vec4(renderlight.position, 1.0f); + light->dir = mv * glm::vec4(renderlight.direction, 0.0f); + light->type = gl::light_element::HEADLIGHTS; + light->in_cutoff = in_cutoff; + light->out_cutoff = out_cutoff; + light->color = renderlight.diffuse * renderlight.factor; + light->linear = falloff_linear / 10.0f; + light->quadratic = falloff_quadratic / 100.0f; + light->ambient = ambient; + light->intensity = intensity; + auto const worldcam{glm::mat3{m_renderpass.pass_camera.modelview()}}; + { + glm::dvec3 projector; + glm::mat3 lightcam; + glm::mat4 depthproj; + float nearplane, fieldofview; + 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 + light->headlight_weights = {scenelight.state, 0.f}; + ++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) + { + uint32_t placed{0}; + uint32_t dropped{0}; + for (auto const &local : m_locallights.lights()) + { + if (false == m_renderpass.pass_camera.visible(scene::bounding_area{local.position, local.range})) + { + continue; + } + if (light_i >= gl::MAX_LIGHTS) + { + ++dropped; + continue; + } + auto &light{light_data.lights[light_i++]}; + write_local_light(light, local, camera, mv); + ++placed; + } + m_locallights.placed(placed, dropped); + } + 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; + // fill sunlight data + light_data.ambient = m_sunlight.ambient * m_sunlight.factor; + light_data.sky_ambient_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 + 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; + } + else + { + model_ubs.fog_density = 0.0f; + } + m_lightcount = static_cast(light_i); + m_lightheaders.clear(); + if (m_draw && m_draw->rendering) + { + upload_light_header(); + } + + // the lights of the clusters: the culling into the lists, for the colour pass which follows + if (Clustered) + { + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &clusters{scene.clusters}; + auto const commands{ctx.commands}; + auto const lightsize{offsetof(gl::light_data, lights) + sizeof(gl::light_element) * m_lightcount}; + auto const lights{ctx.ring.allocate(sizeof(gl::light_data), ctx.device.caps().limits.minStorageBufferOffsetAlignment)}; + if (false == bool(lights)) + { + return; + } + 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); + 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; + barrier.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT | VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT; + barrier.dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + barrier.dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT; + VkDependencyInfo dependency{}; + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency.memoryBarrierCount = 1; + dependency.pMemoryBarriers = &barrier; + vkCmdPipelineBarrier2(commands, &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); + 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); + 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); + clusters.lights_total = light_data.lights_total; + } +} + +void vulkan14_renderer::write_local_light(gl::light_element &Light, gfx::local_lights::light const &Source, glm::dvec3 const &Camera, glm::mat4 const &View) +{ + // a cone wider than a half of the space is no limit for the culling, but the shading still fades the light out of it + Light.type = gl::light_element::SPOT; + Light.pos = View * glm::vec4(glm::vec3{Source.position - Camera}, 1.0f); + Light.dir = glm::normalize(glm::vec3{View * glm::vec4(Source.direction, 0.0f)}); + Light.in_cutoff = Source.cos_inner; + Light.out_cutoff = Source.cos_outer; + if (Source.cos_outer <= -1.f) + { + Light.type = gl::light_element::POINT; + } + Light.color = Source.color; + Light.intensity = 1.0f; + Light.ambient = 0.0f; + // the shading takes the light to end at 1 / sqrt(quadratic) + Light.linear = 0.0f; + Light.quadratic = 1.0f / sq(std::max(Source.range, 0.01f)); + Light.headlight_projection = glm::mat4{1.0f}; + // no shadow map + Light.headlight_weights = glm::vec4{-1.0f, 0.0f, 0.0f, 0.0f}; +} + +void vulkan14_renderer::headlight_projector(light_array::light_record const &Light, glm::dvec3 &Position, glm::mat3 &Rotation, glm::mat4 &Projection, float &Near, float &Fieldofview) +{ + auto const &ownerdimensions{Light.owner->MoverParameters->Dim}; + auto const up{static_cast(Light.owner->VectorUp())}; + auto const size{static_cast(std::max(ownerdimensions.W, ownerdimensions.H) * 1.0)}; + auto const cone{5.f}; + auto const offset{75.f * (5.f / cone)}; + Position = Light.owner->GetPosition() - glm::dvec3(Light.direction * offset) + up * (size * 0.5); + Near = static_cast(ownerdimensions.L * 0.5 + offset - 0.25); + Fieldofview = glm::radians(cone); + Projection = perspective_projection_raw(Fieldofview, 1.0, Near, 1000.0f); + glm::dmat4 viewmatrix{1.0}; + viewmatrix *= glm::lookAt(Position, Position - up * 1.0 + glm::dvec3{Light.direction * 1000.f}, glm::dvec3{0.f, 1.f, 0.f}); + Rotation = glm::mat3{glm::mat4{viewmatrix}}; +} + +// the light cluster grid of a viewport for the projection of its colour pass (gl::cluster_grid::update() and build()); the +// viewports take the buffers of the grid in turn, its boxes are made anew when the projection changes +void vulkan14_renderer::update_clusters(glm::ivec2 const &Targetsize) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &clusters{scene.clusters}; + auto const commands{ctx.commands}; + auto const projection{OpenGLMatrices.data(GL_PROJECTION)}; + auto const znear{m_renderpass.znear}; + auto const zfar{m_renderpass.zfar}; + auto params{clusters.params}; + params.viewport_width = static_cast(Targetsize.x); + params.viewport_height = static_cast(Targetsize.y); + // clip space w is a function of the position in a perspective projection, and a constant in an orthographic one + bool const perspective{glm::length(glm::vec3{projection[0][3], projection[1][3], projection[2][3]}) > 1e-4f}; + bool const usable{perspective && znear > 0.f && zfar > znear}; + params.valid = usable ? 1 : 0; + if (usable && (projection != clusters.params.projection || znear != clusters.params.znear || zfar != clusters.params.zfar)) + { + params.projection = projection; + params.znear = znear; + params.zfar = zfar; + params.slice_scale = static_cast(gl::CLUSTER_GRID_Z) / std::log(zfar / znear); + params.slice_bias = -std::log(znear) * params.slice_scale; + clusters.stale = true; + } + clusters.params = params; + auto const block{ctx.ring.allocate(sizeof(gl::cluster_ubs), ctx.device.caps().limits.minUniformBufferOffsetAlignment)}; + if (false == bool(block)) + { + clusters.params.valid = 0; + return; + } + std::memcpy(block.data, &clusters.params, sizeof(gl::cluster_ubs)); + clusters.params_offset = block.offset; + if (usable && clusters.stale) + { + // the boxes of the clusters depend only on the projection and the depth range + std::array buffers{{{block.buffer, block.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}, + {ctx.ring.handle(), 0, sizeof(gl::light_data)}}}; + VkMemoryBarrier2 barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; + barrier.srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + barrier.srcAccessMask = VK_ACCESS_2_SHADER_STORAGE_READ_BIT; + barrier.dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + barrier.dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT; + VkDependencyInfo dependency{}; + 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); + 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); + barrier.srcAccessMask = VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_2_SHADER_STORAGE_READ_BIT; + vkCmdPipelineBarrier2(commands, &dependency); + clusters.stale = false; + } +} + +// picking: the colours of the controls of the cab or of the nodes of the scenery, and the one under the cursor read back + +void vulkan14_renderer::render_picking(rendermode const Mode) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto const commands{ctx.commands}; + auto &readback{Mode == rendermode::pickcontrols ? scene.pick_controls : scene.pick_scenery}; + if (readback.pending || m_viewports.empty()) + { + return; + } + auto &vp{m_viewports.front()}; + setup_pass(vp, m_renderpass, Mode); + ctx.passes.begin_pass(Mode == rendermode::pickcontrols ? "pick controls" : "pick scenery", {vk14::use_of(*scene.pick_colour, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, colour_stage, colour_access), + vk14::use_of(*scene.pick_depth, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)}); + VkClearValue colourclear{}; + colourclear.color = {{0.f, 0.f, 0.f, 0.f}}; + VkClearValue depthclear{}; + depthclear.depthStencil = {0.f, 0}; + auto colour{attachment_info(scene.pick_colour->view(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, true, colourclear)}; + auto depth{attachment_info(scene.pick_depth->view(), VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, true, depthclear)}; + VkRenderingInfo rendering{}; + rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; + rendering.renderArea = {{0, 0}, {static_cast(pick_buffer_size), static_cast(pick_buffer_size)}}; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &colour; + rendering.pDepthAttachment = &depth; + vkCmdBeginRendering(commands, &rendering); + start_draws(scene.pick_formats, pick_buffer_size, pick_buffer_size, vk14::scene_state::set_plain, false); + setup_matrices(); + walk_blending(false); + scene_ubs.projection = OpenGLMatrices.data(GL_PROJECTION); + upload_scene_block(); + upload_light_header(); + if (Mode == rendermode::pickcontrols) + { + m_picksurfaceitems.clear(); + m_pickcontrolsitems.clear(); + Render_cab(simulation::Train->Dynamic(), 0.0f); + Render(simulation::Train->Dynamic()); + } + else + { + m_picksceneryitems.clear(); + Render(simulation::Region); + } + vkCmdEndRendering(commands); + end_draws(); + + auto buffer{std::make_shared()}; + if (buffer->create(ctx.memory, 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::readback, "pick colour")) + { + vk14::barriers(commands, {vk14::use_of(*scene.pick_colour, 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}; + region.imageOffset = {m_pickposition.x, m_pickposition.y, 0}; + region.imageExtent = {1, 1, 1}; + VkCopyImageToBufferInfo2 copy{}; + copy.sType = VK_STRUCTURE_TYPE_COPY_IMAGE_TO_BUFFER_INFO_2; + copy.srcImage = scene.pick_colour->handle(); + copy.srcImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + copy.dstBuffer = buffer->handle(); + copy.regionCount = 1; + copy.pRegions = ®ion; + vkCmdCopyImageToBuffer2(commands, ©); + host_read_barrier(commands); + readback.data = buffer; + readback.value = ctx.timelinevalue + 1; + readback.pending = true; + } + ctx.passes.end_pass(); + m_renderpass = m_colorpass; +} + +namespace +{ +// the place in the image of the picking under the cursor, in the rows of opengl +glm::ivec2 pick_position() +{ + glm::vec2 mousepos = Global.cursor_pos; + mousepos.y = Global.window_size.y - mousepos.y; + auto pickbufferpos{glm::ivec2(mousepos * glm::vec2(pick_buffer_size) / glm::max(glm::vec2(Global.window_size), glm::vec2(1.f)))}; + return glm::clamp(pickbufferpos, glm::ivec2(0, 0), glm::ivec2(pick_buffer_size - 1, pick_buffer_size - 1)); +} +} // namespace + +void vulkan14_renderer::Update_Pick_Control() +{ + if (!m_context) + { + return; + } + auto &ctx{*m_context}; + auto &readback{ctx.scene.pick_controls}; + if (readback.pending) + { + if (ctx.completed() < readback.value) + { + return; + } + readback.pending = false; + readback.data->invalidate(0, 4); + auto const *data{readback.data->mapped()}; + auto const controlindex = pick_index(glm::ivec3{data[0], data[1], data[2]}); + TSubModel const *control{nullptr}; + glm::vec2 position(0.0f); + if ((controlindex > 0) && (controlindex <= m_pickcontrolsitems.size())) + { + control = m_pickcontrolsitems[controlindex - 1]; + } + else if (255 - data[0] < m_picksurfaceitems.size()) + { + control = m_picksurfaceitems[255 - data[0]]; + position.x = data[1] / 255.0f; + position.y = data[2] / 255.0f; + } + m_pickcontrolitem = control; + for (auto const &callback : m_control_pick_requests) + { + callback(m_pickcontrolitem, position); + } + m_control_pick_requests.clear(); + readback.data.reset(); + } + if (false == m_control_pick_requests.empty()) + { + m_pickposition = pick_position(); + m_pickcontrolswanted = true; + } +} + +void vulkan14_renderer::Update_Pick_Node() +{ + if (!m_context) + { + return; + } + auto &ctx{*m_context}; + auto &readback{ctx.scene.pick_scenery}; + if (readback.pending) + { + if (ctx.completed() < readback.value) + { + return; + } + readback.pending = false; + readback.data->invalidate(0, 4); + auto const *data{readback.data->mapped()}; + auto const nodeindex = pick_index(glm::ivec3{data[0], data[1], data[2]}); + scene::basic_node *node{nullptr}; + if ((nodeindex > 0) && (nodeindex <= m_picksceneryitems.size())) + { + node = m_picksceneryitems[nodeindex - 1]; + } + m_picksceneryitem = node; + for (auto const &callback : m_node_pick_requests) + { + callback(m_picksceneryitem); + } + m_node_pick_requests.clear(); + readback.data.reset(); + } + if (false == m_node_pick_requests.empty()) + { + m_pickposition = pick_position(); + m_picknodewanted = true; + } +} + +void vulkan14_renderer::Pick_Control_Callback(std::function Callback) +{ + if (!Global.render_cab) + { + Callback(nullptr, glm::vec2()); + return; + } + m_control_pick_requests.push_back(Callback); +} + +void vulkan14_renderer::Pick_Node_Callback(std::function Callback) +{ + m_node_pick_requests.push_back(Callback); +} + +TSubModel const *vulkan14_renderer::Pick_Control() const +{ + return m_pickcontrolitem; +} + +scene::basic_node const *vulkan14_renderer::Pick_Node() const +{ + return m_picksceneryitem; +} + +glm::dvec3 vulkan14_renderer::Mouse_Position() const +{ + return m_worldmousecoordinates; +} + +bool vulkan14_renderer::Mouse_Hit() const +{ + return m_mousehit; +} + +glm::mat4 vulkan14_renderer::Camera_View_Matrix() const +{ + return glm::mat4(glm::mat3(m_colorpass.pass_camera.modelview())); +} + +glm::mat4 vulkan14_renderer::Camera_Projection_Matrix() const +{ + return m_colorpass.pass_camera.projection(); +} + +glm::dvec3 vulkan14_renderer::Camera_Position() const +{ + return m_colorpass.pass_camera.position(); +} + +// the position of the cursor of the editor in the scene: the depth under it read back from an earlier frame +glm::dvec3 vulkan14_renderer::Update_Mouse_Position() +{ + if (m_context) + { + auto &ctx{*m_context}; + auto &cursor{ctx.scene.cursor}; + if (cursor.pending && (ctx.completed() >= cursor.value) && (false == ctx.scene.targets.empty())) + { + cursor.pending = false; + float pointdepth{0.f}; + cursor.data->invalidate(0, sizeof(float)); + std::memcpy(&pointdepth, cursor.data->mapped(), sizeof(float)); + cursor.data.reset(); + auto const extent{ctx.scene.targets.front().extent}; + m_mousehit = pointdepth > 0.0f; + if (pointdepth > 0.0f) + { + m_worldmousecoordinates = glm::unProjectZO(glm::vec3(cursor.position, pointdepth), glm::mat4(glm::mat3(m_colorpass.pass_camera.modelview())), m_colorpass.pass_camera.projection(), + glm::vec4(0, 0, extent.width, extent.height)); + } + } + } + return m_colorpass.pass_camera.position() + glm::dvec3{m_worldmousecoordinates}; +} + +// screenshots: the output of the main viewport copied with the frame, saved once the gpu is done with it +void vulkan14_renderer::MakeScreenshot() +{ + if (m_context) + { + m_context->scene.screenshot_wanted = true; + } +} + +void vulkan14_renderer::update_screenshot() +{ + if (!m_context) + { + return; + } + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + if ((false == scene.screenshot_pending) || (ctx.completed() < scene.screenshot_value)) + { + return; + } + scene.screenshot_pending = false; + auto const width{static_cast(scene.screenshot_extent.width)}; + auto const height{static_cast(scene.screenshot_extent.height)}; + scene.screenshot->invalidate(0, VK_WHOLE_SIZE); + auto const *rgba{scene.screenshot->mapped()}; + // rgb rows as glReadPixels() of the opengl renderer gives them: the rows of the images are those of opengl (decision K8) + auto *image = new char[static_cast(width) * height * 3]; + for (std::size_t pixel = 0; pixel < static_cast(width) * height; ++pixel) + { + image[pixel * 3 + 0] = static_cast(rgba[pixel * 4 + 0]); + image[pixel * 3 + 1] = static_cast(rgba[pixel * 4 + 1]); + image[pixel * 3 + 2] = static_cast(rgba[pixel * 4 + 2]); + } + scene.screenshot.reset(); + screenshot_manager::save(image, width, height); +} + +// textures used only by vehicles farther than gfx.textures.releasedistance times the draw range give their images back +// (opengl46_renderer::Update_Texture_Release()) +void vulkan14_renderer::update_texture_release() +{ + if (!m_context || !m_context->textures || (m_colorpass.draw_range <= 0.f)) + { + return; + } + auto &textures{*m_context->textures}; + auto const range{static_cast(Global.gfx_texture_releasedistance) * m_colorpass.draw_range}; + auto const camera{m_colorpass.pass_camera.position()}; + textures.begin_release_scan(); + for (auto *vehicle : simulation::Vehicles.sequence()) + { + if (vehicle == nullptr) + { + continue; + } + bool const isnear{glm::length(vehicle->vPosition - camera) - vehicle->radius() <= range}; + auto const mark_model = [&](TModel3d *Model) { + if (Model == nullptr) + { + return; + } + for (auto const texture : Model_Textures(Model)) + { + textures.mark_as_needed(texture, isnear); + } + }; + mark_model(vehicle->mdModel); + mark_model(vehicle->mdLowPolyInt); + mark_model(vehicle->mdLoad); + for (auto *attachment : vehicle->mdAttachments) + { + mark_model(attachment); + } + for (auto const skin : vehicle->Material()->replacable_skins) + { + if (skin <= 0) + { + continue; + } + auto const &material{m_materials.material(skin)}; + for (auto const texture : material.textures) + { + textures.mark_as_needed(texture, isnear); + } + for (auto const &variant : material.texture_variants) + { + for (auto const texture : variant) + { + textures.mark_as_needed(texture, isnear); + } + } + } + } + textures.release_unneeded(); +} + +std::vector const &vulkan14_renderer::Model_Textures(TModel3d *Model) +{ + auto const lookup{m_modeltextures.find(Model)}; + if (lookup != m_modeltextures.end()) + { + return lookup->second; + } + auto &textures{m_modeltextures[Model]}; + std::vector submodels; + if (Model->GetSMRoot() != nullptr) + { + submodels.emplace_back(Model->GetSMRoot()); + } + while (false == submodels.empty()) + { + auto *submodel{submodels.back()}; + submodels.pop_back(); + if (submodel->NextGet() != nullptr) + { + submodels.emplace_back(submodel->NextGet()); + } + if (submodel->ChildGet() != nullptr) + { + submodels.emplace_back(submodel->ChildGet()); + } + // negative material is one of replaceable skins of the vehicle, these are handled with the vehicle + if (submodel->GetMaterial() <= 0) + { + continue; + } + auto const &material{m_materials.material(submodel->GetMaterial())}; + for (auto const texture : material.textures) + { + if (texture > 0) + { + textures.emplace_back(texture); + } + } + for (auto const &variant : material.texture_variants) + { + for (auto const texture : variant) + { + if (texture > 0) + { + textures.emplace_back(texture); + } + } + } + } + std::sort(std::begin(textures), std::end(textures)); + textures.erase(std::unique(std::begin(textures), std::end(textures)), std::end(textures)); + return textures; +} + +// a preview of a model (opengl46_renderer::Render_Preview()) isn't made by the vulkan renderer yet: the caller falls back to no +// preview +bool vulkan14_renderer::Render_Preview(TAnimModel *Instance, int const Size, int const Margin, bool const Shadows, std::vector &Image) +{ + return false; +} diff --git a/rendering/vulkan14/vulkan14geometry.cpp b/rendering/vulkan14/vulkan14geometry.cpp new file mode 100644 index 00000000..4659840d --- /dev/null +++ b/rendering/vulkan14/vulkan14geometry.cpp @@ -0,0 +1,280 @@ +/* +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/vulkan14geometry.h" +#include "rendering/vulkan14/vulkan14texture.h" + +namespace vk14 +{ + +namespace +{ + +// what opengl reads for the user data of a vertex without any (the attribute isn't given) +gfx::vertex_userdata const no_userdata{glm::vec4{0.f, 0.f, 0.f, 1.f}}; + +} // namespace + +VkPrimitiveTopology topology_of(unsigned int const Type) +{ + switch (Type) + { + case GL_POINTS: + return VK_PRIMITIVE_TOPOLOGY_POINT_LIST; + case GL_LINES: + return VK_PRIMITIVE_TOPOLOGY_LINE_LIST; + case GL_LINE_STRIP: + case GL_LINE_LOOP: + return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; + case GL_TRIANGLE_STRIP: + return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + case GL_TRIANGLE_FAN: + return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN; + case GL_TRIANGLES: + default: + return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + } +} + +vulkan14_geometrybank::~vulkan14_geometrybank() +{ + release_(); +} + +void vulkan14_geometrybank::create_(gfx::geometry_handle const &Geometry) +{ + // a new chunk: the buffers are made anew with it + m_layoutdirty = true; +} + +void vulkan14_geometrybank::replace_(gfx::geometry_handle const &Geometry) +{ + if (m_layoutdirty) + { + return; + } + auto const index{Geometry.chunk - 1}; + if (index >= m_places.size()) + { + m_layoutdirty = true; + return; + } + auto const &chunk{gfx::geometry_bank::chunk(Geometry)}; + auto &place{m_places[index]}; + auto const loop{chunk.type == GL_LINE_LOOP}; + auto const vertexcount{static_cast(chunk.vertices.size()) + ((loop && chunk.indices.empty() && (false == chunk.vertices.empty())) ? 1u : 0u)}; + auto const indexcount{static_cast(chunk.indices.size()) + ((loop && (false == chunk.indices.empty())) ? 1u : 0u)}; + if ((vertexcount != place.vertexcount) || (indexcount != place.indexcount)) + { + // it doesn't fit in its place any more + m_layoutdirty = true; + return; + } + place.dirty = true; + m_chunksdirty = true; +} + +void vulkan14_geometrybank::release_() +{ + auto *deletions{m_resources.deletions}; + if ((deletions != nullptr) && (m_resources.uploads != nullptr) && (m_vertices || m_userdata || m_indices)) + { + deletions->push(m_resources.uploads->current_value(), [vertices = std::move(m_vertices), userdata = std::move(m_userdata), indices = std::move(m_indices)]() {}); + } + m_vertices.reset(); + m_userdata.reset(); + m_indices.reset(); + m_places.clear(); + m_bytes = 0; + m_layoutdirty = true; + m_chunksdirty = false; +} + +void vulkan14_geometrybank::gpu_vertices(geometry_chunk const &Chunk, std::vector &Vertices, std::vector &Userdata) const +{ + Vertices.insert(Vertices.end(), Chunk.vertices.begin(), Chunk.vertices.end()); + if (Chunk.userdata.size() >= Chunk.vertices.size()) + { + Userdata.insert(Userdata.end(), Chunk.userdata.begin(), Chunk.userdata.begin() + Chunk.vertices.size()); + } + else + { + Userdata.insert(Userdata.end(), Chunk.userdata.begin(), Chunk.userdata.end()); + Userdata.resize(Vertices.size(), no_userdata); + } + if ((Chunk.type == GL_LINE_LOOP) && Chunk.indices.empty() && (false == Chunk.vertices.empty())) + { + // back to the first vertex + Vertices.push_back(Chunk.vertices.front()); + Userdata.push_back(Chunk.userdata.empty() ? no_userdata : Chunk.userdata.front()); + } +} + +bool vulkan14_geometrybank::build() +{ + release_(); + + std::vector vertices; + std::vector userdata; + std::vector indices; + m_places.resize(m_chunks.size()); + for (std::size_t i = 0; i < m_chunks.size(); ++i) + { + auto const &chunk{m_chunks[i]}; + auto &place{m_places[i]}; + place.vertexfirst = static_cast(vertices.size()); + place.indexfirst = static_cast(indices.size()); + gpu_vertices(chunk, vertices, userdata); + place.vertexcount = static_cast(vertices.size()) - place.vertexfirst; + indices.insert(indices.end(), chunk.indices.begin(), chunk.indices.end()); + if ((chunk.type == GL_LINE_LOOP) && (false == chunk.indices.empty())) + { + indices.push_back(chunk.indices.front()); + } + place.indexcount = static_cast(indices.size()) - place.indexfirst; + place.dirty = false; + } + m_layoutdirty = false; + m_chunksdirty = false; + if (vertices.empty()) + { + return false; + } + + auto &memory{*m_resources.memory}; + auto &uploads{*m_resources.uploads}; + auto const make = [&](std::shared_ptr &Buffer, void const *Data, VkDeviceSize const Size, VkBufferUsageFlags const Usage, char const *Name) { + Buffer = std::make_shared(); + if ((false == Buffer->create(memory, Size, Usage | VK_BUFFER_USAGE_TRANSFER_DST_BIT, memory_kind::device, Name)) || (false == uploads.upload_buffer(Buffer, Data, Size))) + { + Buffer.reset(); + return false; + } + m_bytes += Size; + return true; + }; + auto ok{make(m_vertices, vertices.data(), vertices.size() * sizeof(gfx::basic_vertex), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, "geometry vertices") && + make(m_userdata, userdata.data(), userdata.size() * sizeof(gfx::vertex_userdata), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, "geometry user data")}; + if (ok && (false == indices.empty())) + { + ok = make(m_indices, indices.data(), indices.size() * sizeof(gfx::basic_index), VK_BUFFER_USAGE_INDEX_BUFFER_BIT, "geometry indices"); + } + if (false == ok) + { + log_error("no memory for the geometry of a bank of " + std::to_string(vertices.size()) + " vertices"); + release_(); + // tried again when it's drawn the next time + m_layoutdirty = true; + return false; + } + return true; +} + +void vulkan14_geometrybank::update() +{ + m_chunksdirty = false; + if ((false == bool(m_vertices)) || (false == bool(m_userdata))) + { + return; + } + std::vector vertices; + std::vector userdata; + std::vector indices; + std::vector> vertexparts, userdataparts, indexparts; + for (std::size_t i = 0; i < m_places.size(); ++i) + { + auto &place{m_places[i]}; + if (false == place.dirty) + { + continue; + } + place.dirty = false; + auto const &chunk{m_chunks[i]}; + gpu_vertices(chunk, vertices, userdata); + vertexparts.emplace_back(place.vertexfirst * sizeof(gfx::basic_vertex), place.vertexcount * sizeof(gfx::basic_vertex)); + userdataparts.emplace_back(place.vertexfirst * sizeof(gfx::vertex_userdata), place.vertexcount * sizeof(gfx::vertex_userdata)); + if ((place.indexcount > 0) && m_indices) + { + indices.insert(indices.end(), chunk.indices.begin(), chunk.indices.end()); + if (chunk.type == GL_LINE_LOOP) + { + indices.push_back(chunk.indices.front()); + } + indexparts.emplace_back(place.indexfirst * sizeof(gfx::basic_index), place.indexcount * sizeof(gfx::basic_index)); + } + } + auto &uploads{*m_resources.uploads}; + if (false == vertices.empty()) + { + uploads.upload_buffer(m_vertices, vertices.data(), vertices.size() * sizeof(gfx::basic_vertex), vertexparts); + uploads.upload_buffer(m_userdata, userdata.data(), userdata.size() * sizeof(gfx::vertex_userdata), userdataparts); + } + if (false == indices.empty()) + { + uploads.upload_buffer(m_indices, indices.data(), indices.size() * sizeof(gfx::basic_index), indexparts); + } +} + +bool vulkan14_geometrybank::source(gfx::geometry_handle const &Geometry, chunk_source &Source) +{ + if ((Geometry.chunk == 0) || (Geometry.chunk > m_chunks.size())) + { + return false; + } + if (m_layoutdirty) + { + build(); + } + else if (m_chunksdirty) + { + update(); + } + auto const index{Geometry.chunk - 1}; + if ((false == bool(m_vertices)) || (index >= m_places.size())) + { + return false; + } + auto const &place{m_places[index]}; + Source.vertices = m_vertices->handle(); + Source.userdata = m_userdata->handle(); + Source.topology = topology_of(m_chunks[index].type); + if (place.indexcount > 0) + { + Source.indices = m_indices->handle(); + Source.count = place.indexcount; + Source.first = place.indexfirst; + Source.basevertex = static_cast(place.vertexfirst); + } + else + { + Source.indices = VK_NULL_HANDLE; + Source.count = place.vertexcount; + Source.first = place.vertexfirst; + Source.basevertex = 0; + } + return Source.count > 0; +} + +bool vulkan14_geometrybank::indirect_source_(gfx::geometry_handle const &Geometry, gfx::indirect_source &Source) +{ + m_last = chunk_source{}; + if (false == source(Geometry, m_last)) + { + return false; + } + Source.count = m_last.count; + Source.first = m_last.first; + Source.basevertex = m_last.basevertex; + Source.indexed = (m_last.indices != VK_NULL_HANDLE); + Source.type = m_chunks[Geometry.chunk - 1].type; + return true; +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14geometry.h b/rendering/vulkan14/vulkan14geometry.h new file mode 100644 index 00000000..9c1cb55f --- /dev/null +++ b/rendering/vulkan14/vulkan14geometry.h @@ -0,0 +1,99 @@ +/* +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 geometry banks of the vulkan renderer (stage D1 of the plan): the chunks of a bank in three device buffers of the bank +// (vertices, their user data, 32 bit indices), made the first time the bank is drawn after its chunks changed. a chunk replaced +// with the same size is copied in place, otherwise the buffers are made anew and the old ones go once the gpu is done with them. +// the data stays in memory as well (gfx::geometry_bank), the simulation reads it back. the primitives are the values of opengl +// the program gives them with; a line loop is drawn as a strip which goes back to its first vertex + +#pragma once + +#include "rendering/geometrybank.h" +#include "rendering/vulkan14/vulkan14common.h" +#include "rendering/vulkan14/vulkan14memory.h" + +#include + +namespace vk14 +{ + +struct resources; + +// where the data of a chunk is in the buffers of its bank +struct chunk_source +{ + VkBuffer vertices{VK_NULL_HANDLE}; + VkBuffer userdata{VK_NULL_HANDLE}; + VkBuffer indices{VK_NULL_HANDLE}; // null for a chunk without indices + std::uint32_t count{0}; // vertices, or indices + std::uint32_t first{0}; // first index, or vertex + std::int32_t basevertex{0}; + VkPrimitiveTopology topology{VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST}; +}; + +// the vulkan primitive of a primitive of opengl (GL_TRIANGLES ...) +VkPrimitiveTopology topology_of(unsigned int Type); + +class vulkan14_geometrybank : public gfx::geometry_bank +{ + public: + explicit vulkan14_geometrybank(resources const &Resources) : m_resources(Resources) {} + ~vulkan14_geometrybank() override; + + // the chunk with its data on the gpu: uploaded now when it changed since the last time. false: nothing to draw. + // gfx::geometrybank_manager::indirect_source() calls it too (which marks the bank as used), and keeps its answer in last() + bool source(gfx::geometry_handle const &Geometry, chunk_source &Source); + chunk_source const &last() const + { + return m_last; + } + // the bytes on the gpu, for the debug panel + VkDeviceSize gpu_bytes() const + { + return m_bytes; + } + + private: + struct chunk_place + { + std::uint32_t vertexfirst{0}; + std::uint32_t vertexcount{0}; // on the gpu: one more than the chunk for a line loop + std::uint32_t indexfirst{0}; + std::uint32_t indexcount{0}; + bool dirty{true}; + }; + + void create_(gfx::geometry_handle const &Geometry) override; + void replace_(gfx::geometry_handle const &Geometry) override; + std::size_t draw_(gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams) override + { + return 0; + } + bool indirect_source_(gfx::geometry_handle const &Geometry, gfx::indirect_source &Source) override; + void release_() override; + // the buffers made anew for all the chunks + bool build(); + // the chunks changed in place copied to the buffers + void update(); + // the vertices of a chunk as they go to the gpu + void gpu_vertices(geometry_chunk const &Chunk, std::vector &Vertices, std::vector &Userdata) const; + + resources const &m_resources; + std::shared_ptr m_vertices; + std::shared_ptr m_userdata; + std::shared_ptr m_indices; + std::vector m_places; + bool m_layoutdirty{true}; // the buffers have to be made anew + bool m_chunksdirty{false}; // some chunks have to be copied + VkDeviceSize m_bytes{0}; + chunk_source m_last; +}; + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14memory.cpp b/rendering/vulkan14/vulkan14memory.cpp index 3155bc54..2ee82dea 100644 --- a/rendering/vulkan14/vulkan14memory.cpp +++ b/rendering/vulkan14/vulkan14memory.cpp @@ -177,6 +177,14 @@ void buffer::flush(VkDeviceSize Offset, VkDeviceSize Size) const } } +void buffer::invalidate(VkDeviceSize Offset, VkDeviceSize Size) const +{ + if ((m_allocation != VK_NULL_HANDLE) && (Size > 0)) + { + vmaInvalidateAllocation(m_allocator, m_allocation, Offset, Size); + } +} + // image image::~image() @@ -334,12 +342,12 @@ void deletion_queue::flush() // frame ring -bool frame_ring::create(allocator const &Allocator, VkDeviceSize SizePerFrame, std::uint32_t Frames, VkBufferUsageFlags Usage, std::string const &Name) +bool frame_ring::create(allocator const &Allocator, VkDeviceSize SizePerFrame, std::uint32_t Frames, VkBufferUsageFlags Usage, std::string const &Name, VkDeviceSize Tail) { m_sizeperframe = SizePerFrame; m_base = 0; m_used = 0; - return m_buffer.create(Allocator, SizePerFrame * Frames, Usage, memory_kind::upload, Name) && (m_buffer.mapped() != nullptr); + return m_buffer.create(Allocator, SizePerFrame * Frames + Tail, Usage, memory_kind::upload, Name) && (m_buffer.mapped() != nullptr); } void frame_ring::destroy() diff --git a/rendering/vulkan14/vulkan14memory.h b/rendering/vulkan14/vulkan14memory.h index 01e15b2a..d97b2874 100644 --- a/rendering/vulkan14/vulkan14memory.h +++ b/rendering/vulkan14/vulkan14memory.h @@ -91,6 +91,8 @@ class buffer } // makes the writes of the cpu seen by the gpu where the memory isn't coherent void flush(VkDeviceSize Offset, VkDeviceSize Size) const; + // makes the writes of the gpu seen by the cpu where the memory isn't coherent (readback memory may be cached) + void invalidate(VkDeviceSize Offset, VkDeviceSize Size) const; explicit operator bool() const { return m_buffer != VK_NULL_HANDLE; @@ -214,7 +216,8 @@ struct ring_slice class frame_ring { public: - bool create(allocator const &Allocator, VkDeviceSize SizePerFrame, std::uint32_t Frames, VkBufferUsageFlags Usage, std::string const &Name); + // Tail: bytes past the part of the last frame, so a descriptor of a fixed range can be given an offset anywhere in a part + bool create(allocator const &Allocator, VkDeviceSize SizePerFrame, std::uint32_t Frames, VkBufferUsageFlags Usage, std::string const &Name, VkDeviceSize Tail = 0); void destroy(); // the part of the frame Frame is used from its start again void begin_frame(std::uint32_t Frame); @@ -226,10 +229,20 @@ class frame_ring { return m_used; } + // the offset in the buffer the next allocation aligned to Alignment would get + VkDeviceSize next_offset(VkDeviceSize Alignment) const + { + auto const alignment{std::max(Alignment, 4)}; + return m_base + (m_used + alignment - 1) / alignment * alignment; + } VkDeviceSize capacity() const { return m_sizeperframe; } + VkBuffer handle() const + { + return m_buffer.handle(); + } private: buffer m_buffer; diff --git a/rendering/vulkan14/vulkan14renderer.cpp b/rendering/vulkan14/vulkan14renderer.cpp index 0fed2880..04c6ff11 100644 --- a/rendering/vulkan14/vulkan14renderer.cpp +++ b/rendering/vulkan14/vulkan14renderer.cpp @@ -8,18 +8,7 @@ http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" -#include "rendering/vulkan14/vulkan14renderer.h" -#include "rendering/vulkan14/vulkan14common.h" -#include "rendering/vulkan14/vulkan14instance.h" -#include "rendering/vulkan14/vulkan14device.h" -#include "rendering/vulkan14/vulkan14swapchain.h" -#include "rendering/vulkan14/vulkan14memory.h" -#include "rendering/vulkan14/vulkan14shaders.h" -#include "rendering/vulkan14/vulkan14descriptors.h" -#include "rendering/vulkan14/vulkan14passes.h" -#include "rendering/vulkan14/vulkan14texture.h" -#include "rendering/vulkan14/vulkan14imgui.h" -#include "rendering/vulkan14/vulkan14bindings.h" +#include "rendering/vulkan14/vulkan14context.h" #include "rendering/nullrenderer.h" #include "application/application.h" #include "simulation/simulation.h" @@ -37,8 +26,11 @@ namespace // background of the opengl renderer (srgb). before that the windows are black, as with the opengl renderer glm::vec3 const clear_colour{51.f / 255.f, 102.f / 255.f, 85.f / 255.f}; -// the ring each frame in flight streams its data through (the user interface for now) -VkDeviceSize const ring_size{8 * 1024 * 1024}; +// the ring each frame in flight streams its data through: the user interface, the blocks of the scene and of the light clusters, +// the light buffers of the passes, the vertices of the particles and the sky; the descriptors read the light buffers with the range +// of the whole gl::light_data, which the tail past the last frame leaves room for +VkDeviceSize const ring_size{16 * 1024 * 1024}; +VkDeviceSize const ring_tail{(sizeof(gl::light_data) + 65535) / 65536 * 65536}; float srgb_to_linear(float const Value) { @@ -47,192 +39,157 @@ float srgb_to_linear(float const Value) } // namespace -// the objects of the vulkan api. destroyed in the reverse order: the swapchains and the frames, then the device, then the instance -struct vulkan14_renderer::context +bool vulkan14_renderer::context::create_frames(std::uint32_t const Count, std::string &Reason) { - struct frame - { - 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 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 - std::uint64_t number{0}; - }; + auto const handle{device.handle()}; - vk14::instance instance; - vk14::device device; - vk14::allocator memory; - vk14::deletion_queue deletions; - vk14::uploader uploads; - vk14::image_table images; - vk14::samplers sampling; - vk14::frame_ring ring; - vk14::shader_library shaders; - vk14::pipeline_library pipelines; - vk14::pass_list passes; - vk14::pending_textures pending; - vk14::resources resources; - vk14::imgui_backend imgui; - // the textures of the files; made after the objects above and gone before them - std::unique_ptr textures; - // [0]: the main window, then the windows of the extra viewports - std::vector> windows; - std::vector frames; - // one semaphore counts the frames done by the gpu; the binary ones are only between the swapchains and the queue - VkSemaphore timeline{VK_NULL_HANDLE}; - std::uint64_t timelinevalue{0}; - // frames recorded since the start - std::uint64_t framecount{0}; - // the windows with an image acquired in the frame being made - std::vector acquired; - bool devicelost{false}; - - ~context() + VkSemaphoreTypeCreateInfo timelineinfo{}; + timelineinfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO; + timelineinfo.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; + timelineinfo.initialValue = 0; + VkSemaphoreCreateInfo semaphoreinfo{}; + semaphoreinfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + semaphoreinfo.pNext = &timelineinfo; + if (!VK14_VERIFY(vkCreateSemaphore(handle, &semaphoreinfo, nullptr, &timeline))) { - destroy(); + Reason = "no timeline semaphore"; + return false; } + instance.set_object_name(handle, VK_OBJECT_TYPE_SEMAPHORE, reinterpret_cast(timeline), "frame timeline"); - bool create_frames(std::uint32_t const Count, std::string &Reason) + frames.resize(Count); + for (std::uint32_t i = 0; i < Count; ++i) { - auto const handle{device.handle()}; - - VkSemaphoreTypeCreateInfo timelineinfo{}; - timelineinfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO; - timelineinfo.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; - timelineinfo.initialValue = 0; - VkSemaphoreCreateInfo semaphoreinfo{}; - semaphoreinfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; - semaphoreinfo.pNext = &timelineinfo; - if (!VK14_VERIFY(vkCreateSemaphore(handle, &semaphoreinfo, nullptr, &timeline))) + VkCommandPoolCreateInfo poolinfo{}; + poolinfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + poolinfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT; + poolinfo.queueFamilyIndex = device.families().graphics; + if (!VK14_VERIFY(vkCreateCommandPool(handle, &poolinfo, nullptr, &frames[i].pool))) { - Reason = "no timeline semaphore"; + Reason = "no command pool"; return false; } - instance.set_object_name(handle, VK_OBJECT_TYPE_SEMAPHORE, reinterpret_cast(timeline), "frame timeline"); - - frames.resize(Count); - for (std::uint32_t i = 0; i < Count; ++i) + 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))) { - VkCommandPoolCreateInfo poolinfo{}; - poolinfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; - poolinfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT; - poolinfo.queueFamilyIndex = device.families().graphics; - if (!VK14_VERIFY(vkCreateCommandPool(handle, &poolinfo, nullptr, &frames[i].pool))) - { - 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))) - { - 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)); + 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)); + } + return true; +} + +// 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) +{ + if (false == memory.create(instance, device, Reason)) + { + return false; + } + uploads.init(memory, deletions, device.physical()); + if (false == sampling.create(device, static_cast(Global.AnisotropicFiltering), Reason)) + { + return false; + } + 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)) + { + Reason = "no ring for the data of the frames"; + return false; + } + if ((false == shaders.init(vk14::binding_preamble(), Reason)) || (false == pipelines.init(device, Reason)) || (false == passes.create(device, Frames, Reason))) + { + return false; + } + resources.gpu = &device; + resources.memory = &memory; + resources.uploads = &uploads; + resources.deletions = &deletions; + resources.sampling = &sampling; + resources.images = &images; + resources.thread = std::this_thread::get_id(); + resources.pending = &pending; + textures = std::make_unique([this]() -> texture_source * { return new vk14::vulkan14_texture(resources); }); + return imgui.create(resources, shaders, pipelines, ring, Reason); +} + +// the value of the timeline the gpu reached, 0 when it can't be read +std::uint64_t vulkan14_renderer::context::completed() const +{ + std::uint64_t value{0}; + if ((timeline == VK_NULL_HANDLE) || (vkGetSemaphoreCounterValue(device.handle(), timeline, &value) != VK_SUCCESS)) + { + return 0; + } + return value; +} + +bool vulkan14_renderer::context::wait(std::uint64_t const Value) const +{ + if ((Value == 0) || (completed() >= Value)) + { return true; } + VkSemaphoreWaitInfo wait{}; + wait.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO; + wait.semaphoreCount = 1; + wait.pSemaphores = &timeline; + wait.pValues = &Value; + return VK14_VERIFY(vkWaitSemaphores(device.handle(), &wait, UINT64_MAX)); +} - // the parts of the renderer made once the device is there - bool create_core(std::uint32_t const Frames, std::string &Reason) +void vulkan14_renderer::context::destroy() +{ + auto const handle{device.handle()}; + if (handle != VK_NULL_HANDLE) { - if (false == memory.create(instance, device, Reason)) - { - return false; - } - uploads.init(memory, deletions, device.physical()); - if (false == sampling.create(device, static_cast(Global.AnisotropicFiltering), Reason)) - { - return false; - } - 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")) - { - Reason = "no ring for the data of the frames"; - return false; - } - if ((false == shaders.init(vk14::binding_preamble(), Reason)) || (false == pipelines.init(device, Reason)) || (false == passes.create(device, Frames, Reason))) - { - return false; - } - resources.gpu = &device; - resources.memory = &memory; - resources.uploads = &uploads; - resources.deletions = &deletions; - resources.sampling = &sampling; - resources.images = &images; - resources.thread = std::this_thread::get_id(); - resources.pending = &pending; - textures = std::make_unique([this]() -> texture_source * { return new vk14::vulkan14_texture(resources); }); - return imgui.create(resources, shaders, pipelines, ring, Reason); + // the end of the renderer, not a frame: everything on the gpu has to be done + vkDeviceWaitIdle(handle); } - - // the value of the timeline the gpu reached, 0 when it can't be read - std::uint64_t completed() const + acquired.clear(); + windows.clear(); + imgui.destroy(); + // the textures give their images to the deletion queue, which goes now that the gpu is idle + textures.reset(); + uploads.end_frame(); + uploads = vk14::uploader{}; + images.clear(); + deletions.flush(); + pipelines.shutdown(); + shaders.shutdown(); + passes.destroy(); + ring.destroy(); + if (handle != VK_NULL_HANDLE) { - std::uint64_t value{0}; - if ((timeline == VK_NULL_HANDLE) || (vkGetSemaphoreCounterValue(device.handle(), timeline, &value) != VK_SUCCESS)) + sampling.destroy(handle); + for (auto &frame : frames) { - return 0; - } - return value; - } - - void destroy() - { - auto const handle{device.handle()}; - if (handle != VK_NULL_HANDLE) - { - // the end of the renderer, not a frame: everything on the gpu has to be done - vkDeviceWaitIdle(handle); - } - acquired.clear(); - windows.clear(); - imgui.destroy(); - // the textures give their images to the deletion queue, which goes now that the gpu is idle - textures.reset(); - uploads.end_frame(); - uploads = vk14::uploader{}; - images.clear(); - deletions.flush(); - pipelines.shutdown(); - shaders.shutdown(); - passes.destroy(); - ring.destroy(); - if (handle != VK_NULL_HANDLE) - { - sampling.destroy(handle); - for (auto &frame : frames) + if (frame.pool != VK_NULL_HANDLE) { - if (frame.pool != VK_NULL_HANDLE) - { - vkDestroyCommandPool(handle, frame.pool, nullptr); - } - } - if (timeline != VK_NULL_HANDLE) - { - vkDestroySemaphore(handle, timeline, nullptr); + vkDestroyCommandPool(handle, frame.pool, nullptr); } } - memory.destroy(); - frames.clear(); - timeline = VK_NULL_HANDLE; - device.destroy(); - instance.destroy(); + if (timeline != VK_NULL_HANDLE) + { + vkDestroySemaphore(handle, timeline, nullptr); + } } -}; + memory.destroy(); + frames.clear(); + timeline = VK_NULL_HANDLE; + device.destroy(); + instance.destroy(); +} namespace { @@ -320,10 +277,20 @@ bool vulkan14_renderer::Init(GLFWwindow *Window) return false; } + if (false == init_scene(reason)) + { + vk14::log_error("the renderer can't start: " + reason); + destroy_scene(); + m_context.reset(); + return false; + } + init_frame_data(); + 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 scenery and the vehicles (D1-D7), picking, mouse position in the scene and screenshots (D6), 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 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)"); WriteLog("Gfx Renderer: setup complete"); update_info(); return true; @@ -331,38 +298,64 @@ bool vulkan14_renderer::Init(GLFWwindow *Window) bool vulkan14_renderer::AddViewport(global_settings::extraviewport_config const &conf) { - if (!m_context) + if (!m_context || m_viewports.empty()) { return false; } + viewport_config *viewport{nullptr}; if (conf.monitor == "MAIN") { - // the projection of the main window is changed only, which the drawing of the scenery takes care of (D6) - vk14::log_info("extra viewport on the main window: drawn from the stage D6 of the plan"); - return true; + // the main window drawn with the projection of the viewport + viewport = &m_viewports.front(); } - - // a window of its own, without an opengl context - auto *window{Application.window(-1, true, conf.width, conf.height, Application.find_monitor(conf.monitor), true, false)}; - if (window == nullptr) + else { - vk14::log_error("the window of the extra viewport on " + conf.monitor + " couldn't be made"); - return false; + // a window of its own, without an opengl context, with a swapchain of its own + auto *window{Application.window(-1, true, conf.width, conf.height, Application.find_monitor(conf.monitor), true, false)}; + if (window == nullptr) + { + vk14::log_error("the window of the extra viewport on " + conf.monitor + " couldn't be made"); + return false; + } + auto &ctx{*m_context}; + 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)) + { + vk14::log_error(reason); + return false; + } + ctx.windows.emplace_back(std::move(swapchain)); + m_viewports.emplace_back(); + viewport = &m_viewports.back(); + viewport->window = ctx.windows.size() - 1; } - auto &ctx{*m_context}; - auto viewport{std::make_unique()}; - std::string reason; - if (false == viewport->create(ctx.instance, ctx.device, window, "viewport " + conf.monitor, static_cast(ctx.frames.size()), reason)) - { - vk14::log_error(reason); - return false; - } - ctx.windows.emplace_back(std::move(viewport)); + viewport->width = conf.width; + viewport->height = conf.height; + viewport->projection = conf.projection; + viewport->proj_type = viewport_config::custom; + viewport->draw_range = conf.draw_range; + vk14::log_info("viewport " + conf.monitor + ": " + std::to_string(conf.width) + "x" + std::to_string(conf.height)); return true; } 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 + vkDeviceWaitIdle(m_context->device.handle()); + } + // the geometry on the gpu goes with the context; the data of the banks stays for the simulation + for (auto *bank : m_banks) + { + if (bank != nullptr) + { + bank->release(); + } + } + destroy_scene(); + m_draw.reset(); m_context.reset(); } @@ -387,184 +380,6 @@ void vulkan14_renderer::report_device_lost(char const *Where) crashreport_add_info("vulkan_device_lost", report); } -bool vulkan14_renderer::Render() -{ - if (!m_context || m_context->devicelost) - { - return false; - } - auto &ctx{*m_context}; - auto const device{ctx.device.handle()}; - auto const slot{static_cast(ctx.framecount % ctx.frames.size())}; - auto &frame{ctx.frames[slot]}; - - Timer::subsystem.gfx_total.start(); - - // the gpu has to be done with the frame which used the slot before - if (frame.done > 0) - { - VkSemaphoreWaitInfo wait{}; - wait.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO; - wait.semaphoreCount = 1; - wait.pSemaphores = &ctx.timeline; - wait.pValues = &frame.done; - if (!VK14_VERIFY(vkWaitSemaphores(device, &wait, UINT64_MAX))) - { - report_device_lost("waiting for a frame"); - return false; - } - ctx.passes.collect(slot); - } - // what the frames done used last goes - ctx.deletions.collect(std::max(ctx.completed(), frame.done)); - VK14_VERIFY(vkResetCommandPool(device, frame.pool, 0)); - ctx.ring.begin_frame(slot); - - 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)); - auto const value{ctx.timelinevalue + 1}; - ctx.uploads.begin_frame(frame.uploads, value); - // the content other threads gave the textures (python) - { - std::vector pending; - { - std::lock_guard lock(ctx.pending.mutex); - pending.swap(ctx.pending.textures); - } - for (auto *texture : pending) - { - texture->apply_pending(); - } - } - - ctx.acquired.clear(); - for (auto &window : ctx.windows) - { - switch (window->acquire(slot)) - { - case vk14::swapchain::acquire_result::ready: - ctx.acquired.push_back(window.get()); - break; - case vk14::swapchain::acquire_result::skipped: - break; - default: - report_device_lost("acquiring an image"); - return false; - } - } - - // the user interface: imgui makes its frame, the images it asks for are uploaded in this frame - Timer::subsystem.gfx_gui.start(); - Application.render_ui(); - Timer::subsystem.gfx_gui.stop(); - - frame.number = ctx.framecount; - ctx.passes.begin_frame(frame.commands, slot, ctx.framecount); - auto const background{simulation::is_ready ? clear_colour : glm::vec3{0.f}}; - bool uidrawn{false}; - for (auto *window : ctx.acquired) - { - auto const colour{window->srgb() ? glm::vec3{srgb_to_linear(background.r), srgb_to_linear(background.g), srgb_to_linear(background.b)} : background}; - if (window != ctx.windows.front().get()) - { - // the extra viewports: their colour only, until the scenery is drawn into them (D6) - window->record_clear(frame.commands, glm::vec4{colour, 1.f}); - continue; - } - // the main window: cleared, the user interface over it, shown - ctx.passes.begin_pass("ui", {window->use(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 = window->view(); - attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; - attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; - attachment.clearValue.color = {{colour.r, colour.g, colour.b, 1.f}}; - VkRenderingInfo rendering{}; - rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO; - rendering.renderArea = {{0, 0}, window->extent()}; - rendering.layerCount = 1; - rendering.colorAttachmentCount = 1; - rendering.pColorAttachments = &attachment; - vkCmdBeginRendering(frame.commands, &rendering); - ctx.imgui.record(frame.commands, window->extent(), window->format(), window->srgb()); - vkCmdEndRendering(frame.commands); - 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)}); - uidrawn = true; - } - if (false == uidrawn) - { - // the main window is minimized: the frame of imgui has nowhere to go - ctx.imgui.skip(); - } - ctx.passes.end_frame(); - auto const uploaded{ctx.uploads.end_frame()}; - VK14_VERIFY(vkEndCommandBuffer(frame.uploads)); - VK14_VERIFY(vkEndCommandBuffer(frame.commands)); - ctx.ring.flush(); - - 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); - - 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); - } - 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); - - // the uploads first, in the same submit: their barriers make them done before the frame samples the images - std::vector commands; - if (uploaded) - { - VkCommandBufferSubmitInfo info{}; - info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; - info.commandBuffer = frame.uploads; - commands.push_back(info); - } - { - VkCommandBufferSubmitInfo info{}; - info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO; - info.commandBuffer = frame.commands; - commands.push_back(info); - } - - 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; - - Timer::subsystem.gfx_total.stop(); - return true; -} - void vulkan14_renderer::SwapBuffers() { if (!m_context) @@ -616,143 +431,71 @@ float vulkan14_renderer::Framerate() return m_framerate; } -// geometry: kept in memory for the simulation, nothing on the gpu until D1 - -gfx::geometrybank_handle vulkan14_renderer::Create_Bank() -{ - return m_geometry.register_bank(std::make_unique()); -} - -gfx::geometry_handle vulkan14_renderer::Insert(gfx::index_array &Indices, gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Geometry, int const Type) -{ - // NOTE: we expect indexed geometry to come with calculated tangents - return m_geometry.create_chunk(Indices, Vertices, Userdata, Geometry, Type); -} - -gfx::geometry_handle vulkan14_renderer::Insert(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Geometry, int const Type) -{ - gfx::calculate_tangents(Vertices, gfx::index_array(), Type); - return m_geometry.create_chunk(Vertices, Userdata, Geometry, Type); -} - -bool vulkan14_renderer::Replace(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type, std::size_t const Offset) -{ - gfx::calculate_tangents(Vertices, gfx::index_array(), Type); - return m_geometry.replace(Vertices, Userdata, Geometry, Offset); -} - -bool vulkan14_renderer::Append(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type) -{ - gfx::calculate_tangents(Vertices, gfx::index_array(), Type); - return m_geometry.append(Vertices, Userdata, Geometry); -} - -gfx::index_array const &vulkan14_renderer::Indices(gfx::geometry_handle const &Geometry) const -{ - return m_geometry.indices(Geometry); -} - -gfx::vertex_array const &vulkan14_renderer::Vertices(gfx::geometry_handle const &Geometry) const -{ - return m_geometry.vertices(Geometry); -} - -gfx::userdata_array const &vulkan14_renderer::UserData(gfx::geometry_handle const &Geometry) const -{ - return m_geometry.userdata(Geometry); -} - -// materials and their shaders: none until D1 - -material_handle vulkan14_renderer::Fetch_Material(std::string const &Filename, bool const Loadnow) -{ - return null_handle; -} - -void vulkan14_renderer::Bind_Material(material_handle const Material, TSubModel const *sm, lighting_data const *lighting) {} - -IMaterial const *vulkan14_renderer::Material(material_handle const Material) const -{ - return IMaterial::null_material(); -} - -std::shared_ptr vulkan14_renderer::Fetch_Shader(std::string const &name) -{ - return nullptr; -} - -// textures: the files read and decoded as for the opengl renderer, made into images when they are first used (create(), e.g. by the -// launcher); the materials of the scene take them from D1 - -texture_handle vulkan14_renderer::Fetch_Texture(std::string const &Filename, bool const Loadnow, GLint format_hint) -{ - if (!m_context || !m_context->textures) - { - return null_handle; - } - return m_context->textures->create(Filename, Loadnow, format_hint); -} - -// the shaders take the textures by the pushed descriptors (D1), there's nothing to bind -void vulkan14_renderer::Bind_Texture(texture_handle const Texture) {} - -void vulkan14_renderer::Bind_Texture(std::size_t const Unit, texture_handle const Texture) {} - -ITexture &vulkan14_renderer::Texture(texture_handle const Texture) -{ - if ((Texture == null_handle) || !m_context || !m_context->textures) - { - return *ITexture::null_texture(); - } - return m_context->textures->mark_as_used(Texture); -} - -ITexture const &vulkan14_renderer::Texture(texture_handle const Texture) const -{ - if ((Texture == null_handle) || !m_context || !m_context->textures) - { - return *ITexture::null_texture(); - } - return m_context->textures->texture(Texture); -} - -// picking: D6 - -void vulkan14_renderer::Pick_Control_Callback(std::function Callback) {} - -void vulkan14_renderer::Pick_Node_Callback(std::function Callback) {} - -TSubModel const *vulkan14_renderer::Pick_Control() const -{ - return nullptr; -} - -scene::basic_node const *vulkan14_renderer::Pick_Node() const -{ - return nullptr; -} - -glm::dvec3 vulkan14_renderer::Mouse_Position() const -{ - return glm::dvec3(0.0); -} - // maintenance void vulkan14_renderer::Update(double const Deltatime) { - if (m_context && m_context->textures && (true == Global.ResourceSweep) && (true == simulation::is_ready)) - { - // textures nothing used for a while give their images back - m_context->textures->update(); - } + Update_Pick_Control(); + Update_Pick_Node(); + update_screenshot(); + m_updateaccumulator += Deltatime; if (m_updateaccumulator < 1.0) { + // too early for any work return; } m_updateaccumulator = 0.0; m_framerate = 1000.f / Timer::subsystem.mainloop_total.average(); + + // adjust draw ranges etc, based on recent performance (opengl46_renderer::Update()) + if (Global.targetfps != 0.0f) + { + float fps_diff = Global.targetfps - m_framerate; + if (fps_diff > 0.0f) + Global.fDistanceFactor = std::max(0.5f, Global.fDistanceFactor - 0.05f); + else + Global.fDistanceFactor = std::min(Global.gfx_distance_factor_max, Global.fDistanceFactor + 0.05f); + } + // legacy framerate parameters + else if (Global.fFpsAverage != 0.f) + { + auto const fps_diff = m_framerate - Global.fFpsAverage; + if (fps_diff < -Global.fFpsDeviation) + { + Global.fDistanceFactor = std::min(std::max(1.0f, Global.fDistanceFactor - 0.05f), Global.gfx_distance_factor_max); + } + else if (fps_diff > Global.fFpsDeviation) + { + Global.fDistanceFactor = std::min(std::min(3.0f, Global.fDistanceFactor + 0.05f), Global.gfx_distance_factor_max); + } + } + + if (Global.UpdateMaterials) + { + // update resources if there was environmental change + simulation_state simulationstate{Global.Weather, Global.Season}; + std::swap(m_simulationstate, simulationstate); + if ((m_simulationstate.season != simulationstate.season) && (false == simulationstate.season.empty())) + { + m_materials.on_season_change(); + } + if ((m_simulationstate.weather != simulationstate.weather) && (false == simulationstate.weather.empty())) + { + m_materials.on_weather_change(); + } + } + + if (m_context && m_context->textures && (true == Global.ResourceSweep) && (true == simulation::is_ready)) + { + // garbage collection + m_geometry.update(); + m_context->textures->update(); + } + if ((Global.gfx_texture_releasedistance > 0.f) && (true == simulation::is_ready)) + { + update_texture_release(); + } update_info(); } @@ -781,15 +524,6 @@ void vulkan14_renderer::update_info() } } -void vulkan14_renderer::Update_Pick_Control() {} - -void vulkan14_renderer::Update_Pick_Node() {} - -glm::dvec3 vulkan14_renderer::Update_Mouse_Position() -{ - return glm::dvec3(0.0); -} - bool vulkan14_renderer::Debug_Ui_State(std::optional State) { if (State) @@ -813,8 +547,3 @@ imgui_renderer *vulkan14_renderer::GetImguiRenderer() { return (m_context ? &m_context->imgui : nullptr); } - -void vulkan14_renderer::MakeScreenshot() -{ - vk14::log_warning("screenshots aren't made by the vulkan renderer yet (D6)"); -} diff --git a/rendering/vulkan14/vulkan14renderer.h b/rendering/vulkan14/vulkan14renderer.h index 4ef2a240..263179fd 100644 --- a/rendering/vulkan14/vulkan14renderer.h +++ b/rendering/vulkan14/vulkan14renderer.h @@ -7,11 +7,12 @@ obtain one at http://mozilla.org/MPL/2.0/. */ -// the vulkan 1.4 renderer, picked with gfxrenderer vulkan. so far the core of the renderer (the track R of the plan, -// claude/vulkan14-renderer-plan.md): the gpu, the windows with their swapchains, the frames in flight, the memory and the -// uploads, the shaders and the pipelines, the pushed descriptors, the passes with their barriers and gpu times, the textures -// and the user interface (imgui) with the launcher. the scenery and the materials come in the later stages of the plan; until -// then those methods keep the data the simulation reads back and draw nothing. +// the vulkan 1.4 renderer, picked with gfxrenderer vulkan (claude/vulkan14-renderer-plan.md): the gpu, the windows with their +// swapchains, the frames in flight, the memory and the uploads, the shaders and the pipelines, the pushed descriptors, the passes +// with their barriers and gpu times, the textures and the user interface (imgui) with the launcher (the track R of the plan), and +// the scene (the track D): the geometry, the materials, the passes of the colour of the scene with the sun shadows, the +// environment map and the post-processing, the picking, the screenshots, the extra viewports and the light clusters. the scene +// is walked by the walk shared with the opengl renderer (gfx::scene_walk), whose draws this renderer records. // // the vulkan headers stay in the files of rendering/vulkan14: the objects of the api live in vulkan14_renderer::context @@ -19,8 +20,24 @@ http://mozilla.org/MPL/2.0/. #include "rendering/renderer.h" #include "rendering/geometrybank.h" +#include "rendering/light.h" +#include "rendering/lightarray.h" +#include "rendering/common/scenewalk.h" +#include "rendering/common/locallights.h" +#include "rendering/particles.h" +#include "model/material.h" +#include "vehicle/Camera.h" +#include "gl/ubo.h" -class vulkan14_renderer : public gfx_renderer +class world_environment; +namespace vk14 +{ +struct scene_program; +struct target_formats; +enum class blend_mode; +} // namespace vk14 + +class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk { public: vulkan14_renderer(); @@ -38,16 +55,23 @@ class vulkan14_renderer : public gfx_renderer gfx::geometry_handle Insert(gfx::index_array &Indices, gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Geometry, int const Type) override; gfx::geometry_handle Insert(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Geometry, int const Type) override; bool Replace(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type, std::size_t const Offset = 0) override; + bool Replace_Indexed(gfx::index_array &Indices, gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry) override; bool Append(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type) override; gfx::index_array const &Indices(gfx::geometry_handle const &Geometry) const override; gfx::vertex_array const &Vertices(gfx::geometry_handle const &Geometry) const override; gfx::userdata_array const &UserData(gfx::geometry_handle const &Geometry) const override; + int Primitive(gfx::geometry_handle const &Geometry) const override; // materials material_handle Fetch_Material(std::string const &Filename, bool const Loadnow = true) override; + material_handle Matte_Material(material_handle const Material) override; + material_handle Tinted_Material(material_handle const Material, glm::vec3 const &Colour) override; + material_handle Blend_Material(material_handle const Material, std::array const &Others) override; + 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; // shaders std::shared_ptr Fetch_Shader(std::string const &name) override; + std::string Shader_Name(std::shared_ptr const &Program) const override; // textures texture_handle Fetch_Texture(std::string const &Filename, bool const Loadnow = true, GLint format_hint = GL_SRGB_ALPHA) override; void Bind_Texture(texture_handle const Texture) override; @@ -60,6 +84,10 @@ class vulkan14_renderer : public gfx_renderer TSubModel const *Pick_Control() const override; scene::basic_node const *Pick_Node() const override; glm::dvec3 Mouse_Position() const override; + bool Mouse_Hit() const override; + glm::mat4 Camera_View_Matrix() const override; + glm::mat4 Camera_Projection_Matrix() const override; + glm::dvec3 Camera_Position() const override; // maintenance void Update(double const Deltatime) override; void Update_Pick_Control() override; @@ -72,27 +100,203 @@ class vulkan14_renderer : public gfx_renderer // ui imgui_renderer *GetImguiRenderer() override; void MakeScreenshot() override; + bool Render_Preview(TAnimModel *Instance, int const Size, int const Margin, bool const Shadows, std::vector &Image) override; static std::unique_ptr create_func(); // how the application makes the windows of the renderer, and whether it can run on the machine static gfx_backend_desc backend_desc(); private: - // the objects of the vulkan api (vulkan14renderer.cpp) + // the objects of the vulkan api (vulkan14context.h) struct context; - void update_info(); - // the device was lost: what was on the gpu goes to the log - void report_device_lost(char const *Where); + // a viewport the scene is drawn to: the main window, an extra viewport (a window of its own or the main window drawn with + // another projection) + struct viewport_config + { + int width{0}; + int height{0}; + float draw_range{1.f}; + bool main{false}; + bool shadow{false}; // its view is taken into the sun shadows + std::size_t window{0}; // the swapchain it goes to (vulkan14_renderer::context::windows) + enum vp_type + { + normal, + custom + } proj_type{normal}; + viewport_proj_config projection{}; + }; + // the sun of the frame, with the factor of the place drawn (tunnels) + struct sun_light : public basic_light + { + float factor{1.f}; + }; + + using gfx::scene_walk::Render; + using gfx::scene_walk::Render_Alpha; + + // the draws of the shared walk of the scene, recorded + void walk_material(material_handle const Material, TSubModel const *Submodel = nullptr, lighting_data const *Lighting = nullptr) override; + void walk_shadow_material(material_handle const Material) override; + opengl_material const &walk_material_data(material_handle const Material) const override; + opengl_material const &walk_material_data(TSubModel const *Submodel) const override; + void walk_program(walk_shader const Program) override; + void walk_texture(std::size_t const Unit, texture_handle const Texture) override; + void walk_blending(bool const Alpha) override; + void walk_sunlight(float const Factor = 1.f) override; + void walk_environment(TEnvironmentType const Environment = e_flat) override; + void walk_ambient_save() override; + void walk_ambient_add(glm::vec3 const &Addition) override; + void walk_ambient_restore() override; + glm::vec4 walk_sun_ambient() const override; + void walk_draw(gfx::geometry_handle const &Geometry) override; + void walk_draw(std::vector::iterator First, std::vector::iterator Last) override; + std::uint32_t walk_instances(glm::mat4 const *Modelviews, std::size_t const Count) override; + void walk_cull_face(bool const Enabled) override; + void walk_glare(bool const Enabled) override; + 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; + + // the scene (vulkan14scene.cpp) + bool init_scene(std::string &Reason); + // what the renderer prepares for the frames once it's there: the viewport of the main window, the common textures and geometry + void init_frame_data(); + void destroy_scene(); + // the scene of the frame: the passes of the viewports + void render_scene(); + void setup_pass(viewport_config &Viewport, renderpass_config &Config, rendermode const Mode, float const Znear = 0.f, float const Zfar = 1.f, bool const Ignoredebug = false); + void setup_matrices(); + // the colour of a viewport, with the passes it needs first: the sun shadows, the environment map, the light clusters + void render_viewport(std::size_t const Viewport); + void render_shadows(viewport_config &Viewport); + bool render_reflections(viewport_config &Viewport); + void render_reflection_face(viewport_config &Viewport); + void render_picking(rendermode const Mode); + // the sky dome and the sun and the moon + void render_environment(world_environment *Environment); + void render_particles(); + void render_precipitation(); + void render_editor_overlay(); + // the post-processing of a viewport: the motion blur, the tonemapping, the chromatic aberration + void render_post(std::size_t const Viewport, bool const Motionblur); + // the output of a viewport to its window, with the user interface over it for the main window + void present_viewport(std::size_t const Viewport); + // the light buffer of the pass: the sun, the ambient light, the fog, the headlights, the lamps (the clustered lights) + void update_lights(light_array &Lights, bool const Headlights = true, bool const Clustered = false); + void write_local_light(gl::light_element &Light, gfx::local_lights::light const &Source, glm::dvec3 const &Camera, glm::mat4 const &View); + void headlight_projector(light_array::light_record const &Light, glm::dvec3 &Position, glm::mat3 &Rotation, glm::mat4 &Projection, float &Near, float &Fieldofview); + // the light buffer and its header written for the draws which follow + void upload_light_header(); + void upload_lights(); + void upload_scene_block(); + void setup_sunlight_intensity(float const Factor = 1.f); + void setup_shadow_color(glm::vec4 const &Shadowcolor); + 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); + // the materials + material_handle invalid_material() const + { + return m_invalid_material; + } + // the draws: the pipeline of the program for the state of the walk, made when it's first needed + void draw_chunk(gfx::geometry_handle const &Geometry, bool const Newblock); + // a pass of the scene recorded with the walk: its targets, its size, its pass set (vk14::scene_state::pass_sets) + void start_draws(vk14::target_formats const &Targets, int const Width, int const Height, int const Set, bool const Shadows); + void end_draws(); + // the rendering of the colour of a viewport, begun with the images cleared or again where it was + void begin_colour_rendering(std::size_t const Viewport, bool const Clear); + void push_constants(void const *Constants); + // a pipeline of the post-processing (a VkPipeline), nullptr when the shader isn't there. Encode: the colour is encoded by the + // shader (OUTPUT_GAMMA), -1: as gfx.shadergamma says + void *post_pipeline(char const *Fragment, int const Format, int const Encode = -1); + // the screenshot copied with a frame the gpu is done with, saved + void update_screenshot(); + void bind_pass_set(); + 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(); + std::uint32_t push_draw_block(); + void open_instance_window(); + void update_texture_release(); std::unique_ptr m_context; - // the data of the geometry, which the simulation reads back; nothing on the gpu yet + // 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; + material_manager m_materials; + material_handle m_invalid_material{null_handle}; + std::vector m_viewports; + viewport_config *m_current_viewport{nullptr}; + renderpass_config m_colorpass; // of the latest colour pass + std::array m_shadowpass; // of the latest cascades of the sun shadows + sun_light m_sunlight; + glm::vec3 m_fogcolor{0.f}; + glm::vec4 m_shadowcolor{colors::shadow}; + gl::scene_ubs scene_ubs{}; + gl::light_data light_data{}; + std::uint32_t m_lightcount{1}; // lights written to light_data + // the copies of the light buffer in the ring of the frame since the lights were last written, by the header and the sun + std::vector, std::uint32_t>> m_lightheaders; + packed_vec3 m_cabambient{0.f}; + gfx::local_lights m_locallights; + std::uint32_t m_lights_lit{0}; + std::uint32_t m_lights_dropped{0}; + // the reflections: the face drawn, and the place and the time of the last update + int m_environmentcubetextureface{0}; + int m_environmentupdateface{6}; + bool m_environmentready{false}; + double m_environmentupdatetime{0.0}; + glm::dvec3 m_environmentupdatelocation{0.0}; + bool m_vehiclesmoved{true}; + texture_handle m_suntexture{-1}; + texture_handle m_moontexture{-1}; + texture_handle m_smoketexture{-1}; + texture_handle m_headlightstexture{-1}; + texture_handle m_precipitationtexture{-1}; + float m_precipitationovercast{-1.f}; + double m_precipitationrotation{0.0}; + // the editor overlay + gfx::geometrybank_handle m_editor_overlay_bank{}; + std::vector m_editor_overlay_geometry; + unsigned int m_editor_overlay_revision{0}; + unsigned int m_nodestateversion{0}; + // picking + std::vector> m_control_pick_requests; + std::vector> m_node_pick_requests; + TSubModel const *m_pickcontrolitem{nullptr}; + scene::basic_node *m_picksceneryitem{nullptr}; + bool m_pickcontrolswanted{false}; + bool m_picknodewanted{false}; + glm::ivec2 m_pickposition{0}; + glm::vec3 m_worldmousecoordinates{0.f}; + bool m_mousehit{false}; + // the state of the draws of the walk being recorded + struct draw_state; + std::unique_ptr m_draw; + // textures used by vehicle models, for the release of far textures + std::unordered_map> m_modeltextures; + std::vector const &Model_Textures(TModel3d *Model); + + // the season and the weather the materials were last given + struct simulation_state + { + std::string weather; + std::string season; + } m_simulationstate; + float m_framerate{0.f}; double m_updateaccumulator{0.0}; bool m_debug_ui_active{false}; std::string m_infotimes; std::string m_infostats; + debug_stats m_laststats; + + void update_info(); + // the device was lost: what was on the gpu goes to the log + void report_device_lost(char const *Where); static bool renderer_register; }; diff --git a/rendering/vulkan14/vulkan14scene.cpp b/rendering/vulkan14/vulkan14scene.cpp new file mode 100644 index 00000000..bb25001d --- /dev/null +++ b/rendering/vulkan14/vulkan14scene.cpp @@ -0,0 +1,1405 @@ +/* +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 scene of the vulkan renderer, part one (the stages D1 and D2 of the plan and the draws of the walk): the resources the +// passes of the scene share (the layouts, the pass sets, the rings of the draws, the stand-in images), the programs and their +// pipelines, the geometry, the materials and the textures, and the draws the shared walk of the scene (gfx::scene_walk) makes, +// recorded into the pass under way. the passes are in vulkan14frame.cpp + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14context.h" +#include "rendering/nullrenderer.h" +#include "model/Model3d.h" +#include "model/AnimModel.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/color.h" + +namespace +{ + +// the rings of the blocks of the draws and of the matrices of the instances, per frame in flight, and the windows of them the +// descriptors of the pass sets read +VkDeviceSize const draw_ring_size{8 * 1024 * 1024}; +VkDeviceSize const draw_window{4 * 1024 * 1024}; +VkDeviceSize const instance_ring_size{8 * 1024 * 1024}; +VkDeviceSize const instance_window{4 * 1024 * 1024}; + +// the topology the pipelines of the class of the primitive are made with (the primitive itself is dynamic state) +VkPrimitiveTopology topology_class(VkPrimitiveTopology const Topology) +{ + switch (Topology) + { + case VK_PRIMITIVE_TOPOLOGY_POINT_LIST: + return VK_PRIMITIVE_TOPOLOGY_POINT_LIST; + case VK_PRIMITIVE_TOPOLOGY_LINE_LIST: + case VK_PRIMITIVE_TOPOLOGY_LINE_STRIP: + return VK_PRIMITIVE_TOPOLOGY_LINE_LIST; + default: + return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + } +} + +vk14::vertex_layout layout_of(vk14::vertex_kind const Kind) +{ + vk14::vertex_layout layout; + switch (Kind) + { + case vk14::vertex_kind::scene: + layout.bindings = {{0, sizeof(gfx::basic_vertex), VK_VERTEX_INPUT_RATE_VERTEX}, {1, sizeof(gfx::vertex_userdata), VK_VERTEX_INPUT_RATE_VERTEX}}; + layout.attributes = {{0, 0, VK_FORMAT_R32G32B32_SFLOAT, static_cast(offsetof(gfx::basic_vertex, position))}, + {1, 0, VK_FORMAT_R32G32B32_SFLOAT, static_cast(offsetof(gfx::basic_vertex, normal))}, + {2, 0, VK_FORMAT_R32G32_SFLOAT, static_cast(offsetof(gfx::basic_vertex, texture))}, + {3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, static_cast(offsetof(gfx::basic_vertex, tangent))}, + {4, 1, VK_FORMAT_R32G32B32A32_SFLOAT, static_cast(offsetof(gfx::vertex_userdata, data))}}; + break; + case vk14::vertex_kind::colour: + 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::particle: + // position, colour, texture coordinates (opengl46_particles::particle_vertex) + layout.bindings = {{0, 36, VK_VERTEX_INPUT_RATE_VERTEX}}; + layout.attributes = {{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, {1, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 12}, {2, 0, VK_FORMAT_R32G32_SFLOAT, 28}}; + break; + case vk14::vertex_kind::position_uv: + 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::none: + default: + break; + } + return layout; +} + +// the locations of the inputs a vertex shader reads (the decorations of its variables of the input class): the attributes it +// doesn't read are left out of its pipelines, which the validation layer reports otherwise +std::vector vertex_input_locations(std::vector const &Spirv) +{ + std::uint32_t constexpr op_decorate{71}; + std::uint32_t constexpr op_variable{59}; + std::uint32_t constexpr decoration_location{30}; + std::uint32_t constexpr storage_input{1}; + std::unordered_map locations; + std::vector inputs; + 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; + } + if ((opcode == op_decorate) && (count >= 4) && (Spirv[word + 2] == decoration_location)) + { + locations[Spirv[word + 1]] = Spirv[word + 3]; + } + else if ((opcode == op_variable) && (count >= 4) && (Spirv[word + 3] == storage_input)) + { + inputs.push_back(Spirv[word + 2]); + } + word += count; + } + std::vector result; + for (auto const input : inputs) + { + auto const lookup{locations.find(input)}; + if (lookup != locations.end()) + { + result.push_back(lookup->second); + } + } + return result; +} + +// 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) +{ + VkImageViewCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + info.image = Image.handle(); + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.format = Image.desc().format; + info.subresourceRange = {Image.aspect(), 0, 1, Layer, 1}; + 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; +} + +} // namespace + +// the resources of the scene + +bool vulkan14_renderer::init_scene(std::string &Reason) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto const device{ctx.device.handle()}; + 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 + { + 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}}; + VkDescriptorSetLayoutCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + info.bindingCount = static_cast(bindings.size()); + info.pBindings = bindings.data(); + if (!VK14_VERIFY(vkCreateDescriptorSetLayout(device, &info, nullptr, &scene.pass_layout))) + { + Reason = "no layout of the pass set"; + return false; + } + vk14::name_object(device, VK_OBJECT_TYPE_DESCRIPTOR_SET_LAYOUT, vk14::handle_value(scene.pass_layout), "pass set"); + } + // the textures of the materials, pushed with the draws + { + std::vector bindings; + for (std::uint32_t unit = 0; unit < gl::MAX_TEXTURES; ++unit) + { + bindings.push_back({MATERIAL_TEXTURE_FIRST + unit, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}); + } + if (false == scene.material_set.create(device, bindings, MATERIAL_SET, VK_PIPELINE_BIND_POINT_GRAPHICS, sizeof(VkDescriptorImageInfo), "material textures")) + { + Reason = "no layout of the textures of the materials"; + return false; + } + } + VkPushConstantRange const graphicsconstants{VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, PUSH_CONSTANTS_SIZE}; + scene.scene_layout = vk14::make_pipeline_layout(device, {scene.pass_layout, scene.material_set.layout}, {graphicsconstants}, "scene"); + if ((scene.scene_layout == VK_NULL_HANDLE) || (false == scene.material_set.create_template(device, scene.scene_layout, "material textures"))) + { + Reason = "no layout of the pipelines of the scene"; + return false; + } + // the post-processing: the images it reads, pushed + { + std::vector bindings; + for (std::uint32_t image = 0; image < 4; ++image) + { + bindings.push_back({POST_TEXTURE_FIRST + image, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 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"; + return false; + } + scene.post_layout = vk14::make_pipeline_layout(device, {scene.post_set.layout}, {graphicsconstants}, "post-processing"); + if ((scene.post_layout == VK_NULL_HANDLE) || (false == scene.post_set.create_template(device, scene.post_layout, "post-processing images"))) + { + Reason = "no layout of the pipelines of the post-processing"; + return false; + } + } + // the compute passes of the clusters: their buffers, pushed + { + auto const stage{VK_SHADER_STAGE_COMPUTE_BIT}; + std::vector bindings{{CL_CLUSTER_UBO, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, stage, nullptr}, {CL_AABB_SSBO, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, stage, nullptr}, + {CL_RANGE_SSBO, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, stage, nullptr}, {CL_INDEX_SSBO, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, stage, nullptr}, + {CL_COUNTER_SSBO, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, stage, nullptr}, {CL_LIGHT_SSBO, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, stage, nullptr}}; + static_assert(sizeof(VkDescriptorBufferInfo) == sizeof(VkDescriptorImageInfo), "the templates are laid out by one stride"); + if (false == scene.cluster_set.create(device, bindings, 0, VK_PIPELINE_BIND_POINT_COMPUTE, sizeof(VkDescriptorBufferInfo), "cluster buffers")) + { + Reason = "no layout of the light clusters"; + return false; + } + VkPushConstantRange const computeconstants{VK_SHADER_STAGE_COMPUTE_BIT, 0, PUSH_CONSTANTS_SIZE}; + scene.cluster_layout = vk14::make_pipeline_layout(device, {scene.cluster_set.layout}, {computeconstants}, "light clusters"); + if ((scene.cluster_layout == VK_NULL_HANDLE) || (false == scene.cluster_set.create_template(device, scene.cluster_layout, "cluster buffers"))) + { + Reason = "no layout of the compute passes of the light clusters"; + return false; + } + } + + // the rings of the draws + auto const storagealignment{ctx.device.caps().limits.minStorageBufferOffsetAlignment}; + scene.draws.window = draw_window; + scene.draws.stride = sizeof(gl::model_ubs); + scene.instances.window = instance_window; + scene.instances.stride = sizeof(glm::mat4); + if ((false == scene.draws.ring.create(ctx.memory, draw_ring_size, frames, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, "draw blocks", draw_window)) || + (false == scene.instances.ring.create(ctx.memory, instance_ring_size, frames, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, "instance matrices", instance_window))) + { + Reason = "no rings for the draws"; + return false; + } + (void)storagealignment; + + // the stand-in images and the maps + 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_R8G8B8A8_UNORM; + desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (false == make_image(scene.white, desc, "white")) + { + return false; + } + std::uint32_t const white{0xffffffff}; + ctx.uploads.upload(scene.white, &white, sizeof(white), {{0, {1, 1, 1}}}, false); + } + { + vk14::image_desc desc; + desc.format = VK_FORMAT_D32_SFLOAT; + desc.view_type = VK_IMAGE_VIEW_TYPE_2D_ARRAY; + desc.layers = gl::MAX_CASCADES; + desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (false == make_image(scene.shadow_empty, desc, "empty shadow map")) + { + return false; + } + if (Global.gfx_shadowmap_enabled) + { + scene.shadow_size = static_cast(std::clamp(Global.shadowtune.map_size, 64u, ctx.device.caps().limits.maxImageDimension2D)); + desc.extent = {scene.shadow_size, scene.shadow_size, 1}; + desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + if (false == make_image(scene.shadow_map, desc, "shadow map")) + { + return false; + } + for (std::uint32_t layer = 0; layer < gl::MAX_CASCADES; ++layer) + { + scene.shadow_layers.push_back(layer_view(device, *scene.shadow_map, layer, "shadow cascade " + std::to_string(layer))); + } + vk14::log_info("shadow map " + std::to_string(scene.shadow_size) + "p"); + } + } + { + 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 = {16, 16, 1}; + desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (false == make_image(scene.env_empty, desc, "empty environment map")) + { + return false; + } + if (Global.gfx_envmap_enabled) + { + auto const size{static_cast(gl::ENVMAP_SIZE)}; + desc.extent = {size, size, 1}; + desc.mips = static_cast(std::floor(std::log2(static_cast(size)))) + 1; + 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; + if (false == make_image(scene.env_map, desc, "environment map")) + { + return false; + } + for (std::uint32_t face = 0; face < 6; ++face) + { + scene.env_faces[face] = layer_view(device, *scene.env_map, face, "environment face " + std::to_string(face)); + } + vk14::image_desc depthdesc; + depthdesc.format = VK_FORMAT_D32_SFLOAT; + depthdesc.extent = {size, size, 1}; + depthdesc.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + if (false == make_image(scene.env_depth, depthdesc, "environment map depth")) + { + return false; + } + } + } + { + // the picking of the controls and of the scenery + vk14::image_desc desc; + desc.format = VK_FORMAT_R8G8B8A8_UNORM; + desc.extent = {1024, 1024, 1}; + desc.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + if (false == make_image(scene.pick_colour, desc, "pick colour")) + { + return false; + } + desc.format = VK_FORMAT_D32_SFLOAT; + desc.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + if (false == make_image(scene.pick_depth, desc, "pick depth")) + { + return false; + } + } + // 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.reflection_formats.colour[0] = VK_FORMAT_R16G16B16A16_SFLOAT; + scene.reflection_formats.colours = 1; + scene.reflection_formats.depth = VK_FORMAT_D32_SFLOAT; + scene.reflection_formats.id = ++scene.formats_count; + scene.pick_formats.colour[0] = VK_FORMAT_R8G8B8A8_UNORM; + scene.pick_formats.colours = 1; + scene.pick_formats.depth = VK_FORMAT_D32_SFLOAT; + scene.pick_formats.id = ++scene.formats_count; + + // the light clusters of the main viewport + { + auto &clusters{scene.clusters}; + auto const usage{VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT}; + 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"))) + { + Reason = "no buffers of the light clusters"; + return false; + } + } + + // the occlusion queries of the lamps + for (std::uint32_t frame = 0; frame < frames; ++frame) + { + VkQueryPoolCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + info.queryType = VK_QUERY_TYPE_OCCLUSION; + info.queryCount = vk14::scene_state::occlusion_queries; + VkQueryPool pool{VK_NULL_HANDLE}; + if (!VK14_VERIFY(vkCreateQueryPool(device, &info, nullptr, &pool))) + { + Reason = "no occlusion queries"; + return false; + } + vkResetQueryPool(device, pool, 0, vk14::scene_state::occlusion_queries); + scene.occlusion_pools.push_back(pool); + } + scene.occlusion_submodels.resize(frames); + + // the programs of the renderer + try + { + 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; + }; + scene.shadow = program("simpleuv.vert", "shadowmap.frag", vk14::vertex_kind::scene); + scene.alpha_shadow = program("simpleuv.vert", "alphashadowmap.frag", vk14::vertex_kind::scene); + scene.pick = program("vertexonly.vert", "pick.frag", vk14::vertex_kind::scene); + scene.pick_surface = program("simpleuv.vert", "pick_surface.frag", vk14::vertex_kind::scene); + scene.line = program("traction.vert", "traction.frag", vk14::vertex_kind::scene); + scene.billboard = program("simpleuv.vert", "billboard.frag", vk14::vertex_kind::scene); + scene.freespot = program("freespot.vert", "freespot.frag", vk14::vertex_kind::scene); + scene.celestial = program("celestial.vert", "celestial.frag", vk14::vertex_kind::none); + 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.quad = ctx.shaders.get("quad.vert"); + for (auto const *name : {"copy.frag", "present.frag", "postfx_tonemapping.frag", "postfx_motionblur.frag", "postfx_chromaticaberration.frag"}) + { + scene.post_fragments.emplace(name, ctx.shaders.get(name)); + } + auto const build{ctx.shaders.get("cluster_build.comp")}; + auto const cull{ctx.shaders.get("cluster_cull.comp")}; + scene.cluster_build = ctx.pipelines.compute(build, scene.cluster_layout, {}, "cluster build"); + scene.cluster_cull = ctx.pipelines.compute(cull, scene.cluster_layout, {}, "cluster cull"); + } + catch (gfx::shader_exception const &e) + { + Reason = std::string("invalid shader: ") + e.what(); + return false; + } + + m_draw = std::make_unique(); + m_draw->textures.fill(null_handle); + return true; +} + +void vulkan14_renderer::destroy_scene() +{ + if (!m_context) + { + return; + } + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto const device{ctx.device.handle()}; + if (device == VK_NULL_HANDLE) + { + return; + } + for (auto *pool : scene.occlusion_pools) + { + vkDestroyQueryPool(device, pool, nullptr); + } + scene.occlusion_pools.clear(); + for (auto *view : scene.shadow_layers) + { + if (view != VK_NULL_HANDLE) + { + vkDestroyImageView(device, view, nullptr); + } + } + scene.shadow_layers.clear(); + for (auto &view : scene.env_faces) + { + if (view != VK_NULL_HANDLE) + { + vkDestroyImageView(device, view, nullptr); + view = VK_NULL_HANDLE; + } + } + if (scene.pass_pool != VK_NULL_HANDLE) + { + vkDestroyDescriptorPool(device, scene.pass_pool, nullptr); + scene.pass_pool = VK_NULL_HANDLE; + } + scene.material_set.destroy(device); + scene.post_set.destroy(device); + scene.cluster_set.destroy(device); + for (auto *layout : {&scene.scene_layout, &scene.post_layout, &scene.cluster_layout}) + { + if (*layout != VK_NULL_HANDLE) + { + vkDestroyPipelineLayout(device, *layout, nullptr); + *layout = VK_NULL_HANDLE; + } + } + if (scene.pass_layout != VK_NULL_HANDLE) + { + vkDestroyDescriptorSetLayout(device, scene.pass_layout, nullptr); + scene.pass_layout = VK_NULL_HANDLE; + } + // the pipelines belong to the pipeline library + scene.materials.clear(); + scene.targets.clear(); + scene.white.reset(); + scene.shadow_empty.reset(); + scene.shadow_map.reset(); + scene.env_empty.reset(); + scene.env_map.reset(); + scene.env_depth.reset(); + scene.pick_colour.reset(); + scene.pick_depth.reset(); + scene.pick_controls = {}; + scene.pick_scenery = {}; + scene.cursor = {}; + scene.screenshot.reset(); + scene.clusters.aabbs.destroy(); + scene.clusters.ranges.destroy(); + scene.clusters.indices.destroy(); + scene.clusters.counters.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() +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &draw{*m_draw}; + auto const device{ctx.device.handle()}; + using namespace vk14::bindings; + + // the beams of the headlights, once their texture is there + auto headlights{scene.white->view()}; + auto headlightssampler{ctx.sampling.clamp_aniso}; + if (m_headlightstexture != null_handle) + { + auto &texture{ctx.textures->mark_as_used(m_headlightstexture)}; + texture.create(); + if (auto const *entry{ctx.images.find(texture.get_id())}) + { + headlights = entry->target->view(); + headlightssampler = entry->sampler; + } + } + if (headlights != scene.headlights_view) + { + scene.pass_sets_stale = true; + } + if (scene.pass_sets_stale) + { + 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); + } + if (false == draw.rendering) + { + return; + } + // 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}; + 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.descriptorSetCount = 1; + info.pDescriptorSets = &scene.pass_sets[draw.set]; + info.dynamicOffsetCount = static_cast(offsets.size()); + info.pDynamicOffsets = offsets.data(); + vkCmdBindDescriptorSets2(draw.commands, &info); + draw.set_dirty = false; +} + +// geometry + +gfx::geometrybank_handle vulkan14_renderer::Create_Bank() +{ + if (!m_context) + { + return m_geometry.register_bank(std::make_unique()); + } + auto bank{std::make_unique(m_context->resources)}; + auto *raw{bank.get()}; + auto const handle{m_geometry.register_bank(std::move(bank))}; + if (m_banks.size() < handle.bank) + { + m_banks.resize(handle.bank, nullptr); + } + m_banks[handle.bank - 1] = raw; + return handle; +} + +gfx::geometry_handle vulkan14_renderer::Insert(gfx::index_array &Indices, gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Geometry, int const Type) +{ + // NOTE: we expect indexed geometry to come with calculated tangents + return m_geometry.create_chunk(Indices, Vertices, Userdata, Geometry, Type); +} + +gfx::geometry_handle vulkan14_renderer::Insert(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Geometry, int const Type) +{ + gfx::calculate_tangents(Vertices, gfx::index_array(), Type); + return m_geometry.create_chunk(Vertices, Userdata, Geometry, Type); +} + +bool vulkan14_renderer::Replace(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type, std::size_t const Offset) +{ + gfx::calculate_tangents(Vertices, gfx::index_array(), Type); + return m_geometry.replace(Vertices, Userdata, Geometry, Offset); +} + +bool vulkan14_renderer::Replace_Indexed(gfx::index_array &Indices, gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry) +{ + // NOTE: we expect indexed geometry to come with calculated tangents + return m_geometry.replace(Indices, Vertices, Userdata, Geometry); +} + +bool vulkan14_renderer::Append(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type) +{ + gfx::calculate_tangents(Vertices, gfx::index_array(), Type); + return m_geometry.append(Vertices, Userdata, Geometry); +} + +gfx::index_array const &vulkan14_renderer::Indices(gfx::geometry_handle const &Geometry) const +{ + return m_geometry.indices(Geometry); +} + +gfx::vertex_array const &vulkan14_renderer::Vertices(gfx::geometry_handle const &Geometry) const +{ + return m_geometry.vertices(Geometry); +} + +gfx::userdata_array const &vulkan14_renderer::UserData(gfx::geometry_handle const &Geometry) const +{ + return m_geometry.userdata(Geometry); +} + +int vulkan14_renderer::Primitive(gfx::geometry_handle const &Geometry) const +{ + return static_cast(m_geometry.type(Geometry)); +} + +// materials and their programs: the materials are read as for the opengl renderer, their programs are made of vertex.vert and +// mat_.frag of shaders/vk14 + +material_handle vulkan14_renderer::Fetch_Material(std::string const &Filename, bool const Loadnow) +{ + return m_materials.create(Filename, Loadnow); +} + +material_handle vulkan14_renderer::Matte_Material(material_handle const Material) +{ + return m_materials.create_matte(Material); +} + +material_handle vulkan14_renderer::Tinted_Material(material_handle const Material, glm::vec3 const &Colour) +{ + return m_materials.create_tinted(Material, Colour); +} + +material_handle vulkan14_renderer::Blend_Material(material_handle const Material, std::array const &Others) +{ + return m_materials.create_blend(Material, Others); +} + +bool vulkan14_renderer::Blendable_Material(material_handle const Material) +{ + return m_materials.blendable(Material); +} + +IMaterial const *vulkan14_renderer::Material(material_handle const Material) const +{ + return &m_materials.material(Material); +} + +std::shared_ptr vulkan14_renderer::Fetch_Shader(std::string const &name) +{ + if (!m_context) + { + return nullptr; + } + auto &materials{m_context->scene.materials}; + auto const lookup{materials.find(name)}; + if (lookup != materials.end()) + { + return lookup->second; + } + // throws gfx::shader_exception, which the materials catch + auto program{std::make_shared()}; + program->vertex = m_context->shaders.get("vertex.vert"); + program->fragment = m_context->shaders.get("mat_" + name + ".frag"); + program->fragment_name = "mat_" + name + ".frag"; + program->vertices = vk14::vertex_kind::scene; + program->name = "mat_" + name; + // the directives of the material, of both stages as the opengl program has them + for (auto const *stage : {program->vertex.get(), program->fragment.get()}) + { + program->texture_conf.insert(stage->source.texture_conf.begin(), stage->source.texture_conf.end()); + program->param_conf.insert(stage->source.param_conf.begin(), stage->source.param_conf.end()); + } + materials.emplace(name, program); + return program; +} + +std::string vulkan14_renderer::Shader_Name(std::shared_ptr const &Program) const +{ + if (m_context) + { + for (auto const &program : m_context->scene.materials) + { + if (program.second == Program) + { + return program.first; + } + } + } + return {}; +} + +// the params of the material for the shaders of the scene in model_ubs, as opengl46_renderer::Bind_Material() sets them +void vulkan14_renderer::Bind_Material(material_handle const Material, TSubModel const *sm, lighting_data const *lighting) +{ + walk_material(Material, sm, lighting); +} + +void vulkan14_renderer::walk_material(material_handle const Material, TSubModel const *Submodel, lighting_data const *Lighting) +{ + if (!m_draw) + { + return; + } + if (Material == null_handle) + { + if (Material != m_invalid_material) + { + walk_material(m_invalid_material); + } + return; + } + auto const &material{m_materials.material(Material)}; + if (false == material.is_good) + { + // use failure indicator instead + if (Material != m_invalid_material) + { + walk_material(m_invalid_material); + } + return; + } + + std::memcpy(&model_ubs.param[0], &material.params[0], sizeof(model_ubs.param)); + for (auto const &entry : material.params_state) + { + glm::vec4 src(1.0f); + // submodel-based parameters + if (Submodel) + { + switch (entry.defaultparam) + { + case gfx::shader_param_default::ambient: + src = Submodel->f4Ambient; + break; + case gfx::shader_param_default::diffuse: + src = Submodel->f4Diffuse; + break; + case gfx::shader_param_default::specular: + src = Submodel->f4Specular; + break; + default: + break; + } + } + if (Lighting) + { + switch (entry.defaultparam) + { + case gfx::shader_param_default::ambient: + src = Lighting->ambient; + break; + case gfx::shader_param_default::diffuse: + src = Lighting->diffuse; + break; + case gfx::shader_param_default::specular: + src = Lighting->specular; + break; + default: + break; + } + } + // material-based parameters + switch (entry.defaultparam) + { + case gfx::shader_param_default::glossiness: + src = glm::vec4{material.glossiness}; + break; + default: + break; + } + if (entry.size == 1) + { + // HACK: convert color to luminosity, if it's passed as single value + src = glm::vec4{colors::RGBtoHSV(glm::vec3{src}).z}; + } + for (size_t j = 0; j < entry.size; j++) + { + model_ubs.param[entry.location][entry.offset + j] = src[j]; + } + } + if (material.opacity) + { + model_ubs.opacity = (m_draw->blending ? -material.opacity.value() : material.opacity.value()); + } + else + { + model_ubs.opacity = (m_draw->blending ? 0.0f : 0.5f); + } + model_ubs.alpha_mult = (Submodel ? Submodel->fVisible : 1.0f); + + // 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}; + for (std::size_t unit = 0; unit < gl::MAX_TEXTURES; ++unit) + { + if (draw.textures[unit] != materialtextures[unit]) + { + draw.textures[unit] = materialtextures[unit]; + draw.textures_dirty = true; + } + } + draw.program = static_cast(material.shader.get()); +} + +void vulkan14_renderer::walk_shadow_material(material_handle const Material) +{ + auto &draw{*m_draw}; + auto &scene{m_context->scene}; + 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(); + } + else + { + draw.program = scene.shadow.get(); + } +} + +opengl_material const &vulkan14_renderer::walk_material_data(material_handle const Material) const +{ + return m_materials.material(Material); +} + +opengl_material const &vulkan14_renderer::walk_material_data(TSubModel const *Submodel) const +{ + return m_materials.material(submodel_material(Submodel)); +} + +void vulkan14_renderer::walk_program(walk_shader const Program) +{ + auto &scene{m_context->scene}; + switch (Program) + { + case walk_shader::pick: + m_draw->program = scene.pick.get(); + break; + case walk_shader::pick_surface: + m_draw->program = scene.pick_surface.get(); + break; + case walk_shader::line: + m_draw->program = scene.line.get(); + break; + case walk_shader::billboard: + m_draw->program = scene.billboard.get(); + break; + case walk_shader::freespot: + m_draw->program = scene.freespot.get(); + break; + } +} + +void vulkan14_renderer::walk_texture(std::size_t const Unit, texture_handle const Texture) +{ + if ((Unit < gl::MAX_TEXTURES) && (m_draw->textures[Unit] != Texture)) + { + m_draw->textures[Unit] = Texture; + m_draw->textures_dirty = true; + } +} + +void vulkan14_renderer::walk_blending(bool const Alpha) +{ + // setup_drawing() of the opengl renderer: the blending and the depth written; the faces culled by the pass + m_draw->blending = Alpha; +} + +void vulkan14_renderer::walk_cull_face(bool const Enabled) +{ + m_draw->cullface = Enabled; +} + +void vulkan14_renderer::walk_glare(bool const Enabled) +{ + // without the depth test, added to what is there + m_draw->glare = Enabled; +} + +void vulkan14_renderer::walk_glare_draw(TSubModel *Submodel, gfx::geometry_handle const &Geometry) +{ + // the point of the light wasn't seen by the occlusion query of the last frame done by the gpu + auto const &visible{m_context->scene.glare_visible}; + auto const lookup{visible.find(Submodel)}; + if ((lookup != visible.end()) && (false == lookup->second)) + { + return; + } + walk_draw(Geometry); +} + +void vulkan14_renderer::walk_light_query(TSubModel *Submodel, bool const Begin) +{ + auto &draw{*m_draw}; + if (false == draw.rendering) + { + return; + } + auto &scene{m_context->scene}; + auto &submodels{scene.occlusion_submodels[m_context->slot]}; + if (Begin) + { + if (submodels.size() >= vk14::scene_state::occlusion_queries) + { + draw.query = ~0u; + return; + } + draw.query = static_cast(submodels.size()); + submodels.push_back(Submodel); + vkCmdBeginQuery(draw.commands, 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.query = ~0u; + } +} + +// the light of the sun and the ambient light, for the draws which follow + +void vulkan14_renderer::walk_sunlight(float const Factor) +{ + setup_sunlight_intensity(Factor); +} + +void vulkan14_renderer::walk_environment(TEnvironmentType const Environment) +{ + switch (Environment) + { + case e_flat: + setup_sunlight_intensity(); + break; + case e_canyon: + setup_sunlight_intensity(0.4f); + break; + case e_tunnel: + setup_sunlight_intensity(0.2f); + break; + default: + break; + } +} + +void vulkan14_renderer::walk_ambient_save() +{ + m_cabambient = light_data.ambient; +} + +void vulkan14_renderer::walk_ambient_add(glm::vec3 const &Addition) +{ + light_data.ambient += packed_vec3(Addition); + upload_light_header(); +} + +void vulkan14_renderer::walk_ambient_restore() +{ + light_data.ambient = m_cabambient; +} + +glm::vec4 vulkan14_renderer::walk_sun_ambient() const +{ + return m_sunlight.ambient; +} + +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; + upload_light_header(); +} + +void vulkan14_renderer::setup_shadow_color(glm::vec4 const &Shadowcolor) +{ + model_ubs.shadow_tone = glm::pow(Shadowcolor.x, 2.2f); +} + +// the draws + +std::uint32_t vulkan14_renderer::push_draw_block() +{ + auto &scene{m_context->scene}; + auto &draws{scene.draws}; + auto const slice{draws.ring.allocate(draws.stride, draws.stride)}; + if (false == bool(slice)) + { + return ~0u; + } + if ((false == draws.open) || (slice.offset + draws.stride > draws.base + draws.window)) + { + // a new window of the blocks, the pass set is bound with its offset + draws.base = slice.offset; + draws.open = true; + m_draw->set_dirty = true; + } + std::memcpy(slice.data, &model_ubs, sizeof(gl::model_ubs)); + return static_cast((slice.offset - draws.base) / draws.stride); +} + +std::uint32_t vulkan14_renderer::walk_instances(glm::mat4 const *Modelviews, std::size_t const Count) +{ + auto &scene{m_context->scene}; + auto &instances{scene.instances}; + auto const capacity{static_cast(instances.window / instances.stride) - 1}; + auto const count{std::min(Count, capacity)}; + auto const size{static_cast(count) * instances.stride}; + if ((false == instances.open) || (instances.ring.next_offset(instances.stride) + size > instances.base + instances.window)) + { + open_instance_window(); + } + auto const slice{instances.ring.allocate(size, instances.stride)}; + if ((false == instances.open) || (false == bool(slice)) || (slice.offset + size > instances.base + instances.window)) + { + // the ring of the frame is full: the instances are drawn where the draws which aren't instanced are + return 0; + } + std::memcpy(slice.data, Modelviews, static_cast(size)); + return static_cast((slice.offset - instances.base) / instances.stride); +} + +// a new window of the instance ring, which begins with the identity of the draws which aren't instanced +void vulkan14_renderer::open_instance_window() +{ + auto &instances{m_context->scene.instances}; + auto const alignment{m_context->device.caps().limits.minStorageBufferOffsetAlignment}; + auto const identity{instances.ring.allocate(instances.stride, std::max(alignment, instances.stride))}; + if (false == bool(identity)) + { + instances.open = false; + return; + } + glm::mat4 const one{1.f}; + std::memcpy(identity.data, &one, sizeof(one)); + instances.base = identity.offset; + instances.open = true; + if (m_draw) + { + m_draw->set_dirty = true; + } +} + +void vulkan14_renderer::walk_draw(gfx::geometry_handle const &Geometry) +{ + model_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW)); + draw_chunk(Geometry, true); +} + +void vulkan14_renderer::walk_draw(std::vector::iterator First, std::vector::iterator Last) +{ + model_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW)); + // the chunks drawn together share the block of the draw + auto newblock{true}; + for (; First != Last; ++First) + { + draw_chunk(*First, newblock); + newblock = false; + } +} + +void vulkan14_renderer::draw_chunk(gfx::geometry_handle const &Geometry, bool const Newblock) +{ + auto &draw{*m_draw}; + if ((false == draw.rendering) || (Geometry == null_handle) || (draw.program == nullptr) || (draw.program->vertices != vk14::vertex_kind::scene)) + { + return; + } + if ((Geometry.bank == 0) || (Geometry.bank > m_banks.size()) || (m_banks[Geometry.bank - 1] == nullptr)) + { + return; + } + // marks the bank as used, and has its data on the gpu + gfx::indirect_source source; + if (false == m_geometry.indirect_source(Geometry, source)) + { + return; + } + auto const &chunk{static_cast(m_banks[Geometry.bank - 1])->last()}; + + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto const commands{draw.commands}; + // the pipeline for the state of the walk + auto const blend{draw.glare ? vk14::blend_mode::scene_additive : (draw.blending ? vk14::blend_mode::scene_alpha : vk14::blend_mode::none)}; + if (false == bind_scene_pipeline(*draw.program, *draw.targets, blend, static_cast(chunk.topology))) + { + return; + } + apply_draw_state(); + if (Newblock || (draw.block == ~0u)) + { + draw.block = push_draw_block(); + if (draw.block == ~0u) + { + return; + } + } + if (draw.set_dirty) + { + bind_pass_set(); + } + if (draw.textures_dirty) + { + push_textures(); + } + if (draw.pushed_draw != draw.block) + { + vk14::draw_constants constants; + constants.draw = 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; + vkCmdPushConstants2(commands, &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); + draw.vertices = chunk.vertices; + draw.userdata = chunk.userdata; + } + auto const instances{m_current_instance_count > 0 ? static_cast(m_current_instance_count) : 1u}; + auto const firstinstance{m_current_instance_count > 0 ? m_current_instance_base : 0u}; + if (chunk.indices != VK_NULL_HANDLE) + { + if (chunk.indices != draw.indices) + { + vkCmdBindIndexBuffer(commands, chunk.indices, 0, VK_INDEX_TYPE_UINT32); + draw.indices = chunk.indices; + } + vkCmdDrawIndexed(commands, chunk.count, instances, chunk.first, chunk.basevertex, firstinstance); + } + else + { + vkCmdDraw(commands, chunk.count, instances, chunk.first, firstinstance); + } + ++draw.draws; +} + +// the pipeline of the program for the targets of the pass, the blending and the class of the primitive, made the first time it's +// asked for; bound with the primitive as dynamic state. false: it can't be made +bool vulkan14_renderer::bind_scene_pipeline(vk14::scene_program &Program, vk14::target_formats const &Targets, vk14::blend_mode const Blend, int const Topology) +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &draw{*m_draw}; + auto const topology{static_cast(Topology)}; + auto const primitiveclass{topology_class(topology)}; + // the settings of the game the shaders take as specialization constants + auto const motion{Targets.colours > 1}; + std::uint32_t const settings{(Global.gfx_shadergamma ? 1u : 0u) | (Global.gfx_shadowmap_enabled ? 2u : 0u) | (Global.gfx_envmap_enabled ? 4u : 0u) | (motion ? 8u : 0u) | 16u | (Global.gfx_extraeffects ? 32u : 0u)}; + auto const key{(static_cast(Targets.id)) | (static_cast(Blend) << 16) | (static_cast(primitiveclass) << 24) | (static_cast(settings) << 32)}; + VkPipeline pipeline{VK_NULL_HANDLE}; + auto const lookup{Program.pipelines.find(key)}; + if (lookup != Program.pipelines.end()) + { + pipeline = lookup->second; + } + else + { + auto fragment{Program.fragment}; + if ((false == motion) && (Targets.colours > 0)) + { + // a shader writing the velocity without its target is reported by the validation layer + if (!Program.fragment_single) + { + try + { + Program.fragment_single = ctx.shaders.get(Program.fragment_name, "#define NO_MOTION_OUTPUT\n"); + } + catch (gfx::shader_exception const &Exception) + { + vk14::log_error(Exception.what()); + } + } + if (Program.fragment_single) + { + fragment = Program.fragment_single; + } + } + vk14::pipeline_desc desc; + desc.stages = {Program.vertex, fragment}; + desc.layout = scene.scene_layout; + desc.vertices = layout_of(Program.vertices); + { + auto const read{vertex_input_locations(Program.vertex->spirv)}; + auto &attributes{desc.vertices.attributes}; + attributes.erase(std::remove_if(attributes.begin(), attributes.end(), [&](VkVertexInputAttributeDescription const &Attribute) { return std::find(read.begin(), read.end(), Attribute.location) == read.end(); }), + attributes.end()); + } + desc.topology = primitiveclass; + desc.blending = Blend; + desc.colour_formats.assign(Targets.colour.begin(), Targets.colour.begin() + Targets.colours); + desc.depth_format = Targets.depth; + desc.samples = Targets.samples; + 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); + Program.pipelines.emplace(key, pipeline); + } + if (pipeline == VK_NULL_HANDLE) + { + return false; + } + if (pipeline != draw.pipeline) + { + vkCmdBindPipeline(draw.commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + draw.pipeline = pipeline; + } + if (topology != draw.topology) + { + vkCmdSetPrimitiveTopology(draw.commands, topology); + draw.topology = topology; + } + return true; +} + +// the dynamic state of the draws for the state of the walk: the faces culled, the depth tested and written +void vulkan14_renderer::apply_draw_state() +{ + auto &draw{*m_draw}; + 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.cull = cull; + } + auto const depthtest{draw.glare ? 0 : 1}; + if (depthtest != draw.depthtest) + { + vkCmdSetDepthTestEnable(draw.commands, 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.depthwrite = depthwrite; + } +} + +// the textures of the draw pushed: the material's, the white stand-in where there is none or it isn't there yet +void vulkan14_renderer::push_textures() +{ + auto &ctx{*m_context}; + auto &scene{ctx.scene}; + auto &draw{*m_draw}; + std::array images; + for (std::size_t unit = 0; unit < gl::MAX_TEXTURES; ++unit) + { + images[unit] = VkDescriptorImageInfo{ctx.sampling.repeat_aniso, scene.white->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + auto const texture{draw.textures[unit]}; + if ((texture == null_handle) || (false == bool(ctx.textures))) + { + continue; + } + auto &source{ctx.textures->mark_as_used(texture)}; + if (false == source.create()) + { + continue; + } + if (auto const *entry{ctx.images.find(source.get_id())}) + { + images[unit] = VkDescriptorImageInfo{entry->sampler, entry->target->view(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + } + } + scene.material_set.push(draw.commands, scene.scene_layout, images.data()); + draw.textures_dirty = false; +} + +// 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) +{ + if (!m_context || !m_context->textures) + { + return null_handle; + } + return m_context->textures->create(Filename, Loadnow, format_hint); +} + +void vulkan14_renderer::Bind_Texture(texture_handle const Texture) +{ + Bind_Texture(0, Texture); +} + +void vulkan14_renderer::Bind_Texture(std::size_t const Unit, texture_handle const Texture) +{ + if (m_draw) + { + walk_texture(Unit, Texture); + } +} + +ITexture &vulkan14_renderer::Texture(texture_handle const Texture) +{ + if ((Texture == null_handle) || !m_context || !m_context->textures) + { + return *ITexture::null_texture(); + } + return m_context->textures->mark_as_used(Texture); +} + +ITexture const &vulkan14_renderer::Texture(texture_handle const Texture) const +{ + if ((Texture == null_handle) || !m_context || !m_context->textures) + { + return *ITexture::null_texture(); + } + return m_context->textures->texture(Texture); +} diff --git a/rendering/vulkan14/vulkan14shaders.cpp b/rendering/vulkan14/vulkan14shaders.cpp index 1b013c2d..db8424ce 100644 --- a/rendering/vulkan14/vulkan14shaders.cpp +++ b/rendering/vulkan14/vulkan14shaders.cpp @@ -10,6 +10,8 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/vulkan14/vulkan14shaders.h" #include "rendering/vulkan14/vulkan14device.h" +#include "rendering/vulkan14/vulkan14bindings.h" +#include "gl/ubo.h" #include "utilities/Globals.h" #include "utilities/Logs.h" #include "utilities/utilities.h" @@ -562,6 +564,24 @@ VkPipeline pipeline_library::get(pipeline_desc const &Desc) attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; attachment.alphaBlendOp = VK_BLEND_OP_ADD; break; + case blend_mode::scene_alpha: + attachment.blendEnable = VK_TRUE; + attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + attachment.colorBlendOp = VK_BLEND_OP_ADD; + attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + attachment.alphaBlendOp = VK_BLEND_OP_ADD; + break; + case blend_mode::scene_additive: + attachment.blendEnable = VK_TRUE; + attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE; + attachment.colorBlendOp = VK_BLEND_OP_ADD; + attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + attachment.alphaBlendOp = VK_BLEND_OP_ADD; + break; case blend_mode::additive: attachment.blendEnable = VK_TRUE; attachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; @@ -703,3 +723,60 @@ void set_draw_state(VkCommandBuffer Commands, VkExtent2D Extent) } } // namespace vk14 + +namespace vk14 +{ + +std::string binding_preamble() +{ + std::string preamble{"#define MASZYNA_CONSTANTS 1\n"}; + auto const define = [&preamble](char const *Name, auto const Value, char const *Suffix = "") { preamble += std::string("#define ") + Name + " " + std::to_string(Value) + Suffix + "\n"; }; + using namespace vk14::bindings; + define("UI_SET", UI_SET); + define("UI_TEXTURE", UI_TEXTURE); + define("PASS_SET", PASS_SET); + define("MATERIAL_SET", MATERIAL_SET); + define("SCENE_UBO", SCENE_UBO); + define("DRAW_SSBO", DRAW_SSBO); + define("INSTANCE_SSBO", INSTANCE_SSBO); + define("LIGHT_SSBO", LIGHT_SSBO); + define("CLUSTER_UBO", CLUSTER_UBO); + define("CLUSTER_RANGE_SSBO", CLUSTER_RANGE_SSBO); + define("CLUSTER_INDEX_SSBO", CLUSTER_INDEX_SSBO); + define("SHADOW_TEX", SHADOW_TEX); + define("ENV_TEX", ENV_TEX); + define("HEADLIGHT_TEX", HEADLIGHT_TEX); + define("PASS_BINDING_COUNT", PASS_BINDING_COUNT); + define("MATERIAL_TEXTURE_FIRST", MATERIAL_TEXTURE_FIRST); + define("CL_CLUSTER_UBO", CL_CLUSTER_UBO); + define("CL_AABB_SSBO", CL_AABB_SSBO); + define("CL_RANGE_SSBO", CL_RANGE_SSBO); + define("CL_INDEX_SSBO", CL_INDEX_SSBO); + define("CL_COUNTER_SSBO", CL_COUNTER_SSBO); + define("CL_LIGHT_SSBO", CL_LIGHT_SSBO); + define("POST_SET", POST_SET); + define("POST_TEXTURE_FIRST", POST_TEXTURE_FIRST); + define("PUSH_CONSTANTS_SIZE", PUSH_CONSTANTS_SIZE); + define("SPEC_OUTPUT_GAMMA", SPEC_OUTPUT_GAMMA); + define("SPEC_SHADOWMAP_ENABLED", SPEC_SHADOWMAP_ENABLED); + define("SPEC_ENVMAP_ENABLED", SPEC_ENVMAP_ENABLED); + define("SPEC_MOTIONBLUR_ENABLED", SPEC_MOTIONBLUR_ENABLED); + define("SPEC_POSTFX_ENABLED", SPEC_POSTFX_ENABLED); + define("SPEC_EXTRAEFFECTS_ENABLED", SPEC_EXTRAEFFECTS_ENABLED); + define("SPEC_COUNT", SPEC_COUNT); + // the constants of the game the blocks of the shaders are sized by, the same as for the opengl renderer (gl/ubo.h) + define("MAX_LIGHTS", static_cast(gl::MAX_LIGHTS), "U"); + define("MAX_CASCADES", static_cast(gl::MAX_CASCADES), "U"); + define("MAX_PARAMS", static_cast(gl::MAX_PARAMS), "U"); + define("MAX_HEADLIGHT_SHADOWS", static_cast(gl::MAX_HEADLIGHT_SHADOWS), "U"); + define("CLUSTER_GRID_X", gl::CLUSTER_GRID_X); + define("CLUSTER_GRID_Y", gl::CLUSTER_GRID_Y); + define("CLUSTER_GRID_Z", gl::CLUSTER_GRID_Z); + define("CLUSTER_COUNT", gl::CLUSTER_COUNT); + 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); + return preamble; +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14shaders.h b/rendering/vulkan14/vulkan14shaders.h index a0c358cd..29776f62 100644 --- a/rendering/vulkan14/vulkan14shaders.h +++ b/rendering/vulkan14/vulkan14shaders.h @@ -76,7 +76,10 @@ enum class blend_mode none, alpha, // the colour over what is there by its alpha, the alpha kept for what's behind (the user interface) premultiplied, - additive + 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) }; struct vertex_layout diff --git a/rendering/vulkan14/vulkan14texture.cpp b/rendering/vulkan14/vulkan14texture.cpp index d98cc48e..32e470e3 100644 --- a/rendering/vulkan14/vulkan14texture.cpp +++ b/rendering/vulkan14/vulkan14texture.cpp @@ -57,10 +57,59 @@ void uploader::begin_frame(VkCommandBuffer Commands, std::uint64_t Value) bool uploader::end_frame() { + if (m_buffercopies && (m_commands != VK_NULL_HANDLE)) + { + // the draws of the frame read what was copied + VkMemoryBarrier2 barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; + barrier.srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT; + barrier.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT; + barrier.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + barrier.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT; + VkDependencyInfo dependency{}; + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency.memoryBarrierCount = 1; + dependency.pMemoryBarriers = &barrier; + vkCmdPipelineBarrier2(m_commands, &dependency); + } + m_buffercopies = false; m_commands = VK_NULL_HANDLE; return m_recorded; } +bool uploader::upload_buffer(std::shared_ptr const &Target, void const *Data, VkDeviceSize Size, std::vector> const &Parts) +{ + if ((Target == nullptr) || (Size == 0)) + { + return true; + } + job upload; + upload.buffertarget = Target; + upload.parts = Parts; + if (upload.parts.empty()) + { + upload.parts.emplace_back(0, Size); + } + upload.staging = std::make_shared(); + if ((false == upload.staging->create(*m_allocator, Size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, memory_kind::upload)) || (upload.staging->mapped() == nullptr)) + { + log_error("no staging buffer of " + memory_size_string(Size) + " for an upload"); + return false; + } + std::memcpy(upload.staging->mapped(), Data, static_cast(Size)); + upload.staging->flush(0, Size); + m_uploaded += Size; + if (m_commands != VK_NULL_HANDLE) + { + record(upload); + } + else + { + m_pending.emplace_back(std::move(upload)); + } + return true; +} + bool uploader::upload(std::shared_ptr const &Image, void const *Data, VkDeviceSize Size, std::vector const &Levels, bool const Mipmaps) { job upload; @@ -89,6 +138,47 @@ bool uploader::upload(std::shared_ptr const &Image, void const *Data, VkD void uploader::record(job &Job) { + if (Job.buffertarget != nullptr) + { + if (false == m_buffercopies) + { + // the frames sent before may still read the buffers the copies write to + VkMemoryBarrier2 barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2; + barrier.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + barrier.srcAccessMask = VK_ACCESS_2_NONE; + barrier.dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT; + barrier.dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT; + VkDependencyInfo dependency{}; + dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO; + dependency.memoryBarrierCount = 1; + dependency.pMemoryBarriers = &barrier; + vkCmdPipelineBarrier2(m_commands, &dependency); + m_buffercopies = true; + } + std::vector regions; + VkDeviceSize source{0}; + for (auto const &part : Job.parts) + { + VkBufferCopy2 region{}; + region.sType = VK_STRUCTURE_TYPE_BUFFER_COPY_2; + region.srcOffset = source; + region.dstOffset = part.first; + region.size = part.second; + regions.push_back(region); + source += part.second; + } + VkCopyBufferInfo2 copy{}; + copy.sType = VK_STRUCTURE_TYPE_COPY_BUFFER_INFO_2; + copy.srcBuffer = Job.staging->handle(); + copy.dstBuffer = Job.buffertarget->handle(); + copy.regionCount = static_cast(regions.size()); + copy.pRegions = regions.data(); + vkCmdCopyBuffer2(m_commands, ©); + m_recorded = true; + m_deletions->push(m_value, [staging = Job.staging, target = Job.buffertarget]() {}); + return; + } auto &target{*Job.target}; auto const commands{m_commands}; auto const aspect{target.aspect()}; diff --git a/rendering/vulkan14/vulkan14texture.h b/rendering/vulkan14/vulkan14texture.h index 8b7b834e..16ecacf6 100644 --- a/rendering/vulkan14/vulkan14texture.h +++ b/rendering/vulkan14/vulkan14texture.h @@ -54,6 +54,10 @@ class uploader // copies Data into Image (the levels given, the others made by blits of the first one when Mipmaps), the image left ready to be // sampled. Data is copied away. false: no staging memory, nothing will be copied bool upload(std::shared_ptr const &Image, void const *Data, VkDeviceSize Size, std::vector const &Levels, bool const Mipmaps); + // copies Data into Target, done before the draws of the frame read it (and after the frames sent before did). Data holds the + // pieces of Parts (offset in Target, size) one after another; all of Data goes to the start of Target when there are none. + // false: no staging memory + bool upload_buffer(std::shared_ptr const &Target, void const *Data, VkDeviceSize Size, std::vector> const &Parts = {}); // the bytes uploaded since the start, for the debug panel std::uint64_t uploaded() const { @@ -69,6 +73,9 @@ class uploader std::shared_ptr staging; std::vector levels; bool mipmaps{false}; + // a copy to a buffer instead: the pieces copied + std::shared_ptr buffertarget; + std::vector> parts; }; void record(job &Job); @@ -78,6 +85,8 @@ class uploader VkCommandBuffer m_commands{VK_NULL_HANDLE}; 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 + bool m_buffercopies{false}; std::vector m_pending; std::uint64_t m_uploaded{0}; mutable std::unordered_map m_blittable; @@ -153,9 +162,10 @@ struct vulkan14_texture : public texture_source bool create(bool const Static = false) override; void release() override; + // as the opengl textures give it: one not made yet has no id of 0, which the materials take for a texture which is missing size_t get_id() const override { - return static_cast(m_id); + return (m_made ? static_cast(m_id) : std::numeric_limits::max()); } void update_from_memory(size_t width, size_t height, const uint8_t *data) override; bool resident() const override diff --git a/shaders.7z b/shaders.7z deleted file mode 100644 index 6e4592fc5905c6c2883f43f6bbc94d0ba058e493..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 74299 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 V0R>$EFaQ7yq<#tk1%pTm007qSTw4GD diff --git a/shaders/README.md b/shaders/README.md index 2ee93eeb..16cfbc2a 100644 --- a/shaders/README.md +++ b/shaders/README.md @@ -41,3 +41,25 @@ source, compile the binaries again (or delete them): a binary older than its sou * The constants of the game (bindings, sizes) are written in `ogl46/common.glsl` and `vk14/common.glsl` for the binaries; the game puts its own values before the sources and checks at the start that the files have the same ones (the shader validator in `ci_shadervalidator` checks them too). + +## vk14 + +The shaders of `vk14/` are those of `ogl46/` made for the descriptor sets of the Vulkan renderer +(`rendering/vulkan14/vulkan14bindings.h`), drawn with the same matrices and the same conventions as the OpenGL renderer +(nothing is turned upside down but the picture shown in the window, `present.frag`). + +* A stage file includes `common.glsl` right after its stage define. The pipeline layout it is drawn with is picked by a + define before that include: none for the passes of the scene, `LAYOUT_POST` for the post-processing (with `quad.vert`), + `LAYOUT_CLUSTER` for the compute passes of the light clusters, `LAYOUT_UI` for the user interface. +* The scene: set `PASS_SET` holds the blocks of the scene, of the draws (`model_draws[]`, the one of a draw picked by the push + constant `pc.draw`), of the instances and of the lights and clusters, and the shadow, environment and headlight maps; + set `MATERIAL_SET` the textures of the material at `MATERIAL_TEXTURE_FIRST + unit` of their `#pragma texture`. The names of + the model block (`modelview`, `param`, `opacity` ...) stand for the fields of the draw, as in the `MODEL_DRAWS` variant of + `ogl46/`. +* The push constants (`pc`, `PUSH_CONSTANTS_SIZE` bytes) carry the draw and the parameters of the special shaders in + `pc.data[]`; each of those says at the top what it takes from them. In the post-processing `param` stands for `pc.data`. +* The settings are specialization constants with the ids `SPEC_*` (`vk14/settings.glsl`). +* The features of the later stages of the renderer (the shadows of the lamps and the headlights, the contact shadows, the + weather and its fog, the sky ambient light, the prefiltered environment map, the automatic exposure, soft and gpu + particles) aren't in `vk14/` yet; the code paths are the ones `ogl46/` takes with them off, the fog is an analytic one + (`vk14/apply_fog.glsl`). diff --git a/shaders/compile_spirv.py b/shaders/compile_spirv.py index 407bac8b..f77c8215 100644 --- a/shaders/compile_spirv.py +++ b/shaders/compile_spirv.py @@ -23,9 +23,10 @@ import sys STAGES = ('.vert', '.frag', '.geom', '.comp') TREES = { - # source directory, binary directory, target environment, variants of the vertex and fragment shaders - 'ogl46': ('ogl46', 'ogl46bin', 'opengl4.5', ['MODEL_DRAWS']), - 'vk14': ('vk14', 'vk14bin', 'vulkan1.4', []), + # source directory, binary directory, target environment, variants of the vertex and fragment shaders, whether a variant is + # made only for the files which use its name (opengl links a program from the binaries of all of its stages or from none) + 'ogl46': ('ogl46', 'ogl46bin', 'opengl4.5', ['MODEL_DRAWS'], False), + 'vk14': ('vk14', 'vk14bin', 'vulkan1.4', ['NO_MOTION_OUTPUT'], True), } INCLUDE = re.compile(r'^[ \t]*#include[ \t]*[<"]([^>"]+)[>"]', re.M) @@ -84,7 +85,7 @@ def main(): failed = [] count = 0 - for key, (sourcedir, binarydir, env, variants) in TREES.items(): + for key, (sourcedir, binarydir, env, variants, onlyused) in TREES.items(): if args.only and args.only != key: continue sources = os.path.join(args.shaders, sourcedir) @@ -96,13 +97,17 @@ def main(): names = sorted(n for n in os.listdir(sources) if os.path.splitext(n)[1] in STAGES) for name in names: jobs = [('', [])] - if os.path.splitext(name)[1] in ('.vert', '.frag'): - jobs += [(v, ['-D%s=1' % v]) for v in variants] try: deps = dependencies(sources, name) except OSError as e: failed.append('%s/%s (%s)' % (sourcedir, name, e)) continue + if os.path.splitext(name)[1] in ('.vert', '.frag'): + text = '' + for dep in deps: + with open(os.path.join(sources, dep), 'r', encoding='utf-8', errors='replace') as f: + text += f.read() + jobs += [(v, ['-D%s=1' % v]) for v in variants if (not onlyused) or (v in text)] for variant, defines in jobs: binary = name + ('.' + variant if variant else '') + '.spv' command = [glslc, '--target-env=' + env] + defines + [os.path.join(sources, name), '-o', os.path.join(binaries, binary)] diff --git a/shaders/vk14/alphashadowmap.frag b/shaders/vk14/alphashadowmap.frag new file mode 100644 index 00000000..9044c6f6 --- /dev/null +++ b/shaders/vk14/alphashadowmap.frag @@ -0,0 +1,13 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +layout(location = 2) in vec2 f_coord; + +#pragma texture(tex1, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D tex1; + +void main() +{ + if (texture(tex1, f_coord).a < 0.5f) + discard; +} diff --git a/shaders/vk14/apply_fog.glsl b/shaders/vk14/apply_fog.glsl new file mode 100644 index 00000000..be00979e --- /dev/null +++ b/shaders/vk14/apply_fog.glsl @@ -0,0 +1,40 @@ +// 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 +// 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)) + { + float sunward = pow(max(dot(normalize(f_pos.xyz), normalize(-lights[0].dir)), 0.0), 30.0); + color = mix(color, lights[0].color, sunward); + } + return vec4(color * amount, 1.0 - amount); +} + +vec3 get_fog_color() +{ + vec4 fog = fog_at_fragment(); + return fog.rgb / max(1.0 - fog.a, 1e-4); +} + +float get_fog_amount() +{ + return 1.0 - fog_at_fragment().a; +} + +vec3 apply_fog(vec3 color) +{ + vec4 fog = fog_at_fragment(); + return color * fog.a + fog.rgb; +} + +vec3 add_fog(vec3 color, float amount) +{ + return color + fog_at_fragment().rgb * amount; +} diff --git a/shaders/vk14/billboard.frag b/shaders/vk14/billboard.frag new file mode 100644 index 00000000..f09b2dd7 --- /dev/null +++ b/shaders/vk14/billboard.frag @@ -0,0 +1,34 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +layout(location = 2) in vec2 f_coord; + +#pragma texture(tex1, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D tex1; + +#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() +{ + vec4 tex_color = texture(tex1, f_coord); + if (POSTFX_ENABLED) + { + out_color = tex_color * param[0]; + } + else + { + out_color = tonemap(tex_color * param[0]); + } + if (MOTIONBLUR_ENABLED) + { + out_motion = vec4(0.0f); + } +} diff --git a/shaders/vk14/celestial.frag b/shaders/vk14/celestial.frag new file mode 100644 index 00000000..aa34aafd --- /dev/null +++ b/shaders/vk14/celestial.frag @@ -0,0 +1,93 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +// the sun, the moon and the textured sprite of the easter egg, drawn over the sky on a quad around the direction of the body +// (celestial.vert). param[0]: colour (rgb) and alpha, param[1]: position in view space and half the size of the quad, +// param[2].xy: corner (sprite) or centre (moon) of the picture in the texture, param[2].z: its size (sprite) or the radius of +// the disc in it (moon), in texture units, param[2].w: kind of body, 0: textured sprite, 1: sun, 2: moon. +// the colour goes out premultiplied, the alpha is how much of the sky behind the body it hides (blending ONE, ONE_MINUS_SRC_ALPHA) + +layout(location = 2) in vec2 f_coord; +layout(location = 15) in vec2 f_local; + +#pragma texture(tex1, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D tex1; + +#include "tonemapping.glsl" + +// shape and light of the sun and the moon, pc.data[0..2] of the push constants (a loose uniform vec4 body[3] in the opengl +// renderer, set by opengl46_renderer::Render(world_environment *)): +// [0].xy: direction to the zenith on the quad, [0].z: height over width of the disc (flattened by the refraction near the +// horizon), [0].w: radius of the disc in units of half the quad +// [1].xyz: direction to the sun in view space, [1].w: light of the earth on the dark part of the moon, against the light of +// the sun on the lit part +// [2].rgb: limb darkening of the sun per channel (linear law) +#define body pc.data + +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() +{ + int kind = int(param[2].w + 0.5); + vec4 color = vec4(0.0); + if (kind == 0) + { + vec4 tex_color = texture(tex1, f_coord) * param[0]; + color = vec4(tex_color.rgb * tex_color.a, 0.0); + } + else + { + // the point on the disc in its radii, x along the horizon, y to the zenith, with the flattening undone + vec2 zenith = body[0].xy; + vec2 across = vec2(zenith.y, -zenith.x); + vec2 p = vec2(dot(f_local, across), dot(f_local, zenith)) / body[0].w; + p.y /= body[0].z; + float r = length(p); + float coverage = clamp((1.0 - r) / max(fwidth(r), 1e-4) + 0.5, 0.0, 1.0); + // the horizon hides what is under it + vec3 direction = normalize(param[1].xyz + vec3(f_local * param[1].w, 0.0)); + float height = (mat3(inv_view) * direction).y; + coverage *= clamp(height / max(fwidth(height), 1e-7) + 0.5, 0.0, 1.0); + // cosine of the angle between the line of sight and the surface of the sphere + float mu = sqrt(max(1.0 - r * r, 0.0)); + if (kind == 1) + { + // limb darkening, kept to the average brightness of the disc + vec3 limb = (vec3(1.0) - body[2].rgb * (1.0 - mu)) / (vec3(1.0) - body[2].rgb / 3.0); + color = vec4(param[0].rgb * limb * coverage, 0.0); + } + else + { + // the moon as a sphere lit from the direction of the sun. the frame of the disc in view space, z to the viewer + vec3 z = -normalize(param[1].xyz); + vec3 x = vec3(across, 0.0); + x = normalize(x - z * dot(x, z)); + vec3 y = cross(z, x); + vec2 q = p / max(r, 1.0); + vec3 normal = q.x * x + q.y * y + mu * z; + float mu0 = max(dot(normal, normalize(body[1].xyz)), 0.0); + // Lommel-Seeliger law: an even disc at the full moon, as the real one, no darkening towards the limb + float lit = 2.0 * mu0 / (mu0 + max(mu, 1e-3)); + vec3 albedo = texture(tex1, param[2].xy + p * param[2].z).rgb; + color = vec4(albedo * param[0].rgb * ((lit + body[1].w) * coverage), coverage * param[0].a); + } + } + if (POSTFX_ENABLED) + { + out_color = color; + } + else + { + out_color = vec4(tonemap(vec4(color.rgb, 1.0)).rgb, color.a); + } + if (MOTIONBLUR_ENABLED) + { + out_motion = vec4(0.0f); + } +} diff --git a/shaders/vk14/celestial.vert b/shaders/vk14/celestial.vert new file mode 100644 index 00000000..032d72c8 --- /dev/null +++ b/shaders/vk14/celestial.vert @@ -0,0 +1,34 @@ +#version 460 core +#define STAGE_VERTEX 1 +#include "common.glsl" +const vec2 vert[4] = vec2[] +( + vec2(-1.0, 1.0), + vec2(-1.0, -1.0), + vec2( 1.0, 1.0), + vec2( 1.0, -1.0) +); + +layout(location = 2) out vec2 f_coord; +// the corner of the quad, -1...1 across it +layout(location = 15) out vec2 f_local; + + +void main() +{ + gl_Position = projection * (vec4(param[1].xyz, 1.0) + vec4(vert[gl_VertexIndex] * param[1].w, 0.0, 0.0)); + + f_local = vert[gl_VertexIndex]; + + vec2 coord = param[2].xy; + float size = param[2].z; + + if (gl_VertexIndex == 0) + f_coord = vec2(coord.x, coord.y); + if (gl_VertexIndex == 1) + f_coord = vec2(coord.x, coord.y - size); + if (gl_VertexIndex == 2) + f_coord = vec2(coord.x + size, coord.y); + if (gl_VertexIndex == 3) + f_coord = vec2(coord.x + size, coord.y - size); +} diff --git a/shaders/vk14/cluster_build.comp b/shaders/vk14/cluster_build.comp new file mode 100644 index 00000000..2eafc168 --- /dev/null +++ b/shaders/vk14/cluster_build.comp @@ -0,0 +1,59 @@ +#version 460 core +#define STAGE_COMPUTE 1 +#define LAYOUT_CLUSTER 1 +#include "common.glsl" +// bounding boxes of the light clusters in view space, see gl/cluster_grid.h +// one invocation per cluster, run when the projection or the depth range of the pass changes + +layout(local_size_x = CLUSTER_BUILD_GROUP_SIZE) in; + +// clip space w at the lower boundary of slice k: the inverse of the slice formula used when shading +float slice_depth(uint k) +{ + return exp((float(k) - cluster_slice_bias) / cluster_slice_scale); +} + +void main() +{ + uint index = gl_GlobalInvocationID.x; + if (index >= uint(CLUSTER_COUNT)) + return; + + uvec3 cell = uvec3( + index % uint(CLUSTER_GRID_X), + (index / uint(CLUSTER_GRID_X)) % uint(CLUSTER_GRID_Y), + index / uint(CLUSTER_GRID_X * CLUSTER_GRID_Y)); + + // clip x, y and w are affine functions of the view space position, while clip z is constant (reverse z with + // the far plane at infinity), which makes the projection matrix singular. the position is recovered from + // those three alone, so off-axis and rotated screens of custom viewports and vr eyes work too + mat3 linear = mat3( + vec3(cluster_projection[0][0], cluster_projection[0][1], cluster_projection[0][3]), + vec3(cluster_projection[1][0], cluster_projection[1][1], cluster_projection[1][3]), + vec3(cluster_projection[2][0], cluster_projection[2][1], cluster_projection[2][3])); + vec3 translation = vec3(cluster_projection[3][0], cluster_projection[3][1], cluster_projection[3][3]); + mat3 unproject = inverse(linear); + + // the tile in clip space xy, the slice in clip space w + vec2 tiles = vec2(CLUSTER_GRID_X, CLUSTER_GRID_Y); + vec2 ndc_min = vec2(cell.xy) / tiles * 2.0 - 1.0; + vec2 ndc_max = vec2(cell.xy + 1U) / tiles * 2.0 - 1.0; + float w_near = slice_depth(cell.z); + float w_far = slice_depth(cell.z + 1U); + + vec3 lower = vec3(3.402823e38); + vec3 upper = vec3(-3.402823e38); + for (int corner = 0; corner < 8; ++corner) + { + vec2 ndc = vec2( + (corner & 1) != 0 ? ndc_max.x : ndc_min.x, + (corner & 2) != 0 ? ndc_max.y : ndc_min.y); + float w = (corner & 4) != 0 ? w_far : w_near; + vec3 position = unproject * (vec3(ndc * w, w) - translation); + lower = min(lower, position); + upper = max(upper, position); + } + + cluster_aabbs[index].lower = vec4(lower, 0.0); + cluster_aabbs[index].upper = vec4(upper, 0.0); +} diff --git a/shaders/vk14/cluster_cull.comp b/shaders/vk14/cluster_cull.comp new file mode 100644 index 00000000..141f9eaf --- /dev/null +++ b/shaders/vk14/cluster_cull.comp @@ -0,0 +1,157 @@ +#version 460 core +#define STAGE_COMPUTE 1 +#define LAYOUT_CLUSTER 1 +#include "common.glsl" +// assigns the lights to the light clusters, see gl/cluster_grid.h +// one invocation per cluster: tests every light of light_ssbo (the sun, slot 0, is handled apart from the clusters) +// against the bounding box of the cluster in view space, and writes the indices of the lights which reach it. +// run every frame after the lights were updated, for the grid of the pass about to be drawn + +layout(local_size_x = CLUSTER_CULL_GROUP_SIZE) in; + +// a light is taken to end where its brightest possible contribution drops under this, a fraction of +// what the display can tell apart. there is no other limit: the 1 km filter on the cpu side is about the distance of a light +// from the camera, not about how far it reaches, and the shading lights a surface at any distance +const float MIN_CONTRIBUTION = 1.0 / 1024.0; +// reach of a light whose contribution does not fall off with the distance; its square still fits in a float +const float UNLIMITED_RANGE = 1.0e18; +// the boxes and the light data are floats: planes of the headlight projection let this much (in meters) of a box through +const float PLANE_MARGIN = 0.05; + +// brightest value the light adds to a surface it hits, as the shading computes it +float light_peak(light_s light) +{ + float peak = max(light.color.r, max(light.color.g, light.color.b)) * light.intensity; + if (light.type == LIGHT_HEADLIGHTS) + // weights are the state of the individual lamps and the beam texture, which is not brighter than 1, picks from them + return peak * max(light.headlight_weights.r, max(light.headlight_weights.g, light.headlight_weights.b)); + return peak * (1.0 + light.ambient); +} + +// distance from the light at which its contribution falls under MIN_CONTRIBUTION, 0 for a light which has none +float light_range(light_s light) +{ + float peak = light_peak(light); + if (!(peak > MIN_CONTRIBUTION)) + return 0.0; + // attenuation which the contribution can drop to without being lost: 1 / k + float k = peak / MIN_CONTRIBUTION; + + if (light.type == LIGHT_HEADLIGHTS) + { + // 1 + linear * d + quadratic * d^2 <= k, the positive root written the way which has no cancellation for a small quadratic + float a = light.quadratic; + float b = light.linear; + float c = 1.0 - k; + float denominator = b + sqrt(b * b - 4.0 * a * c); + return (denominator > 1e-12) ? min(-2.0 * c / denominator, UNLIMITED_RANGE) : UNLIMITED_RANGE; + } + // point and spot lights fall off with the square of the distance; the shading ends them at 1 / sqrt(quadratic) where it is set + float range = sqrt(k); + return (light.quadratic > 0.0) ? min(range, inversesqrt(light.quadratic)) : range; +} + +bool sphere_touches_box(vec3 center, float radius, vec3 lower, vec3 upper) +{ + vec3 offset = center - clamp(center, lower, upper); + return dot(offset, offset) <= radius * radius; +} + +// whether any part of the box, given by its center and half extents, is on the positive side of the plane +bool box_reaches_plane(vec4 plane, vec3 center, vec3 extent) +{ + return dot(plane.xyz, center) + plane.w + dot(abs(plane.xyz), extent) + PLANE_MARGIN * length(plane.xyz) >= 0.0; +} + +// planes bounding the beam of a headlight: the projection takes a view space position to the texture coordinates +// of the beam, and the beam is where those fall in 0..1 and the position is in front of the projector. clip z of that +// projection is a constant (reverse z, no far plane), so only the four sides and the near plane are real planes +bool box_in_headlight_beam(mat4 projection, vec3 center, vec3 extent) +{ + vec4 row_x = vec4(projection[0][0], projection[1][0], projection[2][0], projection[3][0]); + vec4 row_y = vec4(projection[0][1], projection[1][1], projection[2][1], projection[3][1]); + vec4 row_z = vec4(projection[0][2], projection[1][2], projection[2][2], projection[3][2]); + vec4 row_w = vec4(projection[0][3], projection[1][3], projection[2][3], projection[3][3]); + + return box_reaches_plane(row_x, center, extent) + && box_reaches_plane(row_w - row_x, center, extent) + && box_reaches_plane(row_y, center, extent) + && box_reaches_plane(row_w - row_y, center, extent) + && box_reaches_plane(row_z, center, extent) + && box_reaches_plane(row_w - row_z, center, extent); +} + +// whether the sphere touches the infinite cone with the apex at the origin, the unit axis and the cosine of the half angle +bool sphere_touches_cone(vec3 center, float radius, vec3 axis, float cosine) +{ + float sine = max(sqrt(max(1.0 - cosine * cosine, 0.0)), 1e-4); + float along = dot(center, axis); + float across = sqrt(max(dot(center, center) - along * along, 0.0)); + // distance to the boundary ray if the closest point of the cone is on it, and to the apex otherwise + float distance_to_cone = (along * cosine + across * sine > 0.0) ? across * cosine - along * sine : length(center); + return distance_to_cone <= radius; +} + +bool light_reaches_box(light_s light, float range, vec3 lower, vec3 upper) +{ + vec3 center = 0.5 * (lower + upper); + vec3 extent = 0.5 * (upper - lower); + + if (light.type == LIGHT_DIR) + return true; + + if (!sphere_touches_box(light.pos, range, lower, upper)) + return false; + + if (light.type == LIGHT_HEADLIGHTS) + return box_in_headlight_beam(light.headlight_projection, center, extent); + + if ((light.type == LIGHT_SPOT) && (light.out_cutoff > 0.0)) + // the cone is tested against the sphere around the box, which lets some of the corners through. a cone wider than + // a half of the space is taken as the whole sphere + return sphere_touches_cone(center - light.pos, length(extent), normalize(light.dir), light.out_cutoff); + + return true; +} + +void main() +{ + uint index = gl_GlobalInvocationID.x; + if (index >= uint(CLUSTER_COUNT)) + return; + + vec3 lower = cluster_aabbs[index].lower.xyz; + vec3 upper = cluster_aabbs[index].upper.xyz; + + // the lights are in the order of their importance, so when a cluster is full the least important are the ones left out + uint found[MAX_LIGHTS_PER_CLUSTER]; + uint found_count = 0U; + uint dropped = 0U; + + uint lights_end = min(lights_total, uint(lights.length())); + for (uint i = 1U; i < lights_end; ++i) + { + float range = light_range(lights[i]); + if (range <= 0.0 && lights[i].type != LIGHT_DIR) + continue; + if (!light_reaches_box(lights[i], range, lower, upper)) + continue; + + if (found_count < uint(MAX_LIGHTS_PER_CLUSTER)) + found[found_count++] = i; + else + ++dropped; + } + + uint offset = 0U; + if (found_count > 0U) + { + offset = atomicAdd(cluster_index_count, found_count); + for (uint k = 0U; k < found_count; ++k) + cluster_light_indices[offset + k] = found[k]; + } + if (dropped > 0U) + atomicAdd(cluster_overflow_count, dropped); + + cluster_ranges[index] = cluster_range_s(offset, found_count); +} diff --git a/shaders/vk14/color.frag b/shaders/vk14/color.frag new file mode 100644 index 00000000..785e5f89 --- /dev/null +++ b/shaders/vk14/color.frag @@ -0,0 +1,35 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +layout(location = 13) in vec3 f_color; +layout(location = 3) in vec4 f_pos; + +#include "apply_fog.glsl" +#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 + +#include "sky_shoulder.glsl" + +void main() +{ + vec3 col = sky_shoulder(pow(max(f_color.rgb, vec3(0.0)), vec3(2.2))); + 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/common.glsl b/shaders/vk14/common.glsl index e5e920fe..7f70e12d 100644 --- a/shaders/vk14/common.glsl +++ b/shaders/vk14/common.glsl @@ -1,7 +1,22 @@ -// what the shaders of the vulkan 1.4 renderer share: the bindings of rendering/vulkan14/vulkan14bindings.h and the settings. +// what the shaders of the vulkan 1.4 renderer share: the bindings of rendering/vulkan14/vulkan14bindings.h, the constants of the +// game, the settings, the push constants and the resources of the pipeline layout a shader is drawn with. // compiled by the game, the bindings come from the renderer (vk14::binding_preamble()) and are taken instead of these; the // SPIR-V binaries (shaders/vk14bin) are compiled with the values written here, so they have to stay the same as in the game, // which checks them when it starts (and doesn't load the binaries when they differ) +// +// 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) +// 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 +// +// the locations of the inputs and outputs between the stages are the table of shaders/ogl46/common.glsl, the same for a name +// in every shader: 0 f_normal, 1 f_normal_raw, 2 f_coord, 3 f_pos, 4 f_clip_pos, 5 f_clip_future_pos, 6 TangentFragPos, +// 7 UserData, 8-10 f_tbn, 12 f_albedo, 13 f_color, 14 f_coords, 15 f_local, 16 f_soft, 17 f_streak (11 f_model_draw is not +// used: the draw is in the push constants) #ifndef COMMON_GLSL #define COMMON_GLSL @@ -15,18 +30,276 @@ #define UI_TEXTURE 0 #define PASS_SET 0 #define MATERIAL_SET 1 +#define SCENE_UBO 0 +#define DRAW_SSBO 1 +#define INSTANCE_SSBO 2 +#define LIGHT_SSBO 3 +#define CLUSTER_UBO 4 +#define CLUSTER_RANGE_SSBO 5 +#define CLUSTER_INDEX_SSBO 6 +#define SHADOW_TEX 7 +#define ENV_TEX 8 +#define HEADLIGHT_TEX 9 +#define PASS_BINDING_COUNT 10 #define MATERIAL_TEXTURE_FIRST 0 +#define CL_CLUSTER_UBO 0 +#define CL_AABB_SSBO 1 +#define CL_RANGE_SSBO 2 +#define CL_INDEX_SSBO 3 +#define CL_COUNTER_SSBO 4 +#define CL_LIGHT_SSBO 5 +#define POST_SET 0 +#define POST_TEXTURE_FIRST 0 +#define PUSH_CONSTANTS_SIZE 112 #define SPEC_OUTPUT_GAMMA 0 +#define SPEC_SHADOWMAP_ENABLED 1 +#define SPEC_ENVMAP_ENABLED 2 +#define SPEC_MOTIONBLUR_ENABLED 3 +#define SPEC_POSTFX_ENABLED 4 +#define SPEC_EXTRAEFFECTS_ENABLED 5 +#define SPEC_COUNT 6 +#define MAX_LIGHTS 1024U +#define MAX_CASCADES 3U +#define MAX_PARAMS 3U +#define MAX_HEADLIGHT_SHADOWS 4U +#define CLUSTER_GRID_X 16 +#define CLUSTER_GRID_Y 9 +#define CLUSTER_GRID_Z 24 +#define CLUSTER_COUNT 3456 +#define MAX_LIGHTS_PER_CLUSTER 64 +#define CLUSTER_BUILD_GROUP_SIZE 64 +#define CLUSTER_CULL_GROUP_SIZE 64 #endif -// whether the colour written to the window is encoded by the shader (FBOUT): the window has no srgb format (gfx.shadergamma yes). -// a specialization constant, set by the renderer when it makes the pipeline -layout(constant_id = SPEC_OUTPUT_GAMMA) const bool OUTPUT_GAMMA = false; +#include "settings.glsl" -// the colour written to the window -vec4 FBOUT(vec4 x) +#ifndef LAYOUT_UI +// 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 { - return (OUTPUT_GAMMA ? vec4(pow(x.rgb, vec3(1.0 / 2.2)), x.a) : x); + uint draw; + uint flags; + uint pad0; + uint pad1; + vec4 data[6]; +} pc; +#endif + +const uint LIGHT_SPOT = 0U; +const uint LIGHT_POINT = 1U; +const uint LIGHT_DIR = 2U; +const uint LIGHT_HEADLIGHTS = 3U; + +// gl::light_element +struct light_s +{ + vec3 pos; + uint type; + + vec3 dir; + float in_cutoff; + + vec3 color; + float out_cutoff; + + float linear; + float quadratic; + + float intensity; + float ambient; + + mat4 headlight_projection; + vec4 headlight_weights; +}; + +// light clusters, see gl/cluster_grid.h +struct cluster_aabb_s +{ + vec4 lower; + vec4 upper; +}; + +struct cluster_range_s +{ + uint offset; + uint count; +}; + +#if defined(LAYOUT_CLUSTER) + +layout(std140, set = 0, binding = CL_CLUSTER_UBO) uniform cluster_ubo +{ + mat4 cluster_projection; + float cluster_znear; + float cluster_zfar; + float cluster_slice_scale; + float cluster_slice_bias; + vec2 cluster_viewport_size; + uint cluster_valid; +}; + +layout(std430, set = 0, binding = CL_AABB_SSBO) buffer cluster_aabb_ssbo +{ + cluster_aabb_s cluster_aabbs[]; +}; + +layout(std430, set = 0, binding = CL_RANGE_SSBO) buffer cluster_range_ssbo +{ + cluster_range_s cluster_ranges[]; +}; + +layout(std430, set = 0, binding = CL_INDEX_SSBO) buffer cluster_index_ssbo +{ + uint cluster_light_indices[]; +}; + +layout(std430, set = 0, binding = CL_COUNTER_SSBO) buffer cluster_counter_ssbo +{ + uint cluster_index_count; + uint cluster_overflow_count; +}; + +layout(std430, set = 0, binding = CL_LIGHT_SSBO) readonly buffer light_ssbo +{ + vec3 ambient; + // lights stored in lights[], what the cluster culling goes through + uint lights_total; + + vec3 fog_color; + // lights the shading loop goes through when the lights are not clustered + 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) + float sky_ambient_factor; + uint env_ggx; + + light_s lights[]; +}; + +#elif defined(LAYOUT_POST) + +// the post-processing reads the parameters the opengl renderer gives it in the model block from the push constants +#define param pc.data + +#elif !defined(LAYOUT_UI) + +// the block of the scene, gl::scene_ubs (every member kept, the layout is the one of the c++ struct) +layout(std140, set = PASS_SET, binding = SCENE_UBO) uniform scene_ubo +{ + mat4 projection; + mat4 inv_view; + mat4 lightview[MAX_CASCADES]; + vec3 cascade_end; + float time; + vec4 rain_params; + vec4 wiper_pos; + vec4 wiper_timer_out; + vec4 wiper_timer_return; + mat4 headlight_shadow[MAX_HEADLIGHT_SHADOWS]; + vec4 headlight_shadow_params[MAX_HEADLIGHT_SHADOWS]; + vec4 contact_shadow_params; + vec4 contact_shadow_lamps; + 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 +// fields, so the shaders read them as the uniform block of the opengl renderer +struct model_draw_s +{ + mat4 modelview; + mat3 modelviewnormal; + vec4 param[MAX_PARAMS]; + + mat4 future; + float opacity; + float emission; + float fog_density; + float alpha_mult; + float shadow_tone; +}; + +layout(std430, set = PASS_SET, binding = DRAW_SSBO) readonly buffer model_draw_ssbo +{ + 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 + +// 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 +// the identity, not from the camera or the instance), so the product is the final transform per instance per submodel +layout(std430, set = PASS_SET, binding = INSTANCE_SSBO) readonly buffer instance_ssbo +{ + mat4 instance_modelview[]; +}; + +// the light buffer of the pass, gl::light_data +layout(std430, set = PASS_SET, binding = LIGHT_SSBO) readonly buffer light_ssbo +{ + vec3 ambient; + // lights stored in lights[], what the cluster culling goes through + uint lights_total; + + vec3 fog_color; + // lights the shading loop goes through when the lights are not clustered + 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) + float sky_ambient_factor; + uint env_ggx; + + light_s lights[]; +}; + +// 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 +{ + mat4 cluster_projection; + float cluster_znear; + float cluster_zfar; + float cluster_slice_scale; + float cluster_slice_bias; + vec2 cluster_viewport_size; + uint cluster_valid; +}; + +layout(std430, set = PASS_SET, binding = CLUSTER_RANGE_SSBO) readonly buffer cluster_range_ssbo +{ + cluster_range_s cluster_ranges[]; +}; + +layout(std430, set = PASS_SET, binding = CLUSTER_INDEX_SSBO) readonly buffer cluster_index_ssbo +{ + uint cluster_light_indices[]; +}; + +#ifdef STAGE_VERTEX +mat4 effective_modelview() +{ + return instance_modelview[gl_InstanceIndex] * modelview; } +mat3 effective_modelviewnormal() +{ + // instance_modelview can include per-instance scale (uniform or per-axis), so its upper 3x3 is not its own inverse + // transpose in the non-uniform case + mat3 instance_mv3 = mat3(instance_modelview[gl_InstanceIndex]); + mat3 instance_normal = transpose(inverse(instance_mv3)); + return instance_normal * modelviewnormal; +} +#endif + +#endif + #endif diff --git a/shaders/vk14/conversion.glsl b/shaders/vk14/conversion.glsl new file mode 100644 index 00000000..d0f2157c --- /dev/null +++ b/shaders/vk14/conversion.glsl @@ -0,0 +1,108 @@ +vec3 rgb2yuv(vec3 val) +{ + return + clamp( + vec3( + (0.257 * val.r) + (0.504 * val.g) + (0.098 * val.b) + 0.0625, + -(0.148 * val.r) - (0.291 * val.g) + (0.439 * val.b) + 0.5, + (0.439 * val.r) - (0.368 * val.g) - (0.071 * val.b) + 0.5 ), + 0.0, 1.0); +} + +vec3 yuv2rgb(vec3 val) +{ + val -= vec3(0.0625, 0.5, 0.5); + return + clamp( + vec3( + (1.164 * val.r) + (1.596 * val.b), + (1.164 * val.r) - (0.391 * val.g) - (0.813 * val.b), + (1.164 * val.r) + (2.018 * val.g) ), + 0.0, 1.0); +} + +vec3 rgb2hsl( vec3 col ) +{ + float red = col.r; + float green = col.g; + float blue = col.b; + + float minc = min( col.r, min( col.g, col.b )); + float maxc = max( col.r, max( col.g, col.b )); + float delta = maxc - minc; + + float lum = (minc + maxc) * 0.5; + float sat = 0.0; + float hue = 0.0; + + if (lum > 0.0 && lum < 1.0) { + float mul = (lum < 0.5) ? (lum) : (1.0-lum); + sat = delta / (mul * 2.0); + } + + vec3 masks = vec3( + (maxc == red && maxc != green) ? 1.0 : 0.0, + (maxc == green && maxc != blue) ? 1.0 : 0.0, + (maxc == blue && maxc != red) ? 1.0 : 0.0 + ); + + vec3 adds = vec3( + ((green - blue ) / delta), + 2.0 + ((blue - red ) / delta), + 4.0 + ((red - green) / delta) + ); + + float deltaGtz = (delta > 0.0) ? 1.0 : 0.0; + + hue += dot( adds, masks ); + hue *= deltaGtz; + hue /= 6.0; + + if (hue < 0.0) + hue += 1.0; + + return vec3( hue, sat, lum ); +} + +vec3 hsl2rgb( vec3 col ) +{ + const float onethird = 1.0 / 3.0; + const float twothird = 2.0 / 3.0; + const float rcpsixth = 6.0; + + float hue = col.x; + float sat = col.y; + float lum = col.z; + + vec3 xt = vec3( + rcpsixth * (hue - twothird), + 0.0, + rcpsixth * (1.0 - hue) + ); + + if (hue < twothird) { + xt.r = 0.0; + xt.g = rcpsixth * (twothird - hue); + xt.b = rcpsixth * (hue - onethird); + } + + if (hue < onethird) { + xt.r = rcpsixth * (onethird - hue); + xt.g = rcpsixth * hue; + xt.b = 0.0; + } + + xt = min( xt, 1.0 ); + + float sat2 = 2.0 * sat; + float satinv = 1.0 - sat; + float luminv = 1.0 - lum; + float lum2m1 = (2.0 * lum) - 1.0; + vec3 ct = (sat2 * xt) + satinv; + + vec3 rgb; + if (lum >= 0.5) { rgb = (luminv * ct) + lum2m1; } + else { rgb = lum * ct; } + + return rgb; +} diff --git a/shaders/vk14/copy.frag b/shaders/vk14/copy.frag new file mode 100644 index 00000000..51b33a5a --- /dev/null +++ b/shaders/vk14/copy.frag @@ -0,0 +1,15 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#define LAYOUT_POST 1 +#include "common.glsl" +layout(location = 14) in vec2 f_coords; + +#pragma texture(tex1, 0, sRGB) +layout(set = POST_SET, binding = POST_TEXTURE_FIRST + 0) uniform sampler2D tex1; + +layout(location = 0) out vec4 out_color; + +void main() +{ + out_color = texture(tex1, f_coords); +} diff --git a/shaders/vk14/dither.glsl b/shaders/vk14/dither.glsl new file mode 100644 index 00000000..b9861199 --- /dev/null +++ b/shaders/vk14/dither.glsl @@ -0,0 +1,26 @@ +// noise of half a step of the 8-bit output, added to the colour about to be written to the window: smooth dark gradients (the +// fog at night) show no bands, which otherwise crawl over the image with the smallest change of the light. interleaved gradient +// noise (Jimenez 2014) of the pixel, the same in every frame. the colour is the one written to the window: encoded already with +// OUTPUT_GAMMA, linear otherwise (the sRGB framebuffer encodes it, so the step is taken in the encoded values) +#ifndef DITHER_GLSL +#define DITHER_GLSL + +#include "common.glsl" + +vec3 dither_output(vec3 color) +{ + float noise = (fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))) - 0.5) / 255.0; + if (OUTPUT_GAMMA) + { + return max(color + noise, vec3(0.0)); + } + else + { + vec3 linear = max(color, vec3(0.0)); + vec3 encoded = mix(linear * 12.92, 1.055 * pow(linear, vec3(1.0 / 2.4)) - 0.055, step(vec3(0.0031308), linear)); + encoded = max(encoded + noise, vec3(0.0)); + return mix(encoded / 12.92, pow((encoded + 0.055) / 1.055, vec3(2.4)), step(vec3(0.04045), encoded)); + } +} + +#endif diff --git a/shaders/vk14/envmapping.glsl b/shaders/vk14/envmapping.glsl new file mode 100644 index 00000000..2d51dbf4 --- /dev/null +++ b/shaders/vk14/envmapping.glsl @@ -0,0 +1,54 @@ +layout(set = PASS_SET, binding = ENV_TEX) uniform samplerCube envmap; + +vec3 envmap_color( vec3 normal ) +{ + vec3 envcolor; + if (ENVMAP_ENABLED) + { + vec3 refvec = reflect(f_pos.xyz, normal); // view space + refvec = vec3(inv_view * vec4(refvec, 0.0)); // world space + envcolor = texture(envmap, refvec).rgb; + // Sanitize. The env cubemap can briefly contain NaN/Inf after a + // reflection-pass regeneration (m_empty_cubemap was bound during the + // face render and on NVIDIA can sample as undefined). Without this + // guard, a single bad texel propagates through env_reflection in + // apply_lights() and produces one-frame black flashes on glossy + // (esp. specgloss) surfaces. + if (any(isnan(envcolor)) || any(isinf(envcolor))) envcolor = vec3(0.0); + envcolor = max(envcolor, vec3(0.0)); + } + else + { + envcolor = vec3(0.5); + } + return envcolor; +} + +// Roughness-aware env map lookup — uses mip levels for blurry reflections. +// lod 0.0 = mirror sharp, lod ~8.0 = fully diffuse blur. +vec3 envmap_color_lod(vec3 fragnormal, float lod) +{ + if (ENVMAP_ENABLED) + { + vec3 refvec = reflect(f_pos.xyz, fragnormal); // view space — matches envmap_color exactly + refvec = vec3(inv_view * vec4(refvec, 0.0)); // world space — was missing + vec3 envcolor = textureLod(envmap, refvec, lod).rgb; + // See envmap_color() above — same NaN/Inf sanitize, also needed here + // because mipmap generation propagates a single NaN texel across the + // whole mip chain. + if (any(isnan(envcolor)) || any(isinf(envcolor))) envcolor = vec3(0.0); + return max(envcolor, vec3(0.0)); + } + else + { + return vec3(0.5); // was vec3(0.0), match the non-LOD fallback + } +} + +// 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 +vec3 envmap_color_rough(vec3 fragnormal, float roughness) +{ + const float MAX_REFLECTION_LOD = 8.0; + return envmap_color_lod(fragnormal, roughness * MAX_REFLECTION_LOD); +} diff --git a/shaders/vk14/freespot.frag b/shaders/vk14/freespot.frag new file mode 100644 index 00000000..c245194e --- /dev/null +++ b/shaders/vk14/freespot.frag @@ -0,0 +1,38 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +layout(location = 3) in vec4 f_pos; +layout(location = 4) in vec4 f_clip_pos; +layout(location = 5) in vec4 f_clip_future_pos; + + +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 "tonemapping.glsl" +#include "apply_fog.glsl" + +void main() +{ + float x = (gl_PointCoord.x - 0.5f) * 2.0f; + float y = (gl_PointCoord.y - 0.5f) * 2.0f; + float dist = sqrt(x * x + y * y); + if (dist > 0.5f) + discard; +// vec4 color = vec4(param[0].rgb * emission, mix(param[0].a, 0.0f, dist * 2.0f)); + vec4 color = vec4(apply_fog(pow(param[0].rgb, vec3(2.2)) * emission), mix(param[0].a, 0.0f, dist * 2.0f)); +// vec4 color = vec4(add_fog(param[0].rgb * emission * (1.0 - get_fog_amount()), 1.0), mix(param[0].a, 0.0f, dist * 2.0f)); + out_color = FBOUT(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/freespot.vert b/shaders/vk14/freespot.vert new file mode 100644 index 00000000..78b50f3f --- /dev/null +++ b/shaders/vk14/freespot.vert @@ -0,0 +1,21 @@ +#version 460 core +#define STAGE_VERTEX 1 +#include "common.glsl" +layout(location = 0) in vec3 v_vert; +layout(location = 1) in vec3 v_normal; +layout(location = 2) in vec2 v_coord; + + +layout(location = 3) out vec4 f_pos; +layout(location = 4) out vec4 f_clip_pos; +layout(location = 5) out vec4 f_clip_future_pos; + +void main() +{ + f_pos = modelview * vec4(v_vert, 1.0); + f_clip_pos = (projection * modelview) * vec4(v_vert, 1.0f); + f_clip_future_pos = (projection * future * modelview) * vec4(v_vert, 1.0f); + + gl_Position = f_clip_pos; + gl_PointSize = param[1].x; +} diff --git a/shaders/vk14/imgui.frag b/shaders/vk14/imgui.frag index e2fa43ed..f474bb13 100644 --- a/shaders/vk14/imgui.frag +++ b/shaders/vk14/imgui.frag @@ -1,5 +1,6 @@ #version 460 core #define STAGE_FRAGMENT 1 +#define LAYOUT_UI 1 // the user interface (imgui): the colour of the vertices times the texture of the draw (the font, an image, a texture), pushed // with the draw diff --git a/shaders/vk14/imgui.vert b/shaders/vk14/imgui.vert index e8824620..e3acaf17 100644 --- a/shaders/vk14/imgui.vert +++ b/shaders/vk14/imgui.vert @@ -1,5 +1,6 @@ #version 460 core #define STAGE_VERTEX 1 +#define LAYOUT_UI 1 // the user interface (imgui), drawn by vk14::imgui_backend over the image of the window #include "common.glsl" diff --git a/shaders/vk14/light_common.glsl b/shaders/vk14/light_common.glsl new file mode 100644 index 00000000..4db128e8 --- /dev/null +++ b/shaders/vk14/light_common.glsl @@ -0,0 +1,594 @@ +// 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 +layout(set = PASS_SET, binding = SHADOW_TEX) uniform sampler2DArrayShadow shadowmap; +layout(set = PASS_SET, binding = HEADLIGHT_TEX) uniform sampler2D headlightmap; + +#include "envmapping.glsl" +#include "conversion.glsl" + +float glossiness = 1.0; +float metalic = 0.0; +layout(location = 7) in vec4 UserData; +#pragma param(weathering, 2, 3, 1, zero) + +// --------------------------------------------------------------------- +// Lighting balance tunables - tweak these to control overall scene +// exposure without touching tonemapping.glsl. +// +// AMBIENT_SCALE: brightness of SHADED faces (indirect/sky term). +// Lower -> deeper shadows, less burn under bright +// textures. Higher -> flatter / brighter shading. +// +// SUN_DIFFUSE_SCALE: brightness of UNSHADED (sun-lit) faces. Lower +// this to dim hot surfaces in direct sunlight +// without affecting shaded areas. Was 3.5; 2.5 +// calmly fits the ACES tonemap shoulder. +// +// SUN_NDOTL_SHARPNESS: N.L curve on the sun. 1.0 = pure Lambert, higher +// = sharper terminator (more contrast between +// lit and shaded faces of the same surface). +// --------------------------------------------------------------------- +const float AMBIENT_SCALE = 0.65; +const float SUN_DIFFUSE_SCALE = 1.5; +const float SUN_NDOTL_SHARPNESS = 1.25; + +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 +float calc_shadow_map(); +float calc_shadow() +{ + return calc_shadow_map(); +} + +float calc_shadow_map() +{ + if (SHADOWMAP_ENABLED) + { + float distance = dot(f_pos.xyz, f_pos.xyz); + uint cascade; + for (cascade = 0U; cascade < MAX_CASCADES; cascade++) + if (distance <= cascade_end[cascade]) + break; + // cascades fitted to the depth of the view of an earlier frame (gfx.shadow.sdsm) cover only the area of the points seen + // then: a point at the edge of the view or one which came closer since may be outside the map of its cascade. it goes to + // the first further cascade whose map holds it with the reach of the filter, and is lit past the last one + if (shadow_fit.x > 0.0) + { + float border = 4.0 / float(textureSize(shadowmap, 0).x); + for (; cascade < MAX_CASCADES; cascade++) + { + vec2 fitcoords = (lightview[cascade] * vec4(f_pos.xyz, 1.0)).xy; + if (all(greaterThanEqual(fitcoords, vec2(border))) && all(lessThanEqual(fitcoords, vec2(1.0 - border)))) + break; + } + if (cascade >= MAX_CASCADES) + return 0.0; + } + // Normal offset: sample the shadow map from a point pushed along the surface normal + // by a fraction of the cascade's texel footprint. A constant depth bias can't keep up + // with large texels (far cascades, small shadow maps) on surfaces at a grazing angle + // to the sun, which then show shadow acne stripes. lightview maps view space metres + // to [0,1] shadow map coordinates, so its x/y row lengths give texels per metre. + vec3 shadownormal = normalize(f_normal); + float shadowNdotL = clamp(dot(shadownormal, normalize(-lights[0].dir)), 0.0, 1.0); + mat4 shadowview = lightview[cascade]; + float texelspermetre = min(length(vec3(shadowview[0][0], shadowview[1][0], shadowview[2][0])), + length(vec3(shadowview[0][1], shadowview[1][1], shadowview[2][1]))) * float(textureSize(shadowmap, 0).x); + float texelsize = 1.0 / texelspermetre; + vec3 shadowpos = f_pos.xyz + shadownormal * (texelsize * 1.5 * (1.0 - shadowNdotL)); + vec4 lightpos = shadowview * vec4(shadowpos, 1.0); + vec3 coords = lightpos.xyz / lightpos.w; + if (coords.z < 0.0) + return 0.0f; + + + + float bias = 0.00005f * float(cascade + 1U); + + // Fast path: 4 textureGather() calls cover a 4x4 texel block. Raw gathered + // comparisons must be weighted by the sub-texel position (bilinear weights), + // otherwise the result is constant across each shadow map texel and shadow + // edges turn into texel-sized stair steps. With per-axis weights + // (1-f, 1, 1, f) this equals a 3x3 grid of hardware bilinear PCF taps. + vec2 size = vec2(textureSize(shadowmap, 0).xy); + vec2 uv = coords.xy * size - 0.5; + vec2 base = floor(uv); + vec2 f = uv - base; + float refz = coords.z + bias; + float layer = float(cascade); + // sampling at texel corners selects blocks with lower-left texel base-1 and base+1 + vec4 g00 = textureGather(shadowmap, vec3((base + vec2(0.0, 0.0)) / size, layer), refz); + vec4 g10 = textureGather(shadowmap, vec3((base + vec2(2.0, 0.0)) / size, layer), refz); + vec4 g01 = textureGather(shadowmap, vec3((base + vec2(0.0, 2.0)) / size, layer), refz); + vec4 g11 = textureGather(shadowmap, vec3((base + vec2(2.0, 2.0)) / size, layer), refz); + // gather component order: x=(0,1) y=(1,1) z=(1,0) w=(0,0) + float shadow = dot(g00, vec4(1.0 - f.x, 1.0, 1.0 - f.y, (1.0 - f.x) * (1.0 - f.y))) + + dot(g10, vec4(1.0, f.x, f.x * (1.0 - f.y), 1.0 - f.y)) + + dot(g01, vec4((1.0 - f.x) * f.y, f.y, 1.0, 1.0 - f.x)) + + dot(g11, vec4(f.y, f.x * f.y, f.x, 1.0)); + return shadow / 9.0; + } + else + { + return 0.0; + } +} + +// ----------------------------------------------------------------------- +// GGX Microfacet BRDF helpers (Cook-Torrance) +// ----------------------------------------------------------------------- + +// Trowbridge-Reitz (GGX) Normal Distribution Function +// D(N,H,α) = α⁴ / (π · ((NdotH)²·(α⁴−1)+1)²) +// α = roughness² (perceptual remapping so the slider feels linear) +float D_GGX(float NdotH, float roughness) +{ + float a = roughness * roughness; // perceptual -> linear roughness + float a2 = a * a; + float d = (NdotH * NdotH) * (a2 - 1.0) + 1.0; + return a2 / (3.14159265359 * d * d); +} + +// Schlick-GGX single-term masking/shadowing (k remapped for direct lighting) +float G_SchlickGGX(float NdotX, float roughness) +{ + float r = roughness + 1.0; + float k = (r * r) * (1.0 / 8.0); // k_direct = (roughness+1)²/8 + return NdotX / (NdotX * (1.0 - k) + k); +} + +// Separable Smith geometry term (not height-correlated) +// G(N,V,L) = G_SchlickGGX(NdotV) · G_SchlickGGX(NdotL) +float G_Smith(float NdotV, float NdotL, float roughness) +{ + return G_SchlickGGX(NdotV, roughness) * G_SchlickGGX(NdotL, roughness); +} + +const float BRDF_PI = 3.14159265359; +// the smoothest surface the lights are reflected by: a mirror would make a highlight smaller than a pixel +const float MIN_ROUGHNESS = 0.045; +// cap of the brdf for the lamps and headlights (see specular_light()): a tiny light next to a polished surface +const float LOCAL_LIGHT_BRDF_LIMIT = 16.0; +// no cap for the sun +const float SUN_BRDF_LIMIT = 1.0e6; + +// the specular reflectance at normal incidence and the perceptual roughness the lights are reflected with, worked out by +// apply_lights() for the fragment before it adds the lamps (add_local_lights()) +vec3 spec_f0 = vec3(0.04); +float spec_roughness = 0.5; + +// a matte material (mat_default) reflects nothing: neither the surroundings nor the sun and the lamps, only the diffuse light +bool matte = false; + +// light of a light from Light_dir reflected by the surface, in the units of the diffuse shading (a white lambertian surface +// facing the light reflects 1 of it): pi times the cook-torrance brdf with schlick's fresnel of F0, times n.l. Limit caps the +// brdf without the n.l +vec3 specular_light(vec3 light_dir, vec3 fragnormal, vec3 f0, float roughness, float limit) +{ + float NdotL = dot(fragnormal, light_dir); + if (NdotL <= 0.0) + return vec3(0.0); + vec3 V = normalize(-f_pos.xyz); + vec3 H = normalize(light_dir + V); + float NdotV = max(dot(fragnormal, V), 1e-4); + float NdotH = max(dot(fragnormal, H), 0.0); + float VdotH = max(dot(V, H), 0.0); + float brdf = D_GGX(NdotH, roughness) * G_Smith(NdotV, NdotL, roughness) / (4.0 * NdotL * NdotV); + vec3 fresnel = f0 + (1.0 - f0) * pow(1.0 - VdotH, 5.0); + return fresnel * (min(brdf, limit) * NdotL * BRDF_PI); +} + +// perceptual roughness raised where the normal changes much from a pixel to the next (normal maps, tight curves), so the +// highlight of a detail smaller than a pixel spreads over the pixel instead of flickering. Kaplanyan, Tokuyoshi, "Improved +// geometric specular antialiasing", 2019 +float specular_antialiasing(vec3 fragnormal, float roughness) +{ + vec3 dndx = dFdx(fragnormal); + vec3 dndy = dFdy(fragnormal); + float variance = 0.25 * (dot(dndx, dndx) + dot(dndy, dndy)); + float kernel = min(2.0 * variance, 0.18); + float a2 = roughness * roughness * roughness * roughness; + return sqrt(sqrt(clamp(a2 + kernel, 0.0, 1.0))); +} + +// the materials without a specgloss map give glossiness: (param[1].w, 10 by default) as the exponent of the blinn-phong +// highlight of the old renderer. a lobe of exponent n is about a ggx one of alpha sqrt(2 / (n + 2)) +float phong_roughness(float exponent) +{ + return sqrt(sqrt(2.0 / (max(exponent, 0.0) + 2.0))); +} + +// glossiness which apply_lights() turns into the roughness of a blinn-phong exponent (it takes 1 - glossiness / param[1].w) +float phong_glossiness(float exponent) +{ + return (1.0 - phong_roughness(exponent)) * max(abs(param[1].w), 1.0); +} + +// the reflection of the surroundings of a material without a specgloss map is a smooth varnish over the rough surface: the +// old renderer showed it as a sharp reflection, as much of it as reflection: (and the masks of the material) asked for +bool legacy_coat = false; + +// set up the gloss of a material without a specgloss map, before apply_lights() +void legacy_gloss() +{ + glossiness = phong_glossiness(abs(param[1].w)); + legacy_coat = true; +} + +// Returns vec2(diffuse, specular) for a single punctual light. +// +// diffuse – Lambert N·L (Fresnel-weighted diffuse is handled per-material +// in apply_lights, so we return raw N·L here). +// specular – Cook-Torrance GGX: D·G / (4·NdotL·NdotV). +// The Fresnel factor (F) is intentionally omitted here; +// apply_lights already carries a per-material Fresnel term +// that is applied to env reflections and can be routed to +// direct specular there. +// +// Roughness is derived identically to env_roughness in apply_lights so +// that direct and indirect specular highlights read consistently. +vec2 calc_light(vec3 light_dir, vec3 fragnormal) +{ + vec3 N = fragnormal; + vec3 L = light_dir; + vec3 V = normalize(-f_pos.xyz); + vec3 H = normalize(L + V); + + float NdotL = max(dot(N, L), 0.0); + float NdotV = max(dot(N, V), 1e-4); + float NdotH = max(dot(N, H), 0.0); + + float diffuse_v = NdotL; + + // Mirror the env-map roughness derivation so direct and indirect lobes match. + // glossiness == param[1].w → roughness == 0.04 (near-mirror) + // glossiness == 0 → roughness == 1.0 (fully diffuse) + float roughness = clamp(1.0 - glossiness / max(abs(param[1].w), 1.0), 0.04, 1.0); + + // Cook-Torrance specular (no Fresnel — see above): + // f_spec = D(N,H,α) · G(N,V,L,α) / (4 · NdotL · NdotV) + float D = D_GGX(NdotH, roughness); + float G = G_Smith(NdotV, NdotL, roughness); + float specular_v = (NdotL > 0.0) + ? (D * G) / max(4.0 * NdotL * NdotV, 1e-4) + : 0.0; + + return vec2(diffuse_v, specular_v); +} + +vec2 calc_point_light(light_s light, vec3 fragnormal) +{ + vec3 light_dir = normalize(light.pos - f_pos.xyz); + vec2 val = calc_light(light_dir, fragnormal); + val.x += light.ambient; + val *= light.intensity; + + float distance = length(light.pos - f_pos.xyz); + float atten = 1.0f / (distance * distance); + //float atten = 1.0f / (1.0f + light.linear * distance + light.quadratic * (distance * distance)); + + return val * atten; +} + +vec2 calc_spot_light(light_s light, vec3 fragnormal) +{ + vec3 light_dir = normalize(light.pos - f_pos.xyz); + + float theta = dot(light_dir, normalize(-light.dir)); + float epsilon = light.in_cutoff - light.out_cutoff; + float intensity = clamp((theta - light.out_cutoff) / epsilon, 0.0, 1.0); + + vec2 point = calc_point_light(light, fragnormal); + return point * intensity; +} + +vec2 calc_dir_light(light_s light, vec3 fragnormal) +{ + vec3 light_dir = normalize(-light.dir); + return calc_light(light_dir, fragnormal); +} + +// 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 +vec2 calc_headlights(light_s light, vec3 fragnormal, vec3 dpdx, vec3 dpdy) +{ + vec4 headlightpos = light.headlight_projection * f_pos; + vec3 coords = headlightpos.xyz / headlightpos.w; + + if (coords.z > 1.0) + return vec2(0.0); + if (coords.z < 0.0) + return vec2(0.0); + // the beam is what the projection takes in. beyond its sides the texture would be clamped to its edge, and near the projector + // (where a pixel spans many texels) its coarsest levels spill the average of the whole beam over the surroundings + if (coords.x < 0.0 || coords.x > 1.0 || coords.y < 0.0 || coords.y > 1.0) + return vec2(0.0); + + vec3 light_dir = normalize(light.pos - f_pos.xyz); + // Tighter wrap (was +0.25): faces angled away from the headlight cone + // fall off to dark much faster, so cab/exterior surfaces read with a + // clear directional shape instead of a flat half-lit wash. + vec2 part = vec2(1.0) * clamp(dot(fragnormal, light_dir) + 0.10, 0.0, 1.0); + float distance = length(light.pos - f_pos.xyz); + float atten = 1.0f / (1.0f + light.linear * distance + light.quadratic * (distance * distance)); + atten *= mix(1.0, 0.0, clamp((coords.z - 0.998) * 500.0, 0.0, 1.0)); + vec2 beamcoords = headlightpos.xy / headlightpos.w; + vec4 dheadlightposdx = light.headlight_projection * vec4(dpdx, 0.0); + vec4 dheadlightposdy = light.headlight_projection * vec4(dpdy, 0.0); + vec2 dbeamdx = (dheadlightposdx.xy - beamcoords * dheadlightposdx.w) / headlightpos.w; + 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); + // 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); +} + +// 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; + +// 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 +vec2 calc_local_light(light_s light, vec3 fragnormal) +{ + vec3 tolight = light.pos - f_pos.xyz; + float distance2 = dot(tolight, tolight); + vec3 light_dir = tolight * inversesqrt(max(distance2, 1e-8)); + float edge = clamp(1.0 - distance2 * distance2 * (light.quadratic * light.quadratic), 0.0, 1.0); + 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) + return vec2(0.0); + return vec2(max(dot(fragnormal, light_dir), 0.0), 1.0) * atten; +} + +// index of the light cluster the fragment is in, see gl/cluster_grid.h: its screen tile and slice of depth. the depth is the clip +// space w, which is 1 / gl_FragCoord.w. a tile is kept in the grid, as the far edge of the render target and rounding +// right below it give one past the last +uint fragment_cluster() +{ + ivec2 tile = ivec2(gl_FragCoord.xy * (vec2(CLUSTER_GRID_X, CLUSTER_GRID_Y) / cluster_viewport_size)); + tile = clamp(tile, ivec2(0), ivec2(CLUSTER_GRID_X - 1, CLUSTER_GRID_Y - 1)); + int slice = clamp(int(floor(cluster_slice_bias - log(gl_FragCoord.w) * cluster_slice_scale)), 0, CLUSTER_GRID_Z - 1); + return uint(tile.x + CLUSTER_GRID_X * (tile.y + CLUSTER_GRID_Y * slice)); +} + +// adds the light of a headlight or lamp to the diffuse and the specular light of the fragment +void add_local_light(light_s light, vec2 part, vec3 fragnormal, inout vec3 diffuse, inout vec3 specular) +{ + if (part.y <= 0.0) + return; + vec3 color = light.color * light.intensity; + diffuse += color * part.x; + if (matte) + return; + specular += color * (part.y * param[1].y) * specular_light(normalize(light.pos - f_pos.xyz), fragnormal, spec_f0, spec_roughness, LOCAL_LIGHT_BRDF_LIMIT); +} + +// adds the light of the headlights and lamps which reach the fragment to its diffuse and specular light. these are the ones in +// the list of its cluster when the lights are clustered (the sun is not on the list, the callers take care of it), and the +// first lights_count - 1 of lights[] otherwise, which are headlights only. the specular light is reflected with spec_f0 and +// spec_roughness, and is already coloured by the fresnel term +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); + + if (lights_clustered != 0U) + { + cluster_range_s range = cluster_ranges[fragment_cluster()]; + for (uint i = 0U; i < range.count; i++) + { + light_s light = lights[cluster_light_indices[range.offset + i]]; + vec2 part = (light.type == LIGHT_HEADLIGHTS) ? calc_headlights(light, fragnormal, dpdx, dpdy) : calc_local_light(light, fragnormal); + add_local_light(light, part, fragnormal, diffuse, specular); + } + } + else + { + for (uint i = 1U; i < lights_count; i++) + { + light_s light = lights[i]; + add_local_light(light, calc_headlights(light, fragnormal, dpdx, dpdy), fragnormal, diffuse, specular); + } + } +} + +// the light of the headlights and lamps added to Fragcolor, diffuse and specular together (for shaders which don't tell +// the albedo from the reflection) +vec3 add_headlights(vec3 fragcolor, vec3 fragnormal) +{ + vec3 diffuse = vec3(0.0); + vec3 specular = vec3(0.0); + add_local_lights(fragnormal, diffuse, specular); + return fragcolor + diffuse + specular; +} + +// ----------------------------------------------------------------------- +// Split-sum environment BRDF (Karis / UE4 analytic approximation). +// +// This is the missing piece that made matte specgloss surfaces "shine like +// crazy": previously the env reflection was added at full strength +// (envcolor * fresnel * reflectivity) and roughness only blurred the mip, +// never dimmed the energy. A rough surface therefore mirrored the bright +// sky just as strongly as a polished one. +// +// EnvBRDFApprox returns the pre-integrated specular scale (the "DFG" term) +// for a given F0, roughness and view angle. For rough surfaces it collapses +// toward ~0, so low-glossiness materials reflect almost nothing — matching +// what Substance 3D Painter shows. Polished surfaces keep their full +// reflection, and the grazing-angle Fresnel edge is preserved. +// roughness 1.0 (matte) -> scale ~0.015 (virtually no reflection) +// roughness 0.0 (mirror) -> scale ~F0..1 (full reflection + Fresnel rim) +vec3 EnvBRDFApprox(vec3 F0, float roughness, float NoV) +{ + const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022); + const vec4 c1 = vec4( 1.0, 0.0425, 1.040, -0.040); + vec4 r = roughness * c0 + c1; + float a004 = min(r.x * r.x, exp2(-9.28 * NoV)) * r.x + r.y; + vec2 AB = vec2(-1.04, 1.04) * a004 + r.zw; + return F0 * AB.x + AB.y; +} + +// Diffuse colour of the submodel (t3d/e3d) or shape, used as the albedo multiplier: +// untextured materials take it as their colour, textured ones multiply the base texture by it +// (same as the legacy renderer's GL_MODULATE). param[0].rgb holds the authored sRGB colour, +// param[1].x its intensity (HSV value unless a material overrides it). +vec3 material_diffuse() +{ + vec3 diffusecolor = pow(max(param[0].rgb, vec3(0.0)), vec3(2.2)); + float value = max(param[0].r, max(param[0].g, param[0].b)); + return (value > 0.0 ? diffusecolor * (param[1].x / value) : 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 +// reflectance of 4% at normal incidence), Reflectivity how much of the reflected surroundings is the cubemap rather than the +// ambient light, Shadowtone the share of the sun left in its shadow (1 for the materials which take no shadows). +// a Sunless surface (the interior of a cab) gets no light of the sun, its shadow only darkens the ambient light +vec3 shade_surface(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float reflectivity, float specularity, float shadowtone, bool sunless) +{ + // Diffuse colour is applied once, to the albedo; the light terms below are not scaled by it again + texturecolor *= material_diffuse(); + float polished = 0.0; + if (sunless) + { + // Cab interior: dim ambient less than the exterior path - ambient + // is the dominant indoor illumination, but it was still too bright. + fragcolor *= 0.80; + } + else + { +#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); + // a tint only scales the colour of the texture, the steel keeps its own polish + polished = weathered * smoothstep(0.03, 0.12, dot(texturecolor, vec3(0.2126, 0.7152, 0.0722))); + float weatheredluma = min(dot(texturecolor, vec3(0.2126, 0.7152, 0.0722)), 0.05); + vec3 unused = texturecolor * (weatheredluma / max(dot(texturecolor, vec3(0.2126, 0.7152, 0.0722)), 1e-4)); + unused = mix(unused, vec3(1.0, 0.82, 0.74) * (weatheredluma / 0.8525), 0.5); + texturecolor = mix(texturecolor, unused, smoothstep(0.0, 0.4, weathered)); + texturecolor = (tinted > 0.0 ? texturecolor * mix(vec3(1.0), param[2].rgb, tinted) : texturecolor); + specularity = mix(specularity, min(specularity, 0.03), smoothstep(0.0, 0.4, polished)); + metalic = mix(metalic, 0.0, polished); + glossiness = mix(glossiness, min(glossiness, 0.06 * abs(param[1].w)), polished); + reflectivity = mix(reflectivity, min(reflectivity, 0.08 * param[1].z), smoothstep(0.0, 0.4, polished)); +#endif + // Scale ambient before it gets tinted by the texture. + // Sun, headlights and emission are added afterwards so they are NOT + // attenuated by AMBIENT_SCALE - this only dims the indirect term. + fragcolor *= AMBIENT_SCALE; + } + + vec3 emissioncolor = vec3(emission); + vec3 view_dir = normalize(-f_pos.xyz); + float NdotV = max(dot(fragnormal, view_dir), 1e-4); + + // glossiness == param[1].w -> mirror, glossiness == 0 -> fully rough + float roughness = clamp(1.0 - glossiness / max(abs(param[1].w), 1.0), 0.0, 1.0); + roughness = max(specular_antialiasing(fragnormal, roughness), MIN_ROUGHNESS); + // metals reflect with their base colour, dielectrics with the specular level (0.5 -> 0.04) + vec3 f0 = mix(vec3(0.08 * specularity), texturecolor, metalic); + spec_f0 = f0; + spec_roughness = roughness; + + // Pre-integrated env BRDF: how much of the surroundings the surface reflects, for its F0, roughness and the view angle. + // the surroundings are the cubemap where the material asks for it (reflectivity) and the ambient light elsewhere, so a + // surface reflects them always, the rough one barely + vec3 env_spec = EnvBRDFApprox(f0, roughness, NdotV); + float env_roughness = roughness; + vec3 cubemap_spec = env_spec; + float cubemap_share = max(reflectivity, 0.0); + float ambient_share = clamp(1.0 - reflectivity, 0.0, 1.0); + if (matte) + { + env_spec = vec3(0.0); + cubemap_share = 0.0; + } + // what the diffuse layer is left with + vec3 diffuse_albedo = texturecolor * (1.0 - metalic) * (1.0 - env_spec); + if (legacy_coat) + { + // the varnish: a smooth dielectric layer reflecting the cubemap, worn off where the material is weathered + env_roughness = specular_antialiasing(fragnormal, roughness * smoothstep(0.0, 0.4, polished)); + cubemap_spec = EnvBRDFApprox(mix(vec3(0.04), texturecolor, metalic), env_roughness, NdotV); + ambient_share = 1.0; + diffuse_albedo *= 1.0 - cubemap_spec * min(cubemap_share, 1.0); + } + vec3 envcolor = (matte ? vec3(0.0) : envmap_color_rough(fragnormal, env_roughness)); + + if (lights_count == 0U) + return fragcolor * diffuse_albedo + + emissioncolor * texturecolor + + fragcolor * env_spec * ambient_share + + envcolor * cubemap_spec * cubemap_share; + + vec3 light_dir = normalize(-lights[0].dir); + float NdotL = max(dot(fragnormal, light_dir), 0.0); + // Sharpen sun N.L falloff so the lit-to-shaded terminator on cab + // 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; + // 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); + + float shadow = 0.0; + if (shadowtone < 1.0) + shadow = clamp(calc_shadow(), 0.0, 1.0); + + // the cubemap knows nothing of what shades the fragment: it is dimmed a little in the shadow of the sun + float cubemap_shadow = 1.0; + if (sunless) + { + // the sun adds no light in the cab, where it would fall its shadow darkens the ambient light + float diffuseamount = sun_NdotL * lights[0].intensity; + float darkness = (shadowtone < 1.0 ? max(shadow, 1.0 - cloudsun) : 0.0); + fragcolor = mix(fragcolor, fragcolor * shadowtone, clamp(diffuseamount * darkness, 0.0, 1.0)); + } + else + cubemap_shadow = 1.0 - (1.0 - shadowtone) * shadow * 0.5; + + // the reflection of the surroundings, before the light of the sun and the lamps joins the ambient light + vec3 env_reflection = fragcolor * env_spec * ambient_share + + envcolor * cubemap_spec * (cubemap_share * cubemap_shadow); + + // the shadow leaves the share of the sun Shadowtone tells (the old shadow colour of the scenery) to the diffuse light, + // nothing to the highlight + if (!sunless) + fragcolor += sunlight * (sun_NdotL * (1.0 - (1.0 - shadowtone) * shadow)); + vec3 specular = matte ? vec3(0.0) : sunlight * specular_light(light_dir, fragnormal, f0, roughness, SUN_BRDF_LIMIT) * (1.0 - shadow); + + vec3 lampspecular = vec3(0.0); + add_local_lights(fragnormal, fragcolor, lampspecular); + + return fragcolor * diffuse_albedo // ambient, sun and lamps on the diffuse layer + + specular * param[1].y // highlight of the sun, fresnel coloured (metals take their base colour) + + lampspecular // highlights of the lamps, param[1].y already applied + + emissioncolor * texturecolor + + env_reflection; +} + +vec3 apply_lights(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float reflectivity, float specularity, float shadowtone) +{ + return shade_surface(fragcolor, fragnormal, texturecolor, reflectivity, specularity, shadowtone, false); +} + +// the interior of a cab: no light of the sun, its shadow darkens the ambient light +vec3 apply_lights_sunless(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float reflectivity, float specularity, float shadowtone) +{ + return shade_surface(fragcolor, fragnormal, texturecolor, reflectivity, specularity, shadowtone, true); +} diff --git a/shaders/vk14/mat_colored.frag b/shaders/vk14/mat_colored.frag new file mode 100644 index 00000000..e57ab700 --- /dev/null +++ b/shaders/vk14/mat_colored.frag @@ -0,0 +1,4 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +#include "mat_colored.glsl" diff --git a/shaders/vk14/mat_colored.glsl b/shaders/vk14/mat_colored.glsl new file mode 100644 index 00000000..864c8b13 --- /dev/null +++ b/shaders/vk14/mat_colored.glsl @@ -0,0 +1,60 @@ +layout(location = 0) in vec3 f_normal; +layout(location = 2) in vec2 f_coord; +layout(location = 3) in vec4 f_pos; + +layout(location = 4) in vec4 f_clip_pos; +layout(location = 5) in vec4 f_clip_future_pos; + +#include "common.glsl" + +#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, zero) +#pragma param(glossiness, 1, 3, 1, glossiness) + +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() +{ + // no texture: the diffuse colour alone makes the albedo, apply_lights() applies it to the white base + vec4 tex_color = vec4(1.0, 1.0, 1.0, param[0].a); + +// if (tex_color.a < opacity) +// discard; + + vec3 fragcolor = ambient; + vec3 fragnormal = normalize(f_normal); + float reflectivity = param[1].z; + float specularity = dot(pow(param[0].rgb, vec3(2.2)), vec3(0.5)); + legacy_gloss(); + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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_default.frag b/shaders/vk14/mat_default.frag new file mode 100644 index 00000000..77f2be7b --- /dev/null +++ b/shaders/vk14/mat_default.frag @@ -0,0 +1,4 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +#include "mat_default.glsl" diff --git a/shaders/vk14/mat_default.glsl b/shaders/vk14/mat_default.glsl new file mode 100644 index 00000000..e2ff0da4 --- /dev/null +++ b/shaders/vk14/mat_default.glsl @@ -0,0 +1,69 @@ +layout(location = 0) in vec3 f_normal; +layout(location = 2) in vec2 f_coord; +layout(location = 3) in vec4 f_pos; + +layout(location = 4) in vec4 f_clip_pos; +layout(location = 5) in vec4 f_clip_future_pos; + +#include "common.glsl" + +#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, zero) +#pragma param(glossiness, 1, 3, 1, glossiness) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +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() +{ + vec4 tex_color = texture(diffuse, f_coord); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; +// if (tex_color.a < opacity) +// discard; + + vec3 fragcolor = ambient; + vec3 fragnormal = normalize(f_normal); + matte = true; + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, 0.0, 0.0, shadow_tone); + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * alpha_mult); +/* + float distance = dot(f_pos.xyz, f_pos.xyz); + if( distance <= cascade_end.x ) { color.r += 0.25; } + else if( distance <= cascade_end.y ) { color.g += 0.25; } + else if( distance <= cascade_end.z ) { color.b += 0.25; } +*/ + 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, tex_color.a * alpha_mult); + } +} diff --git a/shaders/vk14/mat_default_0.frag b/shaders/vk14/mat_default_0.frag new file mode 100644 index 00000000..e57ab700 --- /dev/null +++ b/shaders/vk14/mat_default_0.frag @@ -0,0 +1,4 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +#include "mat_colored.glsl" diff --git a/shaders/vk14/mat_default_1.frag b/shaders/vk14/mat_default_1.frag new file mode 100644 index 00000000..77f2be7b --- /dev/null +++ b/shaders/vk14/mat_default_1.frag @@ -0,0 +1,4 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +#include "mat_default.glsl" diff --git a/shaders/vk14/mat_default_2.frag b/shaders/vk14/mat_default_2.frag new file mode 100644 index 00000000..0ca0b85f --- /dev/null +++ b/shaders/vk14/mat_default_2.frag @@ -0,0 +1,4 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +#include "mat_reflmap.glsl" diff --git a/shaders/vk14/mat_default_detail.frag b/shaders/vk14/mat_default_detail.frag new file mode 100644 index 00000000..1768e4dd --- /dev/null +++ b/shaders/vk14/mat_default_detail.frag @@ -0,0 +1,77 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +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; + + +#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, zero) +#pragma param(glossiness, 1, 3, 1, glossiness) +#pragma param(detail_scale, 2, 0, 1, one) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +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 + +#pragma texture(detailnormalmap, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D detailnormalmap; + +#define NORMALMAP +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +void main() +{ + vec4 tex_color = texture(diffuse, f_coord); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; +// if (tex_color.a < opacity) +// discard; + + vec3 fragcolor = ambient; + + vec3 normal; + normal.xy = (texture(detailnormalmap, f_coord * param[2].x).rg * 2.0 - 1.0); + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + vec3 fragnormal = normalize(f_tbn * normalize(normal.xyz)); + float reflectivity = param[1].z; + float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; + legacy_gloss(); + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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, tex_color.a * alpha_mult); + } +} diff --git a/shaders/vk14/mat_default_specgloss.frag b/shaders/vk14/mat_default_specgloss.frag new file mode 100644 index 00000000..44f40059 --- /dev/null +++ b/shaders/vk14/mat_default_specgloss.frag @@ -0,0 +1,77 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +layout(location = 0) in vec3 f_normal; +layout(location = 2) in vec2 f_coord; +layout(location = 3) in vec4 f_pos; + +layout(location = 4) in vec4 f_clip_pos; +layout(location = 5) in vec4 f_clip_future_pos; + + +#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, zero) +#pragma param(glossiness, 1, 3, 1, glossiness) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +#pragma texture(specgloss, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D specgloss; + +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() +{ + vec4 tex_color = texture(diffuse, f_coord); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; +// if (tex_color.a < opacity) +// discard; + + vec3 fragcolor = ambient; + vec3 fragnormal = normalize(f_normal); + float reflectivity = param[1].z; + float specularity = texture(specgloss, f_coord).r; + glossiness = texture(specgloss, f_coord).g * abs(param[1].w); + metalic = texture(specgloss, f_coord).b; + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * alpha_mult); +/* + float distance = dot(f_pos.xyz, f_pos.xyz); + if( distance <= cascade_end.x ) { color.r += 0.25; } + else if( distance <= cascade_end.y ) { color.g += 0.25; } + else if( distance <= cascade_end.z ) { color.b += 0.25; } +*/ + 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, tex_color.a * alpha_mult); + } +} diff --git a/shaders/vk14/mat_detail_normalmap.frag b/shaders/vk14/mat_detail_normalmap.frag new file mode 100644 index 00000000..0b0e6e9e --- /dev/null +++ b/shaders/vk14/mat_detail_normalmap.frag @@ -0,0 +1,89 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +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; + + +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 + +#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) +#pragma param(detail_scale, 2, 0, 1, one) +#pragma param(detail_height_scale, 2, 1, 1, one) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +#pragma texture(normalmap, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D normalmap; + +#pragma texture(detailnormalmap, 2, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D detailnormalmap; + +#define NORMALMAP +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +void main() +{ + vec4 tex_color = texture(diffuse, f_coord); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; + + vec3 fragcolor = ambient; + + vec4 normal_map = texture(normalmap, f_coord); + vec4 detailnormal_map = texture(detailnormalmap, f_coord * param[2].x); + vec3 normal; + vec3 normaldetail; + + normaldetail.xy = detailnormal_map.rg* 2.0 - 1.0; + normaldetail.z = sqrt(1.0 - clamp((dot(normaldetail.xy, normaldetail.xy)), 0.0, 1.0)); + normaldetail.xyz = normaldetail.xyz * param[2].y; + normal.xy = normal_map.rg* 2.0 - 1.0; + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + + vec3 fragnormal = normalize(f_tbn * normalize(vec3(normal.xy + normaldetail.xy, normal.z))); + + + float reflblend = (normal_map.a + detailnormal_map.a); + float reflectivity = reflblend > 1.0 ? param[1].z * 1.0 : param[1].z * reflblend; + float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; + legacy_gloss(); + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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_detail_normalmap_specgloss.frag b/shaders/vk14/mat_detail_normalmap_specgloss.frag new file mode 100644 index 00000000..31ca750c --- /dev/null +++ b/shaders/vk14/mat_detail_normalmap_specgloss.frag @@ -0,0 +1,96 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +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; + + +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 + +#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) +#pragma param(detail_scale, 2, 0, 1, one) +#pragma param(detail_height_scale, 2, 1, 1, one) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +#pragma texture(normalmap, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D normalmap; + +#pragma texture(detailnormalmap, 2, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D detailnormalmap; + +#pragma texture(specgloss, 3, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 3) uniform sampler2D specgloss; + +#define NORMALMAP +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +void main() +{ + vec4 tex_color = texture(diffuse, f_coord); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; +// if (tex_color.a < opacity) +// discard; + + vec3 fragcolor = ambient; + + vec4 normal_map = texture(normalmap, f_coord); + vec4 detailnormal_map = texture(detailnormalmap, f_coord * param[2].x); + vec4 specgloss_map = texture(specgloss, f_coord); + vec3 normal; + vec3 normaldetail; + + normaldetail.xy = detailnormal_map.rg* 2.0 - 1.0; + normaldetail.z = sqrt(1.0 - clamp((dot(normaldetail.xy, normaldetail.xy)), 0.0, 1.0)); + normaldetail.xyz = normaldetail.xyz * param[2].y; + normal.xy = normal_map.rg* 2.0 - 1.0; + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + + vec3 fragnormal = normalize(f_tbn * normalize(vec3(normal.xy + normaldetail.xy, normal.z))); + + float reflblend = (normal_map.a + detailnormal_map.a); + float reflectivity = reflblend > 1.0 ? param[1].z * 1.0 : param[1].z * reflblend; + float specularity = specgloss_map.r; + glossiness = specgloss_map.g * abs(param[1].w); + metalic = specgloss_map.b; + + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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_detail_parallax.frag b/shaders/vk14/mat_detail_parallax.frag new file mode 100644 index 00000000..75f7a84e --- /dev/null +++ b/shaders/vk14/mat_detail_parallax.frag @@ -0,0 +1,152 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +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; //tangent matrix nietransponowany; mnożyć przez f_tbn dla TangentLightPos; TangentViewPos; TangentFragPos; +layout(location = 4) in vec4 f_clip_pos; +layout(location = 5) in vec4 f_clip_future_pos; +layout(location = 6) in vec3 TangentFragPos; + + +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 + + +#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) +#pragma param(detail_scale, 2, 0, 1, one) +#pragma param(detail_height_scale, 2, 1, 1, one) +#pragma param(height_scale, 2, 2, 1, zero) +#pragma param(height_offset, 2, 3, 1, zero) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +#pragma texture(normalmap, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D normalmap; + +#pragma texture(detailnormalmap, 2, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D detailnormalmap; + +#define PARALLAX +// param[2].w is the height offset, not the weathering +#define NO_WEATHERING +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +vec2 ParallaxMapping(vec2 f_coord, vec3 viewDir); + +void main() +{ + //parallax mapping + vec3 viewDir = normalize(-TangentFragPos); //tangent view pos - tangent frag pos + vec2 f_coord_p = ParallaxMapping(f_coord, viewDir); + vec4 normal_map = texture(normalmap, f_coord_p); + vec4 detailnormal_map = texture(detailnormalmap, f_coord_p * param[2].x); + vec4 tex_color = texture(diffuse, f_coord_p); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; +// if (tex_color.a < opacity) +// discard; + + vec3 fragcolor = ambient; + + vec3 normal; + vec3 normaldetail; + + normaldetail.xy = detailnormal_map.rg* 2.0 - 1.0; + normaldetail.z = sqrt(1.0 - clamp((dot(normaldetail.xy, normaldetail.xy)), 0.0, 1.0)); + normaldetail.xyz = normaldetail.xyz * param[2].y; + normal.xy = normal_map.rg* 2.0 - 1.0; + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + + vec3 fragnormal = normalize(f_tbn * normalize(vec3(normal.xy + normaldetail.xy, normal.z))); + float reflectivity = param[1].z * normal_map.a; + float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; + legacy_gloss(); + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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); + } +} + +vec2 ParallaxMapping(vec2 f_coord, vec3 viewDir) +{ + float pos_len = length(f_pos.xyz); + // gradients of the coordinates taken here: inside the loop the fragments of a quad don't go the same way + vec2 dcoorddx = dFdx(f_coord); + vec2 dcoorddy = dFdy(f_coord); + + if (pos_len > 100.0) { + return f_coord; + } + // the shift fades out before the distance it stops at, so it doesn't switch off at once there + float fade = 1.0 - smoothstep(80.0, 100.0, pos_len); + + if (EXTRAEFFECTS_ENABLED) + { + const float minLayers = 8.0; + const float maxLayers = 32.0; + float LayersWeight = pos_len / 20.0; + vec2 currentTexCoords = f_coord; + float currentDepthMapValue = textureGrad(normalmap, currentTexCoords, dcoorddx, dcoorddy).b; + LayersWeight = min(abs(dot(vec3(0.0, 0.0, 1.0), viewDir)),LayersWeight); + float numLayers = mix(maxLayers, minLayers, clamp(LayersWeight, 0.0, 1.0)); // number of depth layers + float layerDepth = 1.0 / numLayers; // calculate the size of each layer + float currentLayerDepth = 0.0; // depth of current layer + vec2 P = viewDir.xy * param[2].z * fade; // the amount to shift the texture coordinates per layer (from vector P) + vec2 deltaTexCoords = P / numLayers; + + + while(currentLayerDepth < currentDepthMapValue) + { + currentTexCoords -= deltaTexCoords; // shift texture coordinates along direction of P + currentDepthMapValue = textureGrad(normalmap, currentTexCoords, dcoorddx, dcoorddy).b; // get depthmap value at current texture coordinates + currentLayerDepth += layerDepth; // get depth of next layer + } + + vec2 prevTexCoords = currentTexCoords + deltaTexCoords; // get texture coordinates before collision (reverse operations) + + float afterDepth = currentDepthMapValue - currentLayerDepth; // get depth after and before collision for linear interpolation + float beforeDepth = textureGrad(normalmap, prevTexCoords, dcoorddx, dcoorddy).b - currentLayerDepth + layerDepth; + + float weight = afterDepth / (afterDepth - beforeDepth); // interpolation of texture coordinates + vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight); + + return finalTexCoords; + } + else + { + float height = texture(normalmap, f_coord).b; + vec2 p = viewDir.xy / viewDir.z * (height * (param[2].z - param[2].w) * 0.2) * fade; + return f_coord - p; + } +} diff --git a/shaders/vk14/mat_detail_parallax_specgloss.frag b/shaders/vk14/mat_detail_parallax_specgloss.frag new file mode 100644 index 00000000..d4b34b19 --- /dev/null +++ b/shaders/vk14/mat_detail_parallax_specgloss.frag @@ -0,0 +1,158 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +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; //tangent matrix nietransponowany; mnożyć przez f_tbn dla TangentLightPos; TangentViewPos; TangentFragPos; +layout(location = 4) in vec4 f_clip_pos; +layout(location = 5) in vec4 f_clip_future_pos; +layout(location = 6) in vec3 TangentFragPos; + + +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 + + +#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) +#pragma param(detail_scale, 2, 0, 1, one) +#pragma param(detail_height_scale, 2, 1, 1, one) +#pragma param(height_scale, 2, 2, 1, zero) +#pragma param(height_offset, 2, 3, 1, zero) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +#pragma texture(normalmap, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D normalmap; + +#pragma texture(specgloss, 2, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D specgloss; + +#pragma texture(detailnormalmap, 3, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 3) uniform sampler2D detailnormalmap; + + +#define PARALLAX +// param[2].w is the height offset, not the weathering +#define NO_WEATHERING +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +vec2 ParallaxMapping(vec2 f_coord, vec3 viewDir); + +void main() +{ + //parallax mapping + vec3 viewDir = normalize(vec3(0.0f, 0.0f, 0.0f) - TangentFragPos); //tangent view pos - tangent frag pos + vec2 f_coord_p = ParallaxMapping(f_coord, viewDir); + + vec4 normal_map = texture(normalmap, f_coord_p); + vec4 detailnormal_map = texture(detailnormalmap, f_coord_p * param[2].x); + vec4 tex_color = texture(diffuse, f_coord_p); + vec4 specgloss_map = texture(specgloss, f_coord_p); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; + + vec3 fragcolor = ambient; + + vec3 normal; + vec3 normaldetail; + + normaldetail.xy = detailnormal_map.rg* 2.0 - 1.0; + normaldetail.z = sqrt(1.0 - clamp((dot(normaldetail.xy, normaldetail.xy)), 0.0, 1.0)); + normaldetail.xyz = normaldetail.xyz * param[2].y; + normal.xy = normal_map.rg* 2.0 - 1.0; + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + + vec3 fragnormal = normalize(f_tbn * normalize(vec3(normal.xy + normaldetail.xy, normal.z))); + float reflectivity = param[1].z * normal_map.a; + float specularity = specgloss_map.r; + glossiness = specgloss_map.g * abs(param[1].w); + metalic = specgloss_map.b; + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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); + } +} + +vec2 ParallaxMapping(vec2 f_coord, vec3 viewDir) +{ + + float pos_len = length(f_pos.xyz); + // gradients of the coordinates taken here: inside the loop the fragments of a quad don't go the same way + vec2 dcoorddx = dFdx(f_coord); + vec2 dcoorddy = dFdy(f_coord); + + if (pos_len > 100.0) { + return f_coord; + } + // the shift fades out before the distance it stops at, so it doesn't switch off at once there + float fade = 1.0 - smoothstep(80.0, 100.0, pos_len); + + if (EXTRAEFFECTS_ENABLED) + { + const float minLayers = 8.0; + const float maxLayers = 32.0; + float LayersWeight = pos_len / 20.0; + vec2 currentTexCoords = f_coord; + float currentDepthMapValue = textureGrad(normalmap, currentTexCoords, dcoorddx, dcoorddy).b; + LayersWeight = min(abs(dot(vec3(0.0, 0.0, 1.0), viewDir)),LayersWeight); + float numLayers = mix(maxLayers, minLayers, clamp(LayersWeight, 0.0, 1.0)); // number of depth layers + float layerDepth = 1.0 / numLayers; // calculate the size of each layer + float currentLayerDepth = 0.0; // depth of current layer + vec2 P = viewDir.xy * param[2].z * fade; // the amount to shift the texture coordinates per layer (from vector P) + vec2 deltaTexCoords = P / numLayers; + + + while(currentLayerDepth < currentDepthMapValue) + { + currentTexCoords -= deltaTexCoords; // shift texture coordinates along direction of P + currentDepthMapValue = textureGrad(normalmap, currentTexCoords, dcoorddx, dcoorddy).b; // get depthmap value at current texture coordinates + currentLayerDepth += layerDepth; // get depth of next layer + } + + vec2 prevTexCoords = currentTexCoords + deltaTexCoords; // get texture coordinates before collision (reverse operations) + + float afterDepth = currentDepthMapValue - currentLayerDepth; // get depth after and before collision for linear interpolation + float beforeDepth = textureGrad(normalmap, prevTexCoords, dcoorddx, dcoorddy).b - currentLayerDepth + layerDepth; + + float weight = afterDepth / (afterDepth - beforeDepth); // interpolation of texture coordinates + vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight); + + return finalTexCoords; + } + else + { + float height = texture(normalmap, f_coord).b; + vec2 p = viewDir.xy / viewDir.z * (height * (param[2].z - param[2].w) * 0.2) * fade; + return f_coord - p; + } +} diff --git a/shaders/vk14/mat_invalid.frag b/shaders/vk14/mat_invalid.frag new file mode 100644 index 00000000..ff4682de --- /dev/null +++ b/shaders/vk14/mat_invalid.frag @@ -0,0 +1,29 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +#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() +{ + vec4 color = vec4(1.0, 0.0, 1.0, 1.0); + if (POSTFX_ENABLED) + { + out_color = color; + } + else + { + out_color = tonemap(color); + } + if (MOTIONBLUR_ENABLED) + { + out_motion = vec4(0.0f); + } +} diff --git a/shaders/vk14/mat_normalmap.frag b/shaders/vk14/mat_normalmap.frag new file mode 100644 index 00000000..e96054b6 --- /dev/null +++ b/shaders/vk14/mat_normalmap.frag @@ -0,0 +1,76 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +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; + + +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 + +#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) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +#pragma texture(normalmap, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D normalmap; + +#define NORMALMAP +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +void main() +{ + vec4 tex_color = texture(diffuse, f_coord); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; +// if (tex_color.a < opacity) +// discard; + + vec3 fragcolor = ambient; + + vec3 normal; + normal.xy = (texture(normalmap, f_coord).rg * 2.0 - 1.0); + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + vec3 fragnormal = normalize(f_tbn * normalize(normal.xyz)); + float reflectivity = param[1].z * texture(normalmap, f_coord).a; + float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; + legacy_gloss(); + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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_normalmap_specgloss.frag b/shaders/vk14/mat_normalmap_specgloss.frag new file mode 100644 index 00000000..474a77b0 --- /dev/null +++ b/shaders/vk14/mat_normalmap_specgloss.frag @@ -0,0 +1,81 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +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; + + +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 + +#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) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +#pragma texture(normalmap, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D normalmap; + +#pragma texture(specgloss, 2, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D specgloss; + + +#define NORMALMAP +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +void main() +{ + vec4 tex_color = texture(diffuse, f_coord); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; +// if (tex_color.a < opacity) +// discard; + + vec3 fragcolor = ambient; + + vec3 normal; + normal.xy = (texture(normalmap, f_coord).rg * 2.0 - 1.0); + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + vec3 fragnormal = normalize(f_tbn * normalize(normal.xyz)); + float reflectivity = param[1].z * texture(normalmap, f_coord).a; + float specularity = texture(specgloss, f_coord).r; + glossiness = texture(specgloss, f_coord).g * abs(param[1].w); + metalic = texture(specgloss, f_coord).b; + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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_parallax.frag b/shaders/vk14/mat_parallax.frag new file mode 100644 index 00000000..4154ad47 --- /dev/null +++ b/shaders/vk14/mat_parallax.frag @@ -0,0 +1,137 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +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; //tangent matrix nietransponowany; mnożyć przez f_tbn dla TangentLightPos; TangentViewPos; TangentFragPos; +layout(location = 4) in vec4 f_clip_pos; +layout(location = 5) in vec4 f_clip_future_pos; +layout(location = 6) in vec3 TangentFragPos; + + +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 + +#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) +#pragma param(height_scale, 2, 1, 1, zero) +#pragma param(height_offset, 2, 2, 1, zero) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +#pragma texture(normalmap, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D normalmap; + +#define PARALLAX +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +vec2 ParallaxMapping(vec2 f_coord, vec3 viewDir); + +void main() +{ + //parallax mapping + vec3 viewDir = normalize(vec3(0.0f, 0.0f, 0.0f) - TangentFragPos); //tangent view pos - tangent frag pos + vec2 f_coord_p = ParallaxMapping(f_coord, viewDir); + + vec4 tex_color = texture(diffuse, f_coord_p); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; +// if (tex_color.a < opacity) +// discard; + + vec3 fragcolor = ambient; + + vec3 normal; + normal.xy = (texture(normalmap, f_coord_p).rg * 2.0 - 1.0); + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + vec3 fragnormal = normalize(f_tbn * normalize(normal.xyz)); + float reflectivity = param[1].z * texture(normalmap, f_coord_p).a; + float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; + legacy_gloss(); + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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); + } +} + +vec2 ParallaxMapping(vec2 f_coord, vec3 viewDir) +{ + float pos_len = length(f_pos.xyz); + // gradients of the coordinates taken here: inside the loop the fragments of a quad don't go the same way + vec2 dcoorddx = dFdx(f_coord); + vec2 dcoorddy = dFdy(f_coord); + + if (pos_len > 100.0) { + return f_coord; + } + // the shift fades out before the distance it stops at, so it doesn't switch off at once there + float fade = 1.0 - smoothstep(80.0, 100.0, pos_len); + + if (EXTRAEFFECTS_ENABLED) + { + const float minLayers = 8.0; + const float maxLayers = 32.0; + float LayersWeight = pos_len / 20.0; + vec2 currentTexCoords = f_coord; + float currentDepthMapValue = textureGrad(normalmap, currentTexCoords, dcoorddx, dcoorddy).b; + LayersWeight = min(abs(dot(vec3(0.0, 0.0, 1.0), viewDir)),LayersWeight); + float numLayers = mix(maxLayers, minLayers, clamp(LayersWeight, 0.0, 1.0)); // number of depth layers + float layerDepth = 1.0 / numLayers; // calculate the size of each layer + float currentLayerDepth = 0.0; // depth of current layer + vec2 P = viewDir.xy * param[2].y * fade; // the amount to shift the texture coordinates per layer (from vector P) + vec2 deltaTexCoords = P / numLayers; + + + while(currentLayerDepth < currentDepthMapValue) + { + currentTexCoords -= deltaTexCoords; // shift texture coordinates along direction of P + currentDepthMapValue = textureGrad(normalmap, currentTexCoords, dcoorddx, dcoorddy).b; // get depthmap value at current texture coordinates + currentLayerDepth += layerDepth; // get depth of next layer + } + + vec2 prevTexCoords = currentTexCoords + deltaTexCoords; // get texture coordinates before collision (reverse operations) + + float afterDepth = currentDepthMapValue - currentLayerDepth; // get depth after and before collision for linear interpolation + float beforeDepth = textureGrad(normalmap, prevTexCoords, dcoorddx, dcoorddy).b - currentLayerDepth + layerDepth; + + float weight = afterDepth / (afterDepth - beforeDepth); // interpolation of texture coordinates + vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight); + + return finalTexCoords; + } + else + { + float height = texture(normalmap, f_coord).b; + vec2 p = viewDir.xy / viewDir.z * (height * (param[2].y - param[2].z) * 0.2) * fade; + return f_coord - p; + } +} diff --git a/shaders/vk14/mat_parallax_specgloss.frag b/shaders/vk14/mat_parallax_specgloss.frag new file mode 100644 index 00000000..1a20d972 --- /dev/null +++ b/shaders/vk14/mat_parallax_specgloss.frag @@ -0,0 +1,142 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +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; //tangent matrix nietransponowany; mnożyć przez f_tbn dla TangentLightPos; TangentViewPos; TangentFragPos; +layout(location = 4) in vec4 f_clip_pos; +layout(location = 5) in vec4 f_clip_future_pos; +layout(location = 6) in vec3 TangentFragPos; + + +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 + +#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) +#pragma param(height_scale, 2, 1, 1, zero) +#pragma param(height_offset, 2, 2, 1, zero) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +#pragma texture(normalmap, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D normalmap; + +#pragma texture(specgloss, 2, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D specgloss; + + +#define PARALLAX +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +vec2 ParallaxMapping(vec2 f_coord, vec3 viewDir); + +void main() +{ + //parallax mapping + vec3 viewDir = normalize(vec3(0.0f, 0.0f, 0.0f) - TangentFragPos); //tangent view pos - tangent frag pos + vec2 f_coord_p = ParallaxMapping(f_coord, viewDir); + + vec4 tex_color = texture(diffuse, f_coord_p); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; +// if (tex_color.a < opacity) +// discard; + + vec3 fragcolor = ambient; + + vec3 normal; + normal.xy = (texture(normalmap, f_coord_p).rg * 2.0 - 1.0); + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + vec3 fragnormal = normalize(f_tbn * normalize(normal.xyz)); + float reflectivity = param[1].z * texture(normalmap, f_coord_p).a; + float specularity = texture(specgloss, f_coord_p).r; + glossiness = texture(specgloss, f_coord_p).g * abs(param[1].w); + metalic = texture(specgloss, f_coord_p).b; + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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); + } +} + +vec2 ParallaxMapping(vec2 f_coord, vec3 viewDir) +{ + float pos_len = length(f_pos.xyz); + // gradients of the coordinates taken here: inside the loop the fragments of a quad don't go the same way + vec2 dcoorddx = dFdx(f_coord); + vec2 dcoorddy = dFdy(f_coord); + + if (pos_len > 100.0) { + return f_coord; + } + // the shift fades out before the distance it stops at, so it doesn't switch off at once there + float fade = 1.0 - smoothstep(80.0, 100.0, pos_len); + + if (EXTRAEFFECTS_ENABLED) + { + const float minLayers = 8.0; + const float maxLayers = 32.0; + float LayersWeight = pos_len / 20.0; + vec2 currentTexCoords = f_coord; + float currentDepthMapValue = textureGrad(normalmap, currentTexCoords, dcoorddx, dcoorddy).b; + LayersWeight = min(abs(dot(vec3(0.0, 0.0, 1.0), viewDir)),LayersWeight); + float numLayers = mix(maxLayers, minLayers, clamp(LayersWeight, 0.0, 1.0)); // number of depth layers + float layerDepth = 1.0 / numLayers; // calculate the size of each layer + float currentLayerDepth = 0.0; // depth of current layer + vec2 P = viewDir.xy * param[2].y * fade; // the amount to shift the texture coordinates per layer (from vector P) + vec2 deltaTexCoords = P / numLayers; + + + while(currentLayerDepth < currentDepthMapValue) + { + currentTexCoords -= deltaTexCoords; // shift texture coordinates along direction of P + currentDepthMapValue = textureGrad(normalmap, currentTexCoords, dcoorddx, dcoorddy).b; // get depthmap value at current texture coordinates + currentLayerDepth += layerDepth; // get depth of next layer + } + + vec2 prevTexCoords = currentTexCoords + deltaTexCoords; // get texture coordinates before collision (reverse operations) + + float afterDepth = currentDepthMapValue - currentLayerDepth; // get depth after and before collision for linear interpolation + float beforeDepth = textureGrad(normalmap, prevTexCoords, dcoorddx, dcoorddy).b - currentLayerDepth + layerDepth; + + float weight = afterDepth / (afterDepth - beforeDepth); // interpolation of texture coordinates + vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight); + + return finalTexCoords; + } + else + { + float height = texture(normalmap, f_coord).b; + vec2 p = viewDir.xy / viewDir.z * (height * (param[2].y - param[2].z) * 0.2) * fade; + return f_coord - p; + } +} diff --git a/shaders/vk14/mat_rain_windscreen.frag b/shaders/vk14/mat_rain_windscreen.frag new file mode 100644 index 00000000..341620de --- /dev/null +++ b/shaders/vk14/mat_rain_windscreen.frag @@ -0,0 +1,216 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +// Windshield rain effects +// by @lcddisplay; wiper movement @MichauSto + +layout(location = 0) in vec3 f_normal; +layout(location = 2) in vec2 f_coord; +layout(location = 3) in vec4 f_pos; + +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 + +#pragma texture(diffuse, 0, sRGB_A) +#pragma texture(raindropsatlas, 1, sRGB_A) +#pragma texture(wipermask, 2, sRGB_A) +#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, zero) +#pragma param(glossiness, 1, 3, 1, glossiness) +#pragma param(raindrop_grid_size, 2, 0, 1, one) + +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" +#include "random.glsl" + +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D raindropsatlas; +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D wipermask; + +const float specular_intensity = 1.0; +const float wobble_strength = 0.002; +const float wobble_speed = 30.0; + +vec4 getDropTex(float choice, vec2 uv) { + vec2 offset; + if (choice < 0.25) offset = vec2(0.0, 0.0); + else if (choice < 0.5) offset = vec2(0.5, 0.0); + else if (choice < 0.75) offset = vec2(0.0, 0.5); + else offset = vec2(0.5, 0.5); + return texture(raindropsatlas, offset + uv * 0.5); +} + +float GetMixFactor(in vec2 co, out float side); + +void main() { + vec4 tex_color = texture(diffuse, f_coord); + if (tex_color.a < 0.01) discard; + + vec2 rainCoord = f_coord; + float gridSize = ceil(param[2].x); + + const float numDrops = 20000.0; + const float cycleDuration = 4.0; + + float squareMin = 0.5 / gridSize; + float squareMax = 1.2 / gridSize; + + vec2 cell = floor(rainCoord * gridSize); + + vec3 dropLayer = vec3(0.0); + float dropMaskSum = 0.0; + + // Grid of 9 droplets in immediate neighbourhood + for (int oy = -1; oy <= 1; oy++) { + for (int ox = -1; ox <= 1; ox++) { + + vec2 neighborCell = cell + vec2(ox, oy); + vec2 neighborCenter = (neighborCell + .5) / gridSize; + + float side; + float mixFactor = GetMixFactor(neighborCenter, side); + + uint seed = Hash(uvec3(neighborCell, side)); + + if(mixFactor < RandF(seed)) { + continue; + } + + // Show a percentage of droplets given by rain intensity param + float activationSeed = RandF(seed); + if (activationSeed > rain_params.x) + continue; // kropla nieaktywna + + // Randomly modulate droplet center & size + vec2 dropCenter = (neighborCell + vec2(RandF(seed), RandF(seed))) / gridSize; + float squareSize = mix(squareMin, squareMax, RandF(seed)); + + float lifeTime = time + RandF(seed) * cycleDuration; + float phase = fract(lifeTime / cycleDuration); + float actiwe = clamp(1.0 - phase, 0.0, 1.0); + + // Gravity influence (TODO add vehicle speed & wind here!) + float gravityStart = 0.5; + float gravityPhase = smoothstep(gravityStart, 1.0, phase); + float dropMass = mix(0.3, 1.2, RandF(seed)); + float gravitySpeed = 0.15 * dropMass; + vec2 gravityOffset = vec2(0.0, gravityPhase * gravitySpeed * phase); + + // Random wobble + bool hasWobble = (RandF(seed) < 0.10); + vec2 wobbleOffset = vec2(0.0); + if (hasWobble && gravityPhase > 0.0) { + float intensity = sin(time * wobble_speed + RandF(seed) * 100.) * wobble_strength * gravityPhase; + wobbleOffset = vec2(intensity, 0.0); + } + + vec2 slideOffset = gravityOffset + wobbleOffset; + + // Flatten droplets influenced by gravity + float flattenAmount = smoothstep(0.1, 0.5, gravityPhase); + float flattenX = mix(1.0, 0.4, flattenAmount); + float stretchY = mix(1.0, 1.6, flattenAmount); + + // Droplet local position & mask + vec2 diff = (rainCoord + slideOffset) - dropCenter; + diff.x *= 1.0 / flattenX; + diff.y *= 1.0 / stretchY; + float mask = smoothstep(squareSize * 0.5, squareSize * 0.45, max(abs(diff.x), abs(diff.y))); + + if (mask > 0.001) { + vec2 localUV = (diff + squareSize * 0.5) / squareSize; + float choice = RandF(seed); + vec4 dropTex = getDropTex(choice, localUV); + float sharpAlpha = smoothstep(0.3, 0.9, dropTex.a); + + float ambLum = clamp(dot(ambient, vec3(0.2126, 0.7152, 0.0722)), 0.0, 1.0); + float sunFactor = pow(clamp(ambLum * 6.0, 0.0, 1.0), 0.5); + float dynBright = mix(0.0, 1.0, sunFactor); + + float colorLuma = length(dropTex.rgb); + float alphaRange = smoothstep(0.1, 0.3, colorLuma); + float blackAlpha = mix(0.25, 0.85, alphaRange); + + float sparkle = 0.0; + if (hasWobble) { + sparkle = pow(abs(sin(f_coord.x * 8.0 + time * 2.0 + RandF(seed) * 6.2831)), 40.0) + * mix(0.2, 1.0, sunFactor); + } + + vec3 specularColor = vec3(1.0) * specular_intensity * sparkle; + vec3 dropLit = dropTex.rgb * dynBright + specularColor * sharpAlpha; + + dropLayer += dropLit * sharpAlpha * actiwe * blackAlpha * mask; + dropMaskSum += sharpAlpha * actiwe * blackAlpha * mask; + } + } + } + vec3 finalMix = dropLayer; + float alphaOut = clamp(dropMaskSum, 0.0, 1.0); + out_color = vec4(finalMix, alphaOut); + + { // Overlay windshield texture with alpha + vec3 fragcolor = ambient; + vec3 fragnormal = normalize(f_normal); + float reflectivity = param[1].z; + float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; + legacy_gloss(); + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + vec4 color = vec4(fragcolor, tex_color.a * alpha_mult); + + out_color.xyz = apply_fog(mix(out_color.xyz, color.xyz, color.a)); + out_color.a = mix(out_color.a, 1., color.a); + } + + if (MOTIONBLUR_ENABLED) + { + out_motion = vec4(0.0); + } +} + +float GetMixFactor(in vec2 co, out float side) { + vec4 movePhase = wiper_pos; + bvec4 is_out = lessThanEqual(movePhase, vec4(1.)); + movePhase = mix(vec4(2.) - movePhase, movePhase, is_out); + + vec4 mask = texture(wipermask, co); + + vec4 areaMask = step(.001, mask); + + vec4 maskVal = mix(1. - mask, mask, is_out); + vec4 wipeWidth = smoothstep(1., .9, movePhase) * .25; + vec4 cleaned = smoothstep(movePhase - wipeWidth, movePhase, maskVal) * areaMask; + vec4 side_v = step(maskVal, movePhase); + cleaned *= side_v; + side_v = mix(side_v, vec4(1.) - side_v, is_out); + + // "regeneration", raindrops gradually returning after wiper pass: + vec4 regenPhase = clamp((vec4(time) - mix(wiper_timer_out, wiper_timer_return, side_v) - vec4(.2)) / vec4(rain_params.y), vec4(0.), vec4(1.)); + + side_v = mix(vec4(0.), vec4(1.) - side_v, areaMask); + + vec4 factor_v = mix(vec4(1.), regenPhase * (vec4(1.) - cleaned), areaMask); + + side = 0.; + float out_factor = 1.; + + // Find out the wiper blade that influences given grid cell the most + for(int i = 0; i < 4; ++i) + { + bool is_candidate = factor_v[i] < out_factor; + out_factor = mix(out_factor, factor_v[i], is_candidate); + side = mix(side, side_v[i], is_candidate); + } + + return out_factor; +} diff --git a/shaders/vk14/mat_reflmap.frag b/shaders/vk14/mat_reflmap.frag new file mode 100644 index 00000000..0ca0b85f --- /dev/null +++ b/shaders/vk14/mat_reflmap.frag @@ -0,0 +1,4 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +#include "mat_reflmap.glsl" diff --git a/shaders/vk14/mat_reflmap.glsl b/shaders/vk14/mat_reflmap.glsl new file mode 100644 index 00000000..b4260785 --- /dev/null +++ b/shaders/vk14/mat_reflmap.glsl @@ -0,0 +1,72 @@ +layout(location = 0) in vec3 f_normal; +layout(location = 2) in vec2 f_coord; +layout(location = 3) in vec4 f_pos; + +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 + +#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) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +#pragma texture(reflmap, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D reflmap; + +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +void main() +{ + vec4 tex_color = texture(diffuse, f_coord); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; +// if (tex_color.a < opacity) +// discard; + + vec3 fragcolor = ambient; + vec3 fragnormal = normalize(f_normal); + float reflectivity = param[1].z * texture(reflmap, f_coord).a; + float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; + legacy_gloss(); + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, 0.0, shadow_tone); + + if(alphatestfail) + fragcolor.r = 1.0; + + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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_reflmap_specgloss.frag b/shaders/vk14/mat_reflmap_specgloss.frag new file mode 100644 index 00000000..bfe44f44 --- /dev/null +++ b/shaders/vk14/mat_reflmap_specgloss.frag @@ -0,0 +1,79 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +layout(location = 0) in vec3 f_normal; +layout(location = 2) in vec2 f_coord; +layout(location = 3) in vec4 f_pos; + +layout(location = 4) in vec4 f_clip_pos; +layout(location = 5) in vec4 f_clip_future_pos; + + +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 + +#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) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +#pragma texture(reflmap, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D reflmap; + +#pragma texture(specgloss, 2, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D specgloss; + + +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +void main() +{ + vec4 tex_color = texture(diffuse, f_coord); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; +// if (tex_color.a < opacity) +// discard; + + vec3 fragcolor = ambient; + vec3 fragnormal = normalize(f_normal); + float reflectivity = param[1].z * texture(reflmap, f_coord).a; + float specularity = texture(specgloss, f_coord).r; + glossiness = texture(specgloss, f_coord).g * abs(param[1].w); + metalic = texture(specgloss, f_coord).b; + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + + if(alphatestfail) + fragcolor.r = 1.0; + + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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_shadowlessnormalmap.frag b/shaders/vk14/mat_shadowlessnormalmap.frag new file mode 100644 index 00000000..5b64cebb --- /dev/null +++ b/shaders/vk14/mat_shadowlessnormalmap.frag @@ -0,0 +1,75 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +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; + + +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 + +#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, zero) +#pragma param(glossiness, 1, 3, 1, glossiness) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +#pragma texture(normalmap, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D normalmap; + +#define NORMALMAP +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +void main() +{ + vec4 tex_color = texture(diffuse, f_coord); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; +// if (tex_color.a < opacity) +// discard; + + vec3 fragcolor = ambient; + + vec3 normal; + normal.xy = (texture(normalmap, f_coord).rg * 2.0 - 1.0); + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + vec3 fragnormal = normalize(f_tbn * normalize(normal.xyz)); + float reflectivity = param[1].z * texture(normalmap, f_coord).a; + float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; + legacy_gloss(); + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, 1.0); + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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_shadowlessnormalmap_specgloss.frag b/shaders/vk14/mat_shadowlessnormalmap_specgloss.frag new file mode 100644 index 00000000..519256b0 --- /dev/null +++ b/shaders/vk14/mat_shadowlessnormalmap_specgloss.frag @@ -0,0 +1,80 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +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; + + +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 + +#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, zero) +#pragma param(glossiness, 1, 3, 1, glossiness) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +#pragma texture(normalmap, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D normalmap; + +#pragma texture(specgloss, 2, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D specgloss; + + +#define NORMALMAP +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +void main() +{ + vec4 tex_color = texture(diffuse, f_coord); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; +// if (tex_color.a < opacity) +// discard; + + vec3 fragcolor = ambient; + + vec3 normal; + normal.xy = (texture(normalmap, f_coord).rg * 2.0 - 1.0); + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + vec3 fragnormal = normalize(f_tbn * normalize(normal.xyz)); + float reflectivity = param[1].z * texture(normalmap, f_coord).a; + float specularity = texture(specgloss, f_coord).r; + glossiness = texture(specgloss, f_coord).g * abs(param[1].w); + metalic = texture(specgloss, f_coord).b; + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, 1.0); + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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_stars.frag b/shaders/vk14/mat_stars.frag new file mode 100644 index 00000000..99076ad6 --- /dev/null +++ b/shaders/vk14/mat_stars.frag @@ -0,0 +1,38 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +#include "tonemapping.glsl" + +layout(location = 1) flat in vec3 f_normal_raw; + +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() +{ + float x = (gl_PointCoord.x - 0.5f) * 2.0f; + float y = (gl_PointCoord.y - 0.5f) * 2.0f; + float dist2 = abs(x * x + y * y); + if (dist2 > 0.5f * 0.5f) + discard; + + // color data space is shared with normals, ugh + vec4 color = vec4(pow(f_normal_raw.rgb, vec3(2.2)), 1.0f); + if (POSTFX_ENABLED) + { + out_color = color; + } + else + { + out_color = tonemap(color); + } + if (MOTIONBLUR_ENABLED) + { + out_motion = vec4(0.0f); + } +} diff --git a/shaders/vk14/mat_sunlessnormalmap.frag b/shaders/vk14/mat_sunlessnormalmap.frag new file mode 100644 index 00000000..fa4b2646 --- /dev/null +++ b/shaders/vk14/mat_sunlessnormalmap.frag @@ -0,0 +1,75 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +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; + + +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 + +#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, zero) +#pragma param(glossiness, 1, 3, 1, glossiness) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +#pragma texture(normalmap, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D normalmap; + +#define NORMALMAP +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +void main() +{ + vec4 tex_color = texture(diffuse, f_coord); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; +// if (tex_color.a < opacity) +// discard; + + vec3 fragcolor = ambient; + + vec3 normal; + normal.xy = (texture(normalmap, f_coord).rg * 2.0 - 1.0); + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + vec3 fragnormal = normalize(f_tbn * normalize(normal.xyz)); + float reflectivity = param[1].z * texture(normalmap, f_coord).a; + float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; + legacy_gloss(); + + fragcolor = apply_lights_sunless(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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_sunlessnormalmap_specgloss.frag b/shaders/vk14/mat_sunlessnormalmap_specgloss.frag new file mode 100644 index 00000000..0741ed27 --- /dev/null +++ b/shaders/vk14/mat_sunlessnormalmap_specgloss.frag @@ -0,0 +1,80 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +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; + + +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 + +#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, zero) +#pragma param(glossiness, 1, 3, 1, glossiness) + +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; + +#pragma texture(normalmap, 1, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D normalmap; + +#pragma texture(specgloss, 2, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D specgloss; + + +#define NORMALMAP +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +void main() +{ + vec4 tex_color = texture(diffuse, f_coord); + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; +// if (tex_color.a < opacity) +// discard; + + vec3 fragcolor = ambient; + + vec3 normal; + normal.xy = (texture(normalmap, f_coord).rg * 2.0 - 1.0); + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + vec3 fragnormal = normalize(f_tbn * normalize(normal.xyz)); + float reflectivity = param[1].z * texture(normalmap, f_coord).a; + float specularity = texture(specgloss, f_coord).r; + glossiness = texture(specgloss, f_coord).g * abs(param[1].w); + metalic = texture(specgloss, f_coord).b; + + fragcolor = apply_lights_sunless(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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_trackblend.frag b/shaders/vk14/mat_trackblend.frag new file mode 100644 index 00000000..6f4c543f --- /dev/null +++ b/shaders/vk14/mat_trackblend.frag @@ -0,0 +1,13 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +// the rails and the ballast of a track faded into those of the tracks joined at its ends, see trackblend.glsl +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; +#pragma texture(diffuse1, 1, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D diffuse1; +#pragma texture(diffuse2, 2, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D diffuse2; +#pragma texture(diffuse3, 3, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 3) uniform sampler2D diffuse3; +#include "trackblend.glsl" diff --git a/shaders/vk14/mat_trackblend_normalmap.frag b/shaders/vk14/mat_trackblend_normalmap.frag new file mode 100644 index 00000000..0abc4a7e --- /dev/null +++ b/shaders/vk14/mat_trackblend_normalmap.frag @@ -0,0 +1,22 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +// the rails and the ballast of a track of a normal mapped material faded into those of the tracks joined at its ends, see trackblend.glsl +#define TRACKBLEND_NORMALMAP 1 +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; +#pragma texture(diffuse1, 1, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D diffuse1; +#pragma texture(diffuse2, 2, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D diffuse2; +#pragma texture(diffuse3, 3, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 3) uniform sampler2D diffuse3; +#pragma texture(normalmap, 4, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 4) uniform sampler2D normalmap; +#pragma texture(normalmap1, 5, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 5) uniform sampler2D normalmap1; +#pragma texture(normalmap2, 6, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 6) uniform sampler2D normalmap2; +#pragma texture(normalmap3, 7, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 7) uniform sampler2D normalmap3; +#include "trackblend.glsl" diff --git a/shaders/vk14/mat_trackblend_parallax.frag b/shaders/vk14/mat_trackblend_parallax.frag new file mode 100644 index 00000000..3b54a69f --- /dev/null +++ b/shaders/vk14/mat_trackblend_parallax.frag @@ -0,0 +1,25 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +// the rails and the ballast of a track of a parallax material faded into those of the tracks joined at its ends, see trackblend.glsl +#define TRACKBLEND_NORMALMAP 1 +#define TRACKBLEND_PARALLAX 1 +#pragma param(height_scale, 2, 1, 1, zero) +#pragma param(height_offset, 2, 2, 1, zero) +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; +#pragma texture(diffuse1, 1, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D diffuse1; +#pragma texture(diffuse2, 2, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D diffuse2; +#pragma texture(diffuse3, 3, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 3) uniform sampler2D diffuse3; +#pragma texture(normalmap, 4, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 4) uniform sampler2D normalmap; +#pragma texture(normalmap1, 5, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 5) uniform sampler2D normalmap1; +#pragma texture(normalmap2, 6, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 6) uniform sampler2D normalmap2; +#pragma texture(normalmap3, 7, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 7) uniform sampler2D normalmap3; +#include "trackblend.glsl" diff --git a/shaders/vk14/mat_trackblend_specgloss.frag b/shaders/vk14/mat_trackblend_specgloss.frag new file mode 100644 index 00000000..617c538b --- /dev/null +++ b/shaders/vk14/mat_trackblend_specgloss.frag @@ -0,0 +1,21 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +// the rails and the ballast of a track of a material with normal and specgloss maps faded into those of the tracks joined at its ends, see trackblend.glsl +#define TRACKBLEND_NORMALMAP 1 +#define TRACKBLEND_SPECGLOSS 1 +#pragma texture(diffuse, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; +#pragma texture(diffuse1, 1, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D diffuse1; +#pragma texture(diffuse2, 2, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D diffuse2; +#pragma texture(normalmap, 3, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 3) uniform sampler2D normalmap; +#pragma texture(normalmap1, 4, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 4) uniform sampler2D normalmap1; +#pragma texture(normalmap2, 5, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 5) uniform sampler2D normalmap2; +#pragma texture(specgloss, 6, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 6) uniform sampler2D specgloss; +#include "trackblend.glsl" diff --git a/shaders/vk14/mat_water.frag b/shaders/vk14/mat_water.frag new file mode 100644 index 00000000..f4d85893 --- /dev/null +++ b/shaders/vk14/mat_water.frag @@ -0,0 +1,98 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +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; + + +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 + +#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) +#pragma param(wave_strength, 2, 1, 1, one) +#pragma param(wave_speed, 2, 2, 1, one) + +#pragma texture(normalmap, 0, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D normalmap; + +#pragma texture(dudvmap, 1, RGB) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D dudvmap; + +#pragma texture(diffuse, 2, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D diffuse; + +//wave distortion variables +float move_factor = 0.0; + +#define WATER +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +void main() +{ + //wave distortion + move_factor += (param[2].z * time); + move_factor = mod(move_factor, 1.0); + vec2 texture_coords = f_coord; + vec2 distorted_tex_coord = texture(dudvmap, vec2(texture_coords.x + move_factor, texture_coords.y)).rg * 0.1; + distorted_tex_coord = texture_coords + vec2(distorted_tex_coord.x , distorted_tex_coord.y + move_factor); + vec2 total_distorted_tex_coord = (texture(dudvmap, distorted_tex_coord).rg * 2.0 - 1.0 ) * param[2].y; + texture_coords += total_distorted_tex_coord; + + vec4 tex_color = texture(diffuse, texture_coords); + + vec3 fragcolor = ambient * 0.25; + + vec3 normal; + normal.xy = (texture(normalmap, texture_coords).rg * 2.0 - 1.0); + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + vec3 fragnormal = normalize(f_tbn * normalize(normal.xyz)); + float reflectivity = param[1].z * texture(normalmap, texture_coords ).a; + // water reflects 2% at normal incidence + float specularity = 0.25; + glossiness = abs(param[1].w); + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + + //fragcolor = mix(fragcolor, param[0].rgb, param[1].z); +// fragcolor = (fragcolor * tex_color.rgb * param[1].z) + (fragcolor * (1.0 - param[1].z)); //multiply + //fragcolor = ( max(fragcolor + param[0].rgb -1.0,0.0) * param[1].z + fragcolor * (1.0 - param[1].z)); //linear burn + //fragcolor = ( min(param[0].rgb,fragcolor) * param[1].z + fragcolor * (1.0 - param[1].z)); //darken + + // fresnel effect + vec3 view_dir = normalize(vec3(0.0f, 0.0f, 0.0f) - f_pos.xyz); + float fresnel = pow(max(dot(normalize(f_normal), view_dir), 0.0), 0.2); + float fresnel_inv = ((fresnel - 1.0 ) * -1.0 ); + + vec4 color = vec4(apply_fog(fragcolor), clamp((fresnel_inv + param[0].a), 0.0, 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/mat_water_specgloss.frag b/shaders/vk14/mat_water_specgloss.frag new file mode 100644 index 00000000..f1a51021 --- /dev/null +++ b/shaders/vk14/mat_water_specgloss.frag @@ -0,0 +1,103 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +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; + + +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 + +#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) +#pragma param(wave_strength, 2, 1, 1, one) +#pragma param(wave_speed, 2, 2, 1, one) + +#pragma texture(normalmap, 0, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D normalmap; + +#pragma texture(dudvmap, 1, RGB) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D dudvmap; + +#pragma texture(diffuse, 2, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D diffuse; + +#pragma texture(specgloss, 3, RGBA) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 3) uniform sampler2D specgloss; + + +//wave distortion variables +float move_factor = 0.0; + +#define WATER +#include "light_common.glsl" +#include "apply_fog.glsl" +#include "tonemapping.glsl" + +void main() +{ + //wave distortion + move_factor += (param[2].z * time); + move_factor = mod(move_factor, 1.0); + vec2 texture_coords = f_coord; + vec2 distorted_tex_coord = texture(dudvmap, vec2(texture_coords.x + move_factor, texture_coords.y)).rg * 0.1; + distorted_tex_coord = texture_coords + vec2(distorted_tex_coord.x , distorted_tex_coord.y + move_factor); + vec2 total_distorted_tex_coord = (texture(dudvmap, distorted_tex_coord).rg * 2.0 - 1.0 ) * param[2].y; + texture_coords += total_distorted_tex_coord; + + vec4 tex_color = texture(diffuse, texture_coords); + + vec3 fragcolor = ambient * 0.25; + + vec3 normal; + normal.xy = (texture(normalmap, texture_coords).rg * 2.0 - 1.0); + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + vec3 fragnormal = normalize(f_tbn * normalize(normal.xyz)); + float reflectivity = param[1].z * texture(normalmap, texture_coords ).a; + vec4 specgloss_map = texture(specgloss, texture_coords); + float specularity = specgloss_map.r; + glossiness = specgloss_map.g * abs(param[1].w); + metalic = specgloss_map.b; + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + + //fragcolor = mix(fragcolor, param[0].rgb, param[1].z); +// fragcolor = (fragcolor * tex_color.rgb * param[1].z) + (fragcolor * (1.0 - param[1].z)); //multiply + //fragcolor = ( max(fragcolor + param[0].rgb -1.0,0.0) * param[1].z + fragcolor * (1.0 - param[1].z)); //linear burn + //fragcolor = ( min(param[0].rgb,fragcolor) * param[1].z + fragcolor * (1.0 - param[1].z)); //darken + + // fresnel effect + vec3 view_dir = normalize(vec3(0.0f, 0.0f, 0.0f) - f_pos.xyz); + float fresnel = pow(max(dot(normalize(f_normal), view_dir), 0.0), 0.2); + float fresnel_inv = ((fresnel - 1.0 ) * -1.0 ); + + vec4 color = vec4(apply_fog(fragcolor), clamp((fresnel_inv + param[0].a), 0.0, 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/pick.frag b/shaders/vk14/pick.frag new file mode 100644 index 00000000..dd4b5eb4 --- /dev/null +++ b/shaders/vk14/pick.frag @@ -0,0 +1,10 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" + +layout(location = 0) out vec4 out_color; + +void main() +{ + out_color = vec4(param[0].rgb, 1.0); +} diff --git a/shaders/vk14/pick_surface.frag b/shaders/vk14/pick_surface.frag new file mode 100644 index 00000000..5b527844 --- /dev/null +++ b/shaders/vk14/pick_surface.frag @@ -0,0 +1,11 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" + +layout(location = 0) out vec4 out_color; +layout(location = 2) in vec2 f_coord; + +void main() +{ + out_color = vec4(param[0].r, f_coord, 1.0); +} diff --git a/shaders/vk14/postfx_chromaticaberration.frag b/shaders/vk14/postfx_chromaticaberration.frag new file mode 100644 index 00000000..c33b8a8e --- /dev/null +++ b/shaders/vk14/postfx_chromaticaberration.frag @@ -0,0 +1,64 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#define LAYOUT_POST 1 +#include "common.glsl" +layout(location = 14) in vec2 f_coords; + +layout(location = 0) out vec4 out_color; + +#pragma texture(color_tex, 0, RGB) + +layout(set = POST_SET, binding = POST_TEXTURE_FIRST + 0) uniform sampler2D iChannel0; + +#include "dither.glsl" + +vec2 barrelDistortion(vec2 f_coords, float amt) { + vec2 cc = f_coords - 0.5; + float dist = dot(cc, cc); + return f_coords + cc * dist * amt; +} + +float sat( float t ) +{ + return clamp( t, 0.0, 1.0 ); +} + +float linterp( float t ) { + return sat( 1.0 - abs( 1.0*t - 1.0 ) ); +} + +float remap( float t, float a, float b ) { + return sat( (t - a) / (b - a) ); +} + +vec4 spectrum_offset( float t ) { + vec4 ret; + float lo = step(t,0.5); + float hi = 1.0-lo; + float w = linterp( remap( t, 1.0/6.0, 5.0/6.0 ) ); + ret = vec4(lo,1.0,hi, 1.) * vec4(1.0-w, w, 1.0-w, 1.); + + return pow( ret, vec4(1.0/2.2) ); +} + +const float max_distort = 0.05; +const int num_iter = 10; +const float reci_num_iter_f = 1.0 / float(num_iter); + +void main() +{ + vec2 uv=f_coords.xy; + + vec4 sumcol = vec4(0.0); + vec4 sumw = vec4(0.0); + for ( int i=0; i 0), mimics the C++ kernel generator +vec3 kernel_sample(int i) { + float fi = float(i); + vec3 dir = normalize(vec3( + fract(sin(fi * 12.9898) * 43758.5453) * 2.0 - 1.0, + fract(sin(fi * 78.2330) * 43758.5453) * 2.0 - 1.0, + fract(sin(fi * 37.7190) * 43758.5453) // z >= 0 -> hemisphere + )); + float len = fract(sin(fi * 94.6720) * 43758.5453); + float scale = fi / 32.0; + scale = mix(0.1, 1.0, scale * scale); // bias samples inward + return dir * len * scale; +} + +void main() { + float d = texture(depth_tex, f_coords).r; + if (d <= 0.0) { out_color = vec4(1.0); return; } // skybox -> no occlusion + + vec3 pos = view_pos_from_depth(f_coords, d); + + vec3 ddx = dFdx(pos); + vec3 ddy = dFdy(pos); + vec3 n = normalize(cross(ddx, ddy)); + + // derive screen size from the depth texture instead of a UBO field + vec2 screen_size = vec2(textureSize(depth_tex, 0)); + vec2 noise_uv = f_coords * (screen_size / 4.0); + vec3 rvec = texture(noise_tex, noise_uv).xyz; + + vec3 t = normalize(rvec - n * dot(rvec, n)); + vec3 b = cross(n, t); + mat3 TBN = mat3(t, b, n); + + const int KERNEL = 32; + const float RADIUS = 0.5; + const float BIAS = 0.025; + + float occ = 0.0; + for (int i = 0; i < KERNEL; ++i) { + vec3 sp = TBN * kernel_sample(i); + sp = pos + sp * RADIUS; + + vec4 clip = projection * vec4(sp, 1.0); + vec3 ndc = clip.xyz / clip.w; + vec2 suv = ndc.xy * 0.5 + 0.5; + + float sd = texture(depth_tex, suv).r; + float szv = view_pos_from_depth(suv, sd).z; + + float range = smoothstep(0.0, 1.0, RADIUS / abs(pos.z - szv)); + occ += (szv >= sp.z + BIAS ? 1.0 : 0.0) * range; + } + + out_color = vec4(1.0 - occ / float(KERNEL)); +} \ No newline at end of file diff --git a/shaders/vk14/postfx_ssao_apply.frag b/shaders/vk14/postfx_ssao_apply.frag new file mode 100644 index 00000000..12b3f48c --- /dev/null +++ b/shaders/vk14/postfx_ssao_apply.frag @@ -0,0 +1,99 @@ +#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 +// close they are and by how close their distance is to that of the pixel, so the occlusion does not spill over the +// edges of objects. each pixel of the occlusion stands for the top left one of the four full size pixels it covers +// params.x: 0 ground truth ambient occlusion, 1 the older ssao (its own fixed curve) +// params.y: strength, a power of the occlusion (1 = as computed); params.z: albedo assumed by the multiple bounce term. +// 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 + +layout(location = 14) in vec2 f_coords; + +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; + +#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) +{ + ivec2 full_size = textureSize(depth_tex, 0); + vec2 uv = (vec2(pixel) + 0.5) / vec2(full_size); + vec4 position = inv_projection * vec4(uv * 2.0 - 1.0, depth, 1.0); + return position.xyz / position.w; +} + +float view_distance(ivec2 pixel, float depth) +{ + return length(view_position(pixel, depth)); +} + +// light which comes back after more than one bounce off surfaces of the given albedo, fit of Jimenez et al. 2016 +float multi_bounce(float visibility, float albedo) +{ + float a = 2.0404 * albedo - 0.3324; + float b = -4.7951 * albedo + 0.6417; + float c = 2.7552 * albedo + 0.6903; + return max(visibility, ((visibility * a + b) * visibility + c) * visibility); +} + +void main() +{ + ivec2 full_size = textureSize(depth_tex, 0); + ivec2 half_size = textureSize(ao_tex, 0); + ivec2 pixel = ivec2(gl_FragCoord.xy); + float depth = texelFetch(depth_tex, pixel, 0).r; + if (depth <= 0.0) + { + // the sky + out_color = vec4(1.0); + return; + } + float distance = view_distance(pixel, depth); + + vec2 position = vec2(pixel) * 0.5; + ivec2 base = ivec2(floor(position)); + vec2 fraction = position - vec2(base); + float sum = 0.0; + float weights = 0.0; + for (int y = 0; y < 2; ++y) + { + for (int x = 0; x < 2; ++x) + { + ivec2 texel = clamp(base + ivec2(x, y), ivec2(0), half_size - 1); + ivec2 texelpixel = min(texel * 2, full_size - 1); + float texeldepth = texelFetch(depth_tex, texelpixel, 0).r; + float texeldistance = (texeldepth > 0.0) ? view_distance(texelpixel, texeldepth) : 1e30; + float bilinear = (x == 0 ? 1.0 - fraction.x : fraction.x) * (y == 0 ? 1.0 - fraction.y : fraction.y); + float weight = (bilinear + 1e-3) / (1e-3 + abs(texeldistance - distance) / distance); + sum += texelFetch(ao_tex, texel, 0).r * weight; + weights += weight; + } + } + float ao = clamp(sum / max(weights, 1e-6), 0.0, 1.0); + + if (params.x > 0.5) + { + // the curve the older ssao was applied with, a floor so the shadows do not go black + ao = mix(0.3, 1.0, pow(ao, 0.8)); + } + else + { + ao = pow(ao, params.y); + ao = multi_bounce(ao, params.z); + } + // 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); + out_color = vec4(vec3(ao), 1.0); +} diff --git a/shaders/vk14/postfx_ssao_blur.frag b/shaders/vk14/postfx_ssao_blur.frag new file mode 100644 index 00000000..159d6577 --- /dev/null +++ b/shaders/vk14/postfx_ssao_blur.frag @@ -0,0 +1,18 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#define LAYOUT_POST 1 +#include "common.glsl" +layout(location = 14) in vec2 f_coords; +layout(location = 0) out vec4 out_color; + +#pragma texture(ssao_tex, 0, R) +layout(set = POST_SET, binding = POST_TEXTURE_FIRST + 0) uniform sampler2D ssao_tex; + +void main() { + vec2 texel = 1.0 / vec2(textureSize(ssao_tex, 0)); + float sum = 0.0; + for (int x = -2; x < 2; ++x) + for (int y = -2; y < 2; ++y) + sum += texture(ssao_tex, f_coords + vec2(x, y) * texel).r; + out_color = vec4(sum / 16.0); +} \ No newline at end of file diff --git a/shaders/vk14/postfx_tonemapping.frag b/shaders/vk14/postfx_tonemapping.frag new file mode 100644 index 00000000..a77ad64e --- /dev/null +++ b/shaders/vk14/postfx_tonemapping.frag @@ -0,0 +1,40 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#define LAYOUT_POST 1 +#include "common.glsl" +layout(location = 14) in vec2 f_coords; + +layout(location = 0) out vec4 out_color; + +#pragma texture(tex1, 0, RGB) +layout(set = POST_SET, binding = POST_TEXTURE_FIRST + 0) uniform sampler2D tex1; + +// glow of the bright parts of the image (postfx_bloom_*.frag) +#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 +// 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() +{ + vec2 texcoord = f_coords; + vec3 hdr_color = texture(tex1, texcoord).xyz; + if (param[0].x > 0.0) + hdr_color = mix(hdr_color, texture(bloom_tex, texcoord).rgb * param[0].w, param[0].x); + float exposurescale = param[0].z; + if (param[0].y > 0.5 && exposure > 0.0) + exposurescale *= exposure; + out_color = tonemap(vec4(hdr_color * exposurescale, 1.0)); + if (param[1].x > 0.5) + out_color.rgb = dither_output(out_color.rgb); +} diff --git a/shaders/vk14/precipitation.frag b/shaders/vk14/precipitation.frag new file mode 100644 index 00000000..a610fe60 --- /dev/null +++ b/shaders/vk14/precipitation.frag @@ -0,0 +1,42 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +layout(location = 2) in vec2 f_coord; + +#pragma texture(tex1, 0, sRGB_A) +layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D tex1; + + +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 + +layout(location = 4) in vec4 f_clip_pos; +layout(location = 5) in vec4 f_clip_future_pos; + +#include "tonemapping.glsl" + +void main() +{ + vec4 tex_color = texture(tex1, vec2(f_coord.x, f_coord.y + param[1].x)); + vec4 color = tex_color * param[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, tex_color.a * alpha_mult); + } +} diff --git a/shaders/vk14/precipitation.vert b/shaders/vk14/precipitation.vert new file mode 100644 index 00000000..3c2e7440 --- /dev/null +++ b/shaders/vk14/precipitation.vert @@ -0,0 +1,20 @@ +#version 460 core +#define STAGE_VERTEX 1 +#include "common.glsl" +layout(location = 0) in vec3 v_vert; +layout(location = 1) in vec2 v_coord; + +layout(location = 2) out vec2 f_coord; + +layout(location = 4) out vec4 f_clip_pos; +layout(location = 5) out vec4 f_clip_future_pos; + + +void main() +{ + f_clip_pos = (projection * modelview) * vec4(v_vert, 1.0f); + f_clip_future_pos = (projection * future * modelview) * vec4(v_vert, 1.0f); + + gl_Position = f_clip_pos; + f_coord = v_coord; +} diff --git a/shaders/vk14/present.frag b/shaders/vk14/present.frag new file mode 100644 index 00000000..26f848c7 --- /dev/null +++ b/shaders/vk14/present.frag @@ -0,0 +1,17 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#define LAYOUT_POST 1 +#include "common.glsl" +// the finished image to the window (with quad.vert). the renderer draws with the matrices of the opengl one, so its images +// have the rows in the memory order of opengl, the bottom of the picture first; the window shows the first row at the top. +// this is the only place the picture is turned upside down. no push constants +layout(location = 14) in vec2 f_coords; + +layout(set = POST_SET, binding = POST_TEXTURE_FIRST + 0) uniform sampler2D tex1; + +layout(location = 0) out vec4 out_color; + +void main() +{ + out_color = FBOUT(texture(tex1, vec2(f_coords.x, 1.0 - f_coords.y))); +} diff --git a/shaders/vk14/quad.vert b/shaders/vk14/quad.vert new file mode 100644 index 00000000..3501f409 --- /dev/null +++ b/shaders/vk14/quad.vert @@ -0,0 +1,27 @@ +#version 460 core +#define STAGE_VERTEX 1 +#define LAYOUT_POST 1 +#include "common.glsl" +const vec2 vert[4] = vec2[] +( + vec2(-1.0, 1.0), + vec2(-1.0, -1.0), + vec2( 1.0, 1.0), + vec2( 1.0, -1.0) +); + +const vec2 uv[4] = vec2[] +( + vec2(0.0, 1.0), + vec2(0.0, 0.0), + vec2(1.0, 1.0), + vec2(1.0, 0.0) +); + +layout(location = 14) out vec2 f_coords; + +void main() +{ + gl_Position = vec4(vert[gl_VertexIndex], 0.0, 1.0); + f_coords = uv[gl_VertexIndex]; +} diff --git a/shaders/vk14/random.glsl b/shaders/vk14/random.glsl new file mode 100644 index 00000000..0b25c987 --- /dev/null +++ b/shaders/vk14/random.glsl @@ -0,0 +1,39 @@ + +uint Hash(uint s); +uint Hash(uvec2 s); +uint Hash(uvec3 s); +uint Hash(float s); +uint HashCombine(uint seed, uint value); + +// Evolving Sub-Grid Turbulence for Smoke Animation +// H. Schechter and R. Bridson +uint Hash(uint s) { + s ^= 2747636419u; + s *= 2654435769u; + s ^= s >> 16u; + s *= 2654435769u; + s ^= s >> 16u; + s *= 2654435769u; + return s; +} + +uint Hash(uvec2 s) { + return HashCombine(Hash(s.x), s.y); +} + +uint Hash(uvec3 s) { + return HashCombine(Hash(s.xy), s.z); +} + +uint HashCombine(uint seed, uint value) { + return seed ^ (Hash(value) + 0x9e3779b9u + (seed<<6u) + (seed>>2u)); +} + +uint Rand(inout uint seed) { + seed = Hash(seed); + return seed; +} + +float RandF(inout uint seed) { + return float(Rand(seed)) * 2.3283064365386963e-10; +} diff --git a/shaders/vk14/settings.glsl b/shaders/vk14/settings.glsl new file mode 100644 index 00000000..0dc53345 --- /dev/null +++ b/shaders/vk14/settings.glsl @@ -0,0 +1,25 @@ +// the settings of the game the shaders depend on (gfx.* in eu07.ini): specialization constants the renderer sets when it makes a +// pipeline, the ids are the SPEC_* of rendering/vulkan14/vulkan14bindings.h. included by common.glsl after the constants +#ifndef SETTINGS_GLSL +#define SETTINGS_GLSL + +#ifndef SPEC_OUTPUT_GAMMA +#error "settings.glsl takes the ids of the specialization constants from common.glsl: include common.glsl instead" +#endif + +layout(constant_id = SPEC_SHADOWMAP_ENABLED) const bool SHADOWMAP_ENABLED = true; +layout(constant_id = SPEC_ENVMAP_ENABLED) const bool ENVMAP_ENABLED = true; +layout(constant_id = SPEC_MOTIONBLUR_ENABLED) const bool MOTIONBLUR_ENABLED = true; +// the colour goes on to the post-processing; tonemapped by the shaders of the scene otherwise +layout(constant_id = SPEC_POSTFX_ENABLED) const bool POSTFX_ENABLED = true; +layout(constant_id = SPEC_EXTRAEFFECTS_ENABLED) const bool EXTRAEFFECTS_ENABLED = true; +// whether the colour written to the window is encoded by the shader (FBOUT): the window has no srgb format (gfx.shadergamma yes) +layout(constant_id = SPEC_OUTPUT_GAMMA) const bool OUTPUT_GAMMA = false; + +// the colour written to the window +vec4 FBOUT(vec4 x) +{ + return (OUTPUT_GAMMA ? vec4(pow(x.rgb, vec3(1.0 / 2.2)), x.a) : x); +} + +#endif diff --git a/shaders/vk14/shadowmap.frag b/shaders/vk14/shadowmap.frag new file mode 100644 index 00000000..d11f89bb --- /dev/null +++ b/shaders/vk14/shadowmap.frag @@ -0,0 +1,8 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +layout(location = 2) in vec2 f_coord; + +void main() +{ +} diff --git a/shaders/vk14/simpleuv.vert b/shaders/vk14/simpleuv.vert new file mode 100644 index 00000000..bd5eebe3 --- /dev/null +++ b/shaders/vk14/simpleuv.vert @@ -0,0 +1,14 @@ +#version 460 core +#define STAGE_VERTEX 1 +#include "common.glsl" +layout(location = 0) in vec3 v_vert; +layout(location = 2) in vec2 v_coord; + +layout(location = 2) out vec2 f_coord; + + +void main() +{ + gl_Position = (projection * effective_modelview()) * vec4(v_vert, 1.0f); + f_coord = v_coord; +} diff --git a/shaders/vk14/sky_shoulder.glsl b/shaders/vk14/sky_shoulder.glsl new file mode 100644 index 00000000..6d045316 --- /dev/null +++ b/shaders/vk14/sky_shoulder.glsl @@ -0,0 +1,16 @@ +// the skydome vertex colours carry Perez luminance scaled by a fixed exposure +// (see CSkyDome::SetExposure), so the circumsolar region and the horizon band +// routinely land well above 1.0. hard-clipping them, as this shader used to do, +// flattened everything within ~20 degrees of the sun - and most of the lower sky - +// into one solid white patch. compress the top end instead: below the knee the +// response is unchanged, above it it rolls off towards SKY_SHOULDER_WHITE, so the +// brightness gradient around the sun survives and the patch keeps a soft edge. +const float SKY_SHOULDER_KNEE = 0.25; +const float SKY_SHOULDER_WHITE = 0.95; + +vec3 sky_shoulder(vec3 color) +{ + const float range = SKY_SHOULDER_WHITE - SKY_SHOULDER_KNEE; + vec3 excess = max(color - vec3(SKY_SHOULDER_KNEE), vec3(0.0)); + return min(color, vec3(SKY_SHOULDER_KNEE)) + range * (vec3(1.0) - exp(-excess / range)); +} diff --git a/shaders/vk14/smoke.frag b/shaders/vk14/smoke.frag new file mode 100644 index 00000000..53306a01 --- /dev/null +++ b/shaders/vk14/smoke.frag @@ -0,0 +1,43 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" +layout(location = 13) in vec4 f_color; +layout(location = 2) in vec2 f_coord; +layout(location = 3) in vec4 f_pos; + +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 "apply_fog.glsl" +#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() +{ + vec4 tex_color = texture(tex1, f_coord) * f_color; + 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/smoke.vert b/shaders/vk14/smoke.vert new file mode 100644 index 00000000..d3ca86de --- /dev/null +++ b/shaders/vk14/smoke.vert @@ -0,0 +1,25 @@ +#version 460 core +#define STAGE_VERTEX 1 +#include "common.glsl" +layout(location = 0) in vec3 v_vert; +layout(location = 1) in vec4 v_color; +layout(location = 2) in vec2 v_coord; + +layout(location = 3) out vec4 f_pos; +layout(location = 13) out vec4 f_color; +layout(location = 2) out vec2 f_coord; + +layout(location = 4) out vec4 f_clip_pos; +layout(location = 5) out vec4 f_clip_future_pos; + + +void main() +{ + f_pos = modelview * vec4(v_vert, 1.0f); + f_clip_pos = (projection * modelview) * vec4(v_vert, 1.0f); + f_clip_future_pos = (projection * future * modelview) * vec4(v_vert, 1.0f); + + gl_Position = f_clip_pos; + f_coord = v_coord; + f_color = v_color; +} diff --git a/shaders/vk14/tonemapping.glsl b/shaders/vk14/tonemapping.glsl new file mode 100644 index 00000000..7f8e10e4 --- /dev/null +++ b/shaders/vk14/tonemapping.glsl @@ -0,0 +1,154 @@ +#include "common.glsl" + +const float pureWhite = 1.0; + +vec3 reinhard(vec3 x) +{ +// return x / (x + vec3(1.0)); + + // Reinhard tonemapping operator. + // see: "Photographic Tone Reproduction for Digital Images", eq. 4 + float luminance = dot(x, vec3(0.2126, 0.7152, 0.0722)); + float mappedLuminance = (luminance * (1.0 + luminance/(pureWhite*pureWhite))) / (1.0 + luminance); + // Scale color by ratio of average luminances. + return (mappedLuminance / luminance) * x; +} + +// https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/ +vec3 ACESFilm(vec3 x) +{ + float a = 2.51f; + float b = 0.03f; + float c = 2.43f; + float d = 0.59f; + float e = 0.14f; + return (x*(a*x+b))/(x*(c*x+d)+e); +} + +// https://www.slideshare.net/ozlael/hable-john-uncharted2-hdr-lighting +vec3 filmicF(vec3 x) +{ + float A = 0.22f; + float B = 0.30f; + float C = 0.10f; + float D = 0.20f; + float E = 0.01f; + float F = 0.30f; + return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F; +} + +vec3 filmic(vec3 x) +{ + return filmicF(x) / filmicF(vec3(11.2f)); +} + + +// AgX tonemapping based on nxrighthere / Missing Deadlines implementation. +// 0: Default, 1: Golden, 2: Punchy +#ifndef AGX_LOOK +#define AGX_LOOK 0 +#endif + +vec3 AgxDefaultContrastApprox(vec3 x) +{ + vec3 x2 = x * x; + vec3 x4 = x2 * x2; + + return 15.5 * x4 * x2 + - 40.14 * x4 * x + + 31.96 * x4 + - 6.868 * x2 * x + + 0.4298 * x2 + + 0.1191 * x + - 0.00232; +} + +vec3 Agx(vec3 val) +{ + mat3 agx_mat = mat3( + 0.842479062253094, 0.0784335999999992, 0.0792237451477643, + 0.0423282422610123, 0.878468636469772, 0.0791661274605434, + 0.0423756549057051, 0.0784336, 0.879142973793104 + ); + + // DEFAULT_LOG2_MIN = -6.0 + // DEFAULT_LOG2_MAX = +4.5 + // MIDDLE_GRAY = 0.18 + // log2(pow(2, VALUE) * MIDDLE_GRAY) + const float min_ev = -8.47393; + const float max_ev = 2.026069; + const float agx_eps = 1e-6; + + // Input transform (inset) + val = agx_mat * val; + + // Log2 space encoding. max() avoids -INF/NaN for zero/negative inputs. + val = clamp(log2(max(val, vec3(agx_eps))), min_ev, max_ev); + val = (val - min_ev) / (max_ev - min_ev); + + // Apply sigmoid function approximation. + return AgxDefaultContrastApprox(val); +} + +vec3 AgxEotf(vec3 val) +{ + mat3 agx_mat_inv = mat3( + 1.19687900512017, -0.0980208811401368, -0.0990297440797205, + -0.0528968517574562, 1.15190312990417, -0.0989611768448433, + -0.0529716355144438, -0.0980434501171241, 1.15107367264116 + ); + + // Inverse input transform (outset) + val = agx_mat_inv * val; + + return max(val, vec3(0.0)); +} + +vec3 AgxLook(vec3 val) +{ + vec3 lw = vec3(0.2126, 0.7152, 0.0722); + float luma = dot(val, lw); + + vec3 offset = vec3(0.0); + vec3 slope = vec3(1.0); + vec3 power = vec3(1.6); + float sat = 0.9; + +#if AGX_LOOK == 1 + // Golden + slope = vec3(1.0, 0.9, 0.5); + power = vec3(0.8); + sat = 0.8; +#elif AGX_LOOK == 2 + // Punchy + slope = vec3(1.0); + power = vec3(1.35); + sat = 1.1; +#endif + + // ASC CDL + val = pow(max(val * slope + offset, vec3(0.0)), power); + return vec3(luma) + sat * (val - vec3(luma)); +} + +vec3 ApplyAgX(vec3 linearColorRec709) +{ + linearColorRec709 = Agx(linearColorRec709); + linearColorRec709 = AgxLook(linearColorRec709); + linearColorRec709 = AgxEotf(linearColorRec709); + return linearColorRec709; +} + +vec4 tonemap(vec4 x) +{ + // Use AgX by default. Reinhard and ACES above are kept for reference. + + // Last-line-of-defense sanitize so NaN/Inf/negative HDR values do not + // escape into log2()/pow() and produce black flashes or invalid output. + vec3 hdr = x.rgb; + hdr = mix(hdr, vec3(0.0), vec3(any(isnan(hdr)) || any(isinf(hdr)))); + hdr = max(hdr, vec3(0.0)); + return FBOUT(vec4(ApplyAgX(hdr), x.a)); + //return FBOUT(vec4(ACESFilm(hdr), x.a)); + //return FBOUT(vec4(reinhard(x.rgb), x.a)); +} diff --git a/shaders/vk14/trackblend.glsl b/shaders/vk14/trackblend.glsl new file mode 100644 index 00000000..dca62ee4 --- /dev/null +++ b/shaders/vk14/trackblend.glsl @@ -0,0 +1,205 @@ +// a track drawn with the textures of the tracks joined at its ends blended in near them (world/TrackBlend.h): the weights of the +// tracks joined at the ends p1, p2 and p4 of a switch in UserData.yzw, their textures in diffuse1..3, normalmap1..3, faded into +// the texture of the track. the including shader declares the textures and sets: +// TRACKBLEND_NORMALMAP with the normal maps of the four, lit as the shader of the material of the track: param[2].x 0 as +// mat_default, 1 normalmap, 2 sunlessnormalmap, 3 shadowlessnormalmap +// TRACKBLEND_SPECGLOSS the specgloss map of the track itself as well, the textures of the ends p1 and p2 alone (no room +// for a third set of them) +// TRACKBLEND_PARALLAX with the normal maps, as mat_parallax: each texture shifted by the heights in its own normal map +layout(location = 0) in vec3 f_normal; +layout(location = 2) in vec2 f_coord; +layout(location = 3) in vec4 f_pos; +#ifdef TRACKBLEND_NORMALMAP +layout(location = 8) in mat3 f_tbn; +#endif +#ifdef TRACKBLEND_PARALLAX +layout(location = 6) in vec3 TangentFragPos; +#endif + +layout(location = 4) in vec4 f_clip_pos; +layout(location = 5) in vec4 f_clip_future_pos; + +#include "common.glsl" + +#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, zero) +#pragma param(glossiness, 1, 3, 1, glossiness) +#pragma param(blendlighting, 2, 0, 1, zero) + +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" + +#ifdef TRACKBLEND_PARALLAX +// the texture coordinates shifted by the heights (b) of the normal map, as ParallaxMapping() of mat_parallax.frag +vec2 parallax_coord(sampler2D heights, vec2 coord, vec3 viewdir, vec2 dcoorddx, vec2 dcoorddy) +{ + float pos_len = length(f_pos.xyz); + if (pos_len > 100.0) + return coord; + float fade = 1.0 - smoothstep(80.0, 100.0, pos_len); + if (EXTRAEFFECTS_ENABLED) + { + const float minLayers = 8.0; + const float maxLayers = 32.0; + float LayersWeight = pos_len / 20.0; + vec2 currentTexCoords = coord; + float currentDepthMapValue = textureGrad(heights, currentTexCoords, dcoorddx, dcoorddy).b; + LayersWeight = min(abs(dot(vec3(0.0, 0.0, 1.0), viewdir)), LayersWeight); + float numLayers = mix(maxLayers, minLayers, clamp(LayersWeight, 0.0, 1.0)); + float layerDepth = 1.0 / numLayers; + float currentLayerDepth = 0.0; + vec2 P = viewdir.xy * param[2].y * fade; + vec2 deltaTexCoords = P / numLayers; + while (currentLayerDepth < currentDepthMapValue) + { + currentTexCoords -= deltaTexCoords; + currentDepthMapValue = textureGrad(heights, currentTexCoords, dcoorddx, dcoorddy).b; + currentLayerDepth += layerDepth; + } + vec2 prevTexCoords = currentTexCoords + deltaTexCoords; + float afterDepth = currentDepthMapValue - currentLayerDepth; + float beforeDepth = textureGrad(heights, prevTexCoords, dcoorddx, dcoorddy).b - currentLayerDepth + layerDepth; + float weight = afterDepth / (afterDepth - beforeDepth); + return prevTexCoords * weight + currentTexCoords * (1.0 - weight); + } + else + { + float height = textureGrad(heights, coord, dcoorddx, dcoorddy).b; + vec2 p = viewdir.xy / viewdir.z * (height * (param[2].y - param[2].z) * 0.2) * fade; + return coord - p; + } +} +// each texture at the coordinates shifted by the heights of its own normal map +#define TRACKBLEND_COORD(heights) parallax_coord(heights, f_coord, viewdir, dx, dy) +#else +#define TRACKBLEND_COORD(heights) f_coord +#endif + +void main() +{ + // how far the fragment is into the track from the ends, in the lengths of their blending (track_blend::blending::weigh()): + // from -100 down where the track joined there doesn't blend back and its texture goes all the way to the joint, 10000 where + // nothing's blended at the end +#ifdef TRACKBLEND_SPECGLOSS + vec3 along = vec3(UserData.yz, 10000.0); +#else + vec3 along = UserData.yzw; +#endif + vec3 single = vec3(lessThan(along, vec3(-50.0))); + vec3 fraction = mix(along, -100.0 - along, single); + vec3 weights = mix(vec3(0.5), vec3(1.0), single) * clamp(1.0 - fraction, 0.0, 1.0); + float total = weights.x + weights.y + weights.z; + if (total > 1.0) + { + weights /= total; + total = 1.0; + } + // the gradients are taken outside the branches the textures are sampled in + vec2 dx = dFdx(f_coord); + vec2 dy = dFdy(f_coord); +#ifdef TRACKBLEND_PARALLAX + vec3 viewdir = normalize(vec3(0.0) - TangentFragPos); +#endif + vec2 coord = TRACKBLEND_COORD(normalmap); + vec4 tex_color = textureGrad(diffuse, coord, dx, dy); +#ifdef TRACKBLEND_NORMALMAP + vec4 tex_normal = textureGrad(normalmap, coord, dx, dy); +#endif + if (total > 0.0) + { + vec4 blended = tex_color * (1.0 - total); +#ifdef TRACKBLEND_NORMALMAP + vec4 blendednormal = tex_normal * (1.0 - total); +#endif + if (weights.x > 0.0) + { + vec2 coord1 = TRACKBLEND_COORD(normalmap1); + blended += textureGrad(diffuse1, coord1, dx, dy) * weights.x; +#ifdef TRACKBLEND_NORMALMAP + blendednormal += textureGrad(normalmap1, coord1, dx, dy) * weights.x; +#endif + } + if (weights.y > 0.0) + { + vec2 coord2 = TRACKBLEND_COORD(normalmap2); + blended += textureGrad(diffuse2, coord2, dx, dy) * weights.y; +#ifdef TRACKBLEND_NORMALMAP + blendednormal += textureGrad(normalmap2, coord2, dx, dy) * weights.y; +#endif + } +#ifndef TRACKBLEND_SPECGLOSS + if (weights.z > 0.0) + { + vec2 coord3 = TRACKBLEND_COORD(normalmap3); + blended += textureGrad(diffuse3, coord3, dx, dy) * weights.z; +#ifdef TRACKBLEND_NORMALMAP + blendednormal += textureGrad(normalmap3, coord3, dx, dy) * weights.z; +#endif + } +#endif + tex_color = blended; +#ifdef TRACKBLEND_NORMALMAP + tex_normal = blendednormal; +#endif + } + + bool alphatestfail = ( opacity >= 0.0 ? (tex_color.a < opacity) : (tex_color.a >= -opacity) ); + if(alphatestfail) + discard; + + vec3 fragcolor = ambient; +#ifdef TRACKBLEND_NORMALMAP + vec3 normal; + normal.xy = (tex_normal.rg * 2.0 - 1.0); + normal.z = sqrt(1.0 - clamp((dot(normal.xy, normal.xy)), 0.0, 1.0)); + vec3 fragnormal = normalize(f_tbn * normalize(normal.xyz)); + float reflectivity = param[1].z * tex_normal.a; +#ifdef TRACKBLEND_SPECGLOSS + vec4 tex_specgloss = textureGrad(specgloss, coord, dx, dy); + float specularity = tex_specgloss.r; + glossiness = tex_specgloss.g * abs(param[1].w); + metalic = tex_specgloss.b; +#else + float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; + legacy_gloss(); +#endif + int lighting = int(param[2].x + 0.5); + if (lighting == 2) + fragcolor = apply_lights_sunless(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + else + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, (lighting == 3 ? 1.0 : shadow_tone)); +#else + vec3 fragnormal = normalize(f_normal); + matte = true; + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, 0.0, 0.0, shadow_tone); +#endif + vec4 color = vec4(apply_fog(fragcolor), tex_color.a * 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, tex_color.a * alpha_mult); + } +} diff --git a/shaders/vk14/traction.frag b/shaders/vk14/traction.frag new file mode 100644 index 00000000..7f72d0fc --- /dev/null +++ b/shaders/vk14/traction.frag @@ -0,0 +1,39 @@ +#version 460 core +#define STAGE_FRAGMENT 1 +#include "common.glsl" + +layout(location = 3) in vec4 f_pos; +layout(location = 4) in vec4 f_clip_pos; +layout(location = 5) in vec4 f_clip_future_pos; + +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 "tonemapping.glsl" +#include "apply_fog.glsl" + +void main() +{ + vec4 color = vec4(apply_fog(pow(param[0].rgb, vec3(2.2))), param[0].a); + 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/traction.vert b/shaders/vk14/traction.vert new file mode 100644 index 00000000..20ca8171 --- /dev/null +++ b/shaders/vk14/traction.vert @@ -0,0 +1,20 @@ +#version 460 core +#define STAGE_VERTEX 1 +#include "common.glsl" +layout(location = 0) in vec3 v_vert; +layout(location = 1) in vec3 v_normal; +layout(location = 2) in vec2 v_coord; + + +layout(location = 4) out vec4 f_clip_pos; +layout(location = 5) out vec4 f_clip_future_pos; +layout(location = 3) out vec4 f_pos; + +void main() +{ + f_pos = modelview * vec4(v_vert, 1.0f); + f_clip_pos = (projection * modelview) * vec4(v_vert, 1.0f); + f_clip_future_pos = (projection * future * modelview) * vec4(v_vert, 1.0f); + + gl_Position = f_clip_pos; +} diff --git a/shaders/vk14/vbocolor.vert b/shaders/vk14/vbocolor.vert new file mode 100644 index 00000000..cd915ac0 --- /dev/null +++ b/shaders/vk14/vbocolor.vert @@ -0,0 +1,19 @@ +#version 460 core +#define STAGE_VERTEX 1 +#include "common.glsl" +layout(location = 0) in vec3 v_vert; +layout(location = 1) in vec3 v_color; + +layout(location = 13) out vec3 f_color; +layout(location = 3) out vec4 f_pos; +layout(location = 4) out vec4 f_clip_pos; + + +void main() +{ + f_pos = modelview * vec4(v_vert, 1.0); + f_clip_pos = (projection * modelview) * vec4(v_vert, 1.0); + f_color = pow(v_color, vec3(1.0)); + + gl_Position = f_clip_pos; +} diff --git a/shaders/vk14/vertex.vert b/shaders/vk14/vertex.vert new file mode 100644 index 00000000..2119390e --- /dev/null +++ b/shaders/vk14/vertex.vert @@ -0,0 +1,55 @@ +#version 460 core +#define STAGE_VERTEX 1 +#include "common.glsl" +layout(location = 0) in vec3 v_vert; +layout(location = 1) in vec3 v_normal; +layout(location = 2) in vec2 v_coord; +layout(location = 3) in vec4 v_tangent; +layout(location = 4) in vec4 v_userdata; + + +// the depth prepass (depthprepass.vert) works the position out the same way, and has to get the same depth +invariant gl_Position; + +layout(location = 0) out vec3 f_normal; +layout(location = 1) flat out vec3 f_normal_raw; +layout(location = 2) out vec2 f_coord; +layout(location = 3) out vec4 f_pos; +layout(location = 8) out mat3 f_tbn; + +layout(location = 4) out vec4 f_clip_pos; +layout(location = 5) out vec4 f_clip_future_pos; + +//out vec3 TangentLightPos; +//out vec3 TangentViewPos; +layout(location = 6) out vec3 TangentFragPos; +layout(location = 7) out vec4 UserData; + +void main() +{ + mat4 mv = effective_modelview(); + mat3 mvn = effective_modelviewnormal(); + + f_normal = normalize(mvn * v_normal); + f_normal_raw = v_normal; + f_coord = v_coord; + f_pos = mv * vec4(v_vert, 1.0); + f_clip_pos = projection * f_pos; + f_clip_future_pos = (projection * future) * f_pos; + + gl_Position = f_clip_pos; + gl_PointSize = param[1].x; + + vec3 N = f_normal; + vec3 T = mvn * v_tangent.xyz; + T = normalize(T - N * dot(N, T)); + // w is -1 where the texture is mirrored (calculate_tangents()), the bitangent turns with it + vec3 B = cross(N, T) * (v_tangent.w < 0.0 ? -1.0 : 1.0); + f_tbn = mat3(T, B, N); + + mat3 TBN = transpose(f_tbn); +// TangentLightPos = TBN * f_light_pos.xyz; +// TangentViewPos = TBN * vec3(0.0, 0.0, 0.0); + TangentFragPos = TBN * f_pos.xyz; + UserData = v_userdata; +} diff --git a/shaders/vk14/vertexonly.vert b/shaders/vk14/vertexonly.vert new file mode 100644 index 00000000..14c2b335 --- /dev/null +++ b/shaders/vk14/vertexonly.vert @@ -0,0 +1,10 @@ +#version 460 core +#define STAGE_VERTEX 1 +#include "common.glsl" +layout(location = 0) in vec3 v_vert; + + +void main() +{ + gl_Position = (projection * effective_modelview()) * vec4(v_vert, 1.0); +} diff --git a/world/Traction.h b/world/Traction.h index 54e71d1c..2f2dd93f 100644 --- a/world/Traction.h +++ b/world/Traction.h @@ -17,10 +17,15 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Names.h" class TTractionPowerSource; +namespace gfx +{ +class scene_walk; +} class TTraction : public scene::basic_node { friend opengl46_renderer; + friend gfx::scene_walk; public: // na razie TTractionPowerSource *psPower[ 2 ] { nullptr, nullptr }; // najbliższe zasilacze z obu kierunków