From 06d7b9e9f0408cfb5bf2c02e8543dcf7535a6738 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Fri, 16 Oct 2026 02:43:27 +0200 Subject: [PATCH] Cast shadows of the cab lamp from the cab in the GL 4.6 renderer --- CMakeLists.txt | 1 + docs/opengl46-renderer.md | 23 +- gl/glsl_common.cpp | 1 + gl/ubo.h | 6 + rendering/opengl46/opengl46locallights.cpp | 4 +- rendering/opengl46/opengl46locallights.h | 11 + rendering/opengl46/opengl46localshadows.cpp | 223 ++++++++++++++++++++ rendering/opengl46/opengl46localshadows.h | 106 ++++++++++ rendering/opengl46/opengl46renderer.cpp | 64 +++++- rendering/opengl46/opengl46renderer.h | 7 + shaders/light_common.glsl | 54 ++++- shaders/shadowcube.geom | 47 +++++ shaders/shadowcube.vert | 15 ++ utilities/Globals.cpp | 11 + utilities/Globals.h | 3 + 15 files changed, 568 insertions(+), 8 deletions(-) create mode 100644 rendering/opengl46/opengl46localshadows.cpp create mode 100644 rendering/opengl46/opengl46localshadows.h create mode 100644 shaders/shadowcube.geom create mode 100644 shaders/shadowcube.vert diff --git a/CMakeLists.txt b/CMakeLists.txt index 546ae3b0..25439c85 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -338,6 +338,7 @@ if (WITH_OPENGL_MODERN) "rendering/opengl46/opengl46exposure.cpp" "rendering/opengl46/opengl46ao.cpp" "rendering/opengl46/opengl46locallights.cpp" + "rendering/opengl46/opengl46localshadows.cpp" "rendering/opengl46/opengl46instancecull.cpp" "rendering/opengl46/opengl46precipitation.cpp" "rendering/opengl46/opengl46particles.cpp" diff --git a/docs/opengl46-renderer.md b/docs/opengl46-renderer.md index c547a9c2..f9e28eff 100644 --- a/docs/opengl46-renderer.md +++ b/docs/opengl46-renderer.md @@ -116,8 +116,22 @@ scales can also be changed in the **Light clusters** debug window. which bounces around the cab (`gfx.lights.cab.bounce`), and the sun shadows stay. The lamps of models and compartments fade in from the light level at which scenery lamps switch on (`ls_Dark`) to full -darkness; in daylight they are not gathered at all. None of these lights casts shadows yet, so they also light what is -behind walls within their reach; that reach is kept short for compartments and the cab. +darkness; in daylight they are not gathered at all. Of these lights only the cab lamp casts shadows (see below); the others +also light what is behind walls within their reach, which is kept short for compartments. + +**Shadows of the cab lamp** (`gfx.lights.shadows`): the cab casts shadows in the light of its lamp, the desk, the seats, +the levers and the walls (light leaves the cab only through the windows, glass doesn't stop it). They come from a cube +shadow map around the lamp, drawn with the opaque parts of the cab model only. The map is kept in the space of the cab, so +it stays right as the vehicle moves; it is drawn anew every few frames, for the moving parts of the cab, and at once when +the lamp turns on or the cab changes. All six faces are drawn in one go, so drawing it costs about as much as drawing the +cab once more. Nothing is drawn with the lamp off (in the daylight it is). The level: + +| Level | Map | Drawn | Edges | +|---|---|---|---| +| `no` (0) | none | never | lamp without shadows, as before | +| `low` (1) | 512 x 512 per face, 6 MB | every 4th frame | hard (2x2 filter) | +| `medium` (2, default) | 1024 x 1024 per face, 24 MB | every 2nd frame | soft, 4 samples | +| `high` (3) | 1024 x 1024 per face, 24 MB | every frame | softer, 8 samples | | Entry | Default | Meaning | |---|---|---| @@ -129,6 +143,7 @@ behind walls within their reach; that reach is kept short for compartments and t | `gfx.lights.cab.intensity` | `1.5` | brightness of the cab lamp | | `gfx.lights.cab.bounce` | `0.3` | 0..1: share of the former ambient light of the cab lamp kept with the lamp on | | `gfx.lights.range` | `300` | distance from the camera the lamps of models and vehicles are gathered from, metres | +| `gfx.lights.shadows` | `medium` | `no`/`low`/`medium`/`high` (or 0..3): shadows of the cab lamp, see above | ### Changed option @@ -165,7 +180,9 @@ editor goes through the first `dynamiclights` headlights, the reflections throug windows): the switch for clustered lights, the heat map, the grid state and the same counters, and the switches and scales of the lamps with their counts. Shift+F12 shows the lamps in the `local lights:` row: how many were gathered (from scenery models, vehicles and compartments, and the cab lamp), how many were placed in the light buffer of the main view - (those that may light something in view) and did not fit, and the cpu time of gathering them. + (those that may light something in view) and did not fit, and the cpu time of gathering them. The `lamp shadows:` row + gives the size of the cube map, whether it holds the cab lamp, the draw calls and cpu time of its last drawing, and the + timings the GPU time of drawing it (`lamp shadows:`); the level can be changed in the window. - **Sky and exposure** window (next to the Light clusters window): the switches and scales above, the share of the atmosphere, the colours of the sun, ambient light and fog in use, and the measured luminance and exposure. Shift+F12 shows the GPU time of `atmosphere:` (look-up tables, only when the sun moved or the weather changed), diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index 4cffe0d1..c7b74748 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -56,6 +56,7 @@ void gl::glsl_common_setup() "#define ATMOSPHERE_MULTISCATTERING_UNIT " + std::to_string(ATMOSPHERE_MULTISCATTERING_UNIT) + "\n" + "#define ATMOSPHERE_SKYVIEW_UNIT " + std::to_string(ATMOSPHERE_SKYVIEW_UNIT) + "\n" + "#define SKY_VIEW_TEX " + std::to_string(SKY_VIEW_TEX) + "\n" + + "#define LOCAL_SHADOW_TEX " + std::to_string(LOCAL_SHADOW_TEX) + "\n" + R"STRING( const uint LIGHT_SPOT = 0U; const uint LIGHT_POINT = 1U; diff --git a/gl/ubo.h b/gl/ubo.h index 200e10cd..bcfbec5e 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -58,6 +58,8 @@ namespace gl const size_t ENVMAP_GGX_GROUP_SIZE = 8; // sky-view table of the atmosphere (sky_atmosphere.frag). past the units the texture cache keeps track of, bound directly const size_t SKY_VIEW_TEX = MAX_TEXTURES + HELPER_TEXTURES + 0; + // cube shadow maps of the lamps (cube map array, depth compared; opengl46_local_shadows). bound directly, like the one above + const size_t LOCAL_SHADOW_TEX = MAX_TEXTURES + HELPER_TEXTURES + 1; struct scene_ubs { @@ -136,6 +138,10 @@ namespace gl float intensity; float ambient; + // HEADLIGHTS: view space to the space of the beam texture, and the weights of its channels. + // SPOT and POINT (lamps): view space to the space of the cube shadow map of the lamp (origin at the lamp), and + // x: layer of the cube map array the map is in, below 0 when the lamp has no shadows; y: near plane of the map; + // z: size of a texel of the map one metre from the lamp; w: radius of the filter, in texels glm::mat4 headlight_projection; glm::vec4 headlight_weights; }; diff --git a/rendering/opengl46/opengl46locallights.cpp b/rendering/opengl46/opengl46locallights.cpp index 41d12410..b4429b53 100644 --- a/rendering/opengl46/opengl46locallights.cpp +++ b/rendering/opengl46/opengl46locallights.cpp @@ -333,8 +333,10 @@ void opengl46_local_lights::gather_cab_lamp(gfx::geometrybank_manager const &Geo auto const lamp{glm::vec3{bounds.center.x, std::max(bounds.center.y, bounds.center.y + bounds.extent.y - 0.25f), bounds.center.z}}; m_cablamp = light{}; m_cablamp.position = vehicle->GetWorldPosition(glm::dvec3{lamp}); + m_cablampvehicle = vehicle; + m_cablamplocal = lamp; m_cablamp.color = to_linear(vehicle->InteriorLight) * (level * Global.gfx_lights_cab_intensity); - // within the cab: with no shadows the lamp would light the track below and the body of the vehicle around it + // within the cab: without shadows (gfx.lights.shadows) the lamp would light the track below and the body of the vehicle around it m_cablamp.range = std::clamp(glm::length(bounds.extent) * 1.25f, 2.5f, 8.f); m_cablampon = (max_component(m_cablamp.color) > 0.f); } diff --git a/rendering/opengl46/opengl46locallights.h b/rendering/opengl46/opengl46locallights.h index 63db7c1b..82b71e11 100644 --- a/rendering/opengl46/opengl46locallights.h +++ b/rendering/opengl46/opengl46locallights.h @@ -69,6 +69,15 @@ class opengl46_local_lights { return m_cablampon ? &m_cablamp : nullptr; } + // the vehicle of the cab lamp and the position of the lamp in the space of its cab model (mdKabina), when the lamp is on + TDynamicObject const *cab_lamp_vehicle() const + { + return m_cablampon ? m_cablampvehicle : nullptr; + } + glm::vec3 const &cab_lamp_local() const + { + return m_cablamplocal; + } // counts of the last pass which wrote the lights to the light buffer: placed, and left out for the lack of room void placed(std::uint32_t const Placed, std::uint32_t const Dropped) { @@ -110,6 +119,8 @@ class opengl46_local_lights std::vector m_lights; light m_cablamp; bool m_cablampon{false}; + TDynamicObject const *m_cablampvehicle{nullptr}; + glm::vec3 m_cablamplocal{0.f}; std::unordered_map m_modellights; // whether the model has light points std::unordered_map m_cells; std::unordered_map m_sections; // compartments, by their submodel diff --git a/rendering/opengl46/opengl46localshadows.cpp b/rendering/opengl46/opengl46localshadows.cpp new file mode 100644 index 00000000..64fd265d --- /dev/null +++ b/rendering/opengl46/opengl46localshadows.cpp @@ -0,0 +1,223 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "rendering/opengl46/opengl46localshadows.h" + +#include "gl/framebuffer.h" +#include "gl/ubo.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/utilities.h" + +namespace +{ + +// near plane of the maps, metres. surfaces closer to the lamp (its own fitting) cast no shadow +float const SHADOW_NEAR{0.15f}; +// uniform locations of shadowcube.geom +GLint const NEAR_LOCATION{0}; +GLint const LAYER_LOCATION{1}; +// layer of the map of the cab lamp in the array +int const CAB_SLOT{0}; + +} // namespace + +opengl46_local_shadows::~opengl46_local_shadows() +{ + release(); + if (m_empty != 0) + glDeleteTextures(1, &m_empty); +} + +void opengl46_local_shadows::init() +{ + gl::shader vertex("shadowcube.vert"); + gl::shader geometry("shadowcube.geom"); + { + gl::shader fragment("shadowmap.frag"); + m_program = std::make_unique(std::vector>{vertex, geometry, fragment}); + } + { + gl::shader fragment("alphashadowmap.frag"); + m_alpha_program = std::make_unique(std::vector>{vertex, geometry, fragment}); + } + glProgramUniform1f(*m_program, NEAR_LOCATION, SHADOW_NEAR); + glProgramUniform1f(*m_alpha_program, NEAR_LOCATION, SHADOW_NEAR); + glProgramUniform1i(*m_program, LAYER_LOCATION, CAB_SLOT * 6); + glProgramUniform1i(*m_alpha_program, LAYER_LOCATION, CAB_SLOT * 6); + + // a shadow sampler with no depth texture to read from is undefined, also where the shading doesn't read it + glCreateTextures(GL_TEXTURE_CUBE_MAP_ARRAY, 1, &m_empty); + glTextureStorage3D(m_empty, 1, GL_DEPTH_COMPONENT32F, 1, 1, 6); + glTextureParameteri(m_empty, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTextureParameteri(m_empty, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTextureParameteri(m_empty, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); + glTextureParameteri(m_empty, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); + float const farthest{0.f}; + glClearTexImage(m_empty, 0, GL_DEPTH_COMPONENT, GL_FLOAT, &farthest); +} + +opengl46_local_shadows::level_setup opengl46_local_shadows::setup(int const Level) +{ + switch (Level) + { + case 1: + return {512, 4, 0.f}; + case 2: + return {1024, 2, 1.f}; + case 3: + return {1024, 1, 2.f}; + default: + return {0, 0, 0.f}; + } +} + +void opengl46_local_shadows::allocate(int const Size) +{ + release(); + if (Size <= 0) + return; + glCreateTextures(GL_TEXTURE_CUBE_MAP_ARRAY, 1, &m_texture); + glTextureStorage3D(m_texture, 1, GL_DEPTH_COMPONENT32F, Size, Size, 6 * (CAB_SLOT + 1)); + // one level: the lookups need no derivatives, also in the loops over the lights the fragments of a quad don't go through together + glTextureParameteri(m_texture, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTextureParameteri(m_texture, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTextureParameteri(m_texture, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTextureParameteri(m_texture, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTextureParameteri(m_texture, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); + // 1 (shadow) where the map holds n / d at least as large as the reference: a surface at most as far from the lamp + glTextureParameteri(m_texture, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); + + glCreateFramebuffers(1, &m_framebuffer); + // all layers: the geometry shader picks the face + glNamedFramebufferTexture(m_framebuffer, GL_DEPTH_ATTACHMENT, m_texture, 0); + glNamedFramebufferDrawBuffer(m_framebuffer, GL_NONE); + glNamedFramebufferReadBuffer(m_framebuffer, GL_NONE); + if (glCheckNamedFramebufferStatus(m_framebuffer, GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + { + ErrorLog("lamp shadow map framebuffer setup failed, the lamps have no shadows"); + release(); + Global.gfx_lights_shadows = 0; + return; + } + m_size = Size; + WriteLog("lamp shadow maps: " + std::to_string(Size) + " x " + std::to_string(Size) + " cube"); +} + +void opengl46_local_shadows::release() +{ + if (m_framebuffer != 0) + glDeleteFramebuffers(1, &m_framebuffer); + if (m_texture != 0) + glDeleteTextures(1, &m_texture); + m_framebuffer = 0; + m_texture = 0; + m_size = 0; + m_valid = false; +} + +void opengl46_local_shadows::bind() +{ + glBindTextureUnit(gl::LOCAL_SHADOW_TEX, (m_texture != 0) ? m_texture : m_empty); +} + +bool opengl46_local_shadows::update_wanted(TDynamicObject const *Vehicle, TModel3d const *Cab, glm::vec3 const &Lamp, unsigned int const Framestamp) +{ + auto const level{std::clamp(Global.gfx_lights_shadows, 0, 3)}; + if (level != m_level) + { + m_level = level; + auto const size{setup(level).size}; + if (size != m_size) + allocate(size); + m_valid = false; + } + if ((m_size == 0) || (Vehicle == nullptr) || (Cab == nullptr)) + { + // with the lamp off nothing reads the map; once it is on again the cab may have changed + m_valid = false; + return false; + } + if ((false == m_valid) || (Vehicle != m_vehicle) || (Cab != m_cab) || (Lamp != m_lamp)) + { + m_valid = false; + } + // the moving parts of the cab are followed every few frames + if (m_valid && (Framestamp - m_drawnframe < setup(level).interval)) + { + return false; + } + m_vehicle = Vehicle; + m_cab = Cab; + m_lamp = Lamp; + m_drawnframe = Framestamp; + return true; +} + +void opengl46_local_shadows::begin() +{ + m_cpustart = std::chrono::steady_clock::now(); + m_timer.begin(); + // the framebuffer is bound past the cache of gl::framebuffer, which end() resets + gl::framebuffer::unbind(); + glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffer); + glViewport(0, 0, m_size, m_size); + glDisable(GL_BLEND); + glDepthMask(GL_TRUE); + glEnable(GL_DEPTH_TEST); + // one sided walls of the cab face the lamp, they are to cast shadows as well + glDisable(GL_CULL_FACE); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + float const farthest{0.f}; + glClearNamedFramebufferfv(m_framebuffer, GL_DEPTH, 0, &farthest); +} + +void opengl46_local_shadows::end(std::uint32_t const Drawcalls) +{ + glEnable(GL_CULL_FACE); + gl::framebuffer::unbind(); + m_timer.end(); + m_valid = true; + m_drawcalls = Drawcalls; + ++m_updatecount; + auto const elapsed{std::chrono::duration(std::chrono::steady_clock::now() - m_cpustart).count()}; + m_cputime = (m_cputime == 0.0 ? elapsed : m_cputime * 0.9 + elapsed * 0.1); +} + +glm::vec4 opengl46_local_shadows::weights() const +{ + // a face spans twice the distance from the lamp + return glm::vec4{static_cast(CAB_SLOT), SHADOW_NEAR, 2.f / static_cast(std::max(m_size, 1)), setup(m_level).radius}; +} + +std::string opengl46_local_shadows::info_times() +{ + std::string text; + if (auto const time = m_timer.result()) + text += " lamp shadows: " + to_string(static_cast(*time) / 1000000.0, 3) + " ms\n"; + return text; +} + +std::string opengl46_local_shadows::info() const +{ + if (m_size == 0) + return ""; + return "\nlamp shadows: " + std::to_string(m_size) + " cube, " + (m_valid ? "cab lamp" : "none") + ", " + std::to_string(m_drawcalls) + " drawcalls, cpu " + to_string(m_cputime, 2) + + " ms"; +} + +void opengl46_local_shadows::draw_debug_ui() +{ + char const *levels[]{"no", "low (512, every 4th frame)", "medium (1024, every 2nd frame)", "high (1024, every frame, softer)"}; + ImGui::Combo("lamp shadows (gfx.lights.shadows)", &Global.gfx_lights_shadows, levels, IM_ARRAYSIZE(levels)); + if (m_size == 0) + return; + ImGui::Text("cube %d x %d, %s, drawn %u times, %u drawcalls, %.2f ms (cpu)", m_size, m_size, m_valid ? "cab lamp" : "no lamp", m_updatecount, m_drawcalls, m_cputime); +} diff --git a/rendering/opengl46/opengl46localshadows.h b/rendering/opengl46/opengl46localshadows.h new file mode 100644 index 00000000..0d716be1 --- /dev/null +++ b/rendering/opengl46/opengl46localshadows.h @@ -0,0 +1,106 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include "gl/shader.h" +#include "gl/query.h" + +class TDynamicObject; +class TModel3d; + +// cube shadow maps of the lamps of the opengl 4.6 renderer (Global.gfx_lights_shadows). for now one map, of the lamp of the +// occupied cab, with the shadows cast by the cab (the model drawn by Render_cab()). +// the map is kept in the space of the cab model, centred on the lamp: it stays right while the vehicle moves and turns, and is +// drawn anew only every few frames (for the moving parts of the cab: levers, doors, wipers) or when the lamp, the cab or the +// level change. the six faces are drawn at once, by a geometry shader (shadowcube.vert/.geom), so the cab costs one draw call +// per submodel, as in the colour pass. the depth is n / d (d the distance along the axis of the face), see local_light_shadow() +// in light_common.glsl, which reads the map for the light with the layer in its headlight_weights.x +class opengl46_local_shadows +{ + public: + opengl46_local_shadows() = default; + ~opengl46_local_shadows(); + opengl46_local_shadows(const opengl46_local_shadows &) = delete; + opengl46_local_shadows &operator=(const opengl46_local_shadows &) = delete; + + // creates the programs and the stand-in map. throws gl::shader_exception + void init(); + // puts the map (or the stand-in, which shadows nothing) on its texture unit, gl::LOCAL_SHADOW_TEX. once per context and frame + void bind(); + // whether the map of the cab lamp is to be drawn in this frame: Vehicle and Cab the occupied vehicle and its cab model, + // Lamp the position of the lamp in the space of the cab model; Vehicle nullptr when the lamp is off. (re)makes the map to + // the size of the level when the level changed + bool update_wanted(TDynamicObject const *Vehicle, TModel3d const *Cab, glm::vec3 const &Lamp, unsigned int const Framestamp); + // sets up the drawing to the map: the framebuffer, viewport, cleared faces, the state of the faces. the geometry is to be + // drawn with program() and a modelview from the space of the model to the space of the map + void begin(); + // ends the drawing to the map, leaves the default framebuffer bound and the culling of faces on + void end(std::uint32_t const Drawcalls); + // shadow program of the cube maps, of the alpha tested materials when Alpha + gl::program &program(bool const Alpha) + { + return Alpha ? *m_alpha_program : *m_program; + } + // whether the map holds the shadows of the lamp of Vehicle, the occupied vehicle + bool ready(TDynamicObject const *Vehicle) const + { + return m_valid && (Vehicle != nullptr) && (Vehicle == m_vehicle); + } + // position of the lamp the map was drawn for, in the space of the cab model + glm::vec3 const &lamp() const + { + return m_lamp; + } + // the headlight_weights of the light which reads the map: layer, near plane, size of a texel one metre from the lamp, + // radius of the filter in texels + glm::vec4 weights() const; + // lines for the gpu times and statistics of the debug panel + std::string info_times(); + std::string info() const; + // controls for the debug window + void draw_debug_ui(); + + private: + // size, refresh interval (frames) and filter radius (texels, 0: the 2x2 filter of the hardware) of a level + struct level_setup + { + int size; + unsigned int interval; + float radius; + }; + static level_setup setup(int const Level); + void allocate(int const Size); + void release(); + + std::unique_ptr m_program; + std::unique_ptr m_alpha_program; + GLuint m_texture{0}; // cube map array, one cube + GLuint m_empty{0}; // stand-in: a cube map array of 1x1 faces with the farthest depth + GLuint m_framebuffer{0}; + int m_size{0}; // of a face of m_texture, 0 when there is none + int m_level{-1}; // level the map was made for + bool m_valid{false}; // m_texture holds the map of m_vehicle + TDynamicObject const *m_vehicle{nullptr}; + TModel3d const *m_cab{nullptr}; + glm::vec3 m_lamp{0.f}; + unsigned int m_drawnframe{0}; // framestamp the map was last drawn in + gl::gpu_timer m_timer; + // statistics + std::uint32_t m_drawcalls{0}; // of the last drawing of the map + std::uint32_t m_updatecount{0}; // drawings of the map so far + double m_cputime{0.0}; // milliseconds, smoothed + std::chrono::steady_clock::time_point m_cpustart; +}; diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index dd0027b2..46c6b4ad 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -191,6 +191,7 @@ bool opengl46_renderer::Init(GLFWwindow *Window) m_instancecull.init(); m_exposure.init(); m_ao.init(); + m_localshadows.init(); } catch (gl::shader_exception const &e) { @@ -723,6 +724,7 @@ void opengl46_renderer::SwapBuffers() m_debugtimestext += m_exposure.info_times(); m_debugtimestext += m_ao.info_times(); m_debugtimestext += m_instancecull.info_times(); + m_debugtimestext += m_localshadows.info_times(); m_debugtimestext += "uilayer: " + to_string( Timer::subsystem.gfx_gui.average(), 2 ) + " ms\n"; if( DebugModeFlag ) m_debugtimestext += m_textures.info(); @@ -765,6 +767,7 @@ void opengl46_renderer::SwapBuffers() + " (" + std::to_string( m_lights_dropped ) + " over capacity), " + ( Global.gfx_lights_clustered ? "clustered" : "first " + std::to_string( std::max( Global.DynamicLightCount, 0 ) ) ) + "\n" + m_locallights.info() + + m_localshadows.info() + m_instancecull.info(); if( auto const cullstats = ( m_viewports.empty() || !( Global.gfx_lights_clustered || Global.gfx_lights_clusterheatmap ) ? std::nullopt : m_viewports.front()->clusters->stats() ) ) { m_debugstatstext += @@ -813,6 +816,8 @@ void opengl46_renderer::draw_debug_ui() } ImGui::Separator(); m_locallights.draw_debug_ui(); + ImGui::Separator(); + m_localshadows.draw_debug_ui(); } ImGui::End(); @@ -931,6 +936,8 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) setup_shadow_empty_map(); setup_env_map(nullptr); + // the cube shadow maps of the lamps, or the stand-in for them + m_localshadows.bind(); if (Global.gfx_shadowmap_enabled && vp.main) { @@ -959,6 +966,9 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) setup_env_map(m_env_tex.get()); } + if (vp.main) + Render_local_shadows(); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); setup_drawing(false); @@ -2478,12 +2488,21 @@ void opengl46_renderer::Bind_Material_Shadow(material_handle const Material) if (material.textures[0] != null_handle) { m_textures.bind(0, material.textures[0]); - m_alpha_shadow_shader->bind(); + if (m_cubeshadows) + m_localshadows.program(true).bind(); + else + m_alpha_shadow_shader->bind(); } + else if (m_cubeshadows) { + m_localshadows.program(false).bind(); + } else { m_shadow_shader->bind(); } } + else if (m_cubeshadows) { + m_localshadows.program(false).bind(); + } else { m_shadow_shader->bind(); } @@ -6138,8 +6157,22 @@ void opengl46_renderer::Update_Lights(light_array &Lights, bool const Headlights m_cablamplit = false; if( locallights ) { if( auto const *lamp{ m_locallights.cab_lamp() } ) { - Write_Local_Light( light_data.lights[ light_i++ ], *lamp, camera, mv ); + auto &light{ light_data.lights[ light_i++ ] }; + Write_Local_Light( light, *lamp, camera, mv ); m_cablamplit = true; + auto const *vehicle{ m_locallights.cab_lamp_vehicle() }; + if( m_localshadows.ready( vehicle ) ) { + // view space to the space of the map: the space of the cab model, with the lamp at the origin. the vehicle is + // where it is now, the map stays right as it moves + auto const toworld{ glm::dmat4{ glm::transpose( glm::mat3{ mv } ) } }; + auto const tomap{ + glm::translate( glm::dmat4{ 1.0 }, -glm::dvec3{ m_localshadows.lamp() } ) + * glm::inverse( vehicle->mMatrix ) + * glm::translate( glm::dmat4{ 1.0 }, camera - vehicle->GetPosition() ) + * toworld }; + light.headlight_projection = glm::mat4{ tomap }; + light.headlight_weights = m_localshadows.weights(); + } } } auto const headlightsstart{light_i}; @@ -6311,7 +6344,32 @@ void opengl46_renderer::Write_Local_Light(gl::light_element &Light, opengl46_loc Light.linear = 0.0f; Light.quadratic = 1.0f / sq(std::max(Source.range, 0.01f)); Light.headlight_projection = glm::mat4{1.0f}; - Light.headlight_weights = glm::vec4{0.0f}; + // no shadow map + Light.headlight_weights = glm::vec4{-1.0f, 0.0f, 0.0f, 0.0f}; +} + +void opengl46_renderer::Render_local_shadows() +{ + // only the light buffers with the lamps read the maps + auto const *vehicle{Global.gfx_lights_clustered ? m_locallights.cab_lamp_vehicle() : nullptr}; + if (false == m_localshadows.update_wanted(vehicle, (vehicle != nullptr ? vehicle->mdKabina : nullptr), m_locallights.cab_lamp_local(), m_framestamp)) + return; + + glDebug("render lamp shadows"); + // the cab drawn by the shadow pass, with the programs of the cube map. the camera is put at the vehicle, which Render_cab() + // places the cab relative to, and the modelview takes the cab model to the space of the map + m_renderpass.draw_mode = rendermode::shadows; + m_renderpass.pass_camera.position() = vehicle->GetPosition(); + m_renderpass.draw_stats = {}; + OpenGLMatrices.mode(GL_MODELVIEW); + OpenGLMatrices.load_matrix(glm::mat4{glm::translate(glm::dmat4{1.0}, -glm::dvec3{m_localshadows.lamp()}) * glm::inverse(vehicle->mMatrix)}); + m_localshadows.begin(); + m_cubeshadows = true; + Render_cab(vehicle, 0.0f, false); + m_cubeshadows = false; + m_localshadows.end(static_cast(m_renderpass.draw_stats.drawcalls)); + m_renderpass = m_colorpass; + glPolygonMode(GL_FRONT_AND_BACK, Global.bWireFrame ? GL_LINE : GL_FILL); } void opengl46_renderer::Update_Clusters(viewport_config &vp, glm::ivec2 const &Targetsize) diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index d2838c95..f7e759c4 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -36,6 +36,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/opengl46/opengl46exposure.h" #include "rendering/opengl46/opengl46ao.h" #include "rendering/opengl46/opengl46locallights.h" +#include "rendering/opengl46/opengl46localshadows.h" #include "rendering/opengl46/opengl46instancecull.h" // bare-bones render controller, in lack of anything better yet @@ -300,6 +301,8 @@ class opengl46_renderer : public gfx_renderer { void Render_pass(viewport_config &vp, rendermode const Mode); // creates dynamic environment cubemap bool Render_reflections(viewport_config &vp); + // draws the cube shadow maps of the lamps which need it in this frame (the cab lamp: the cab) + void Render_local_shadows(); bool Render(world_environment *Environment); void Render(scene::basic_region *Region); void Render(section_sequence::iterator First, section_sequence::iterator Last); @@ -507,6 +510,10 @@ class opengl46_renderer : public gfx_renderer { 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; + // 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; double m_precipitationrotation; diff --git a/shaders/light_common.glsl b/shaders/light_common.glsl index 357c2a5f..d4e44d38 100644 --- a/shaders/light_common.glsl +++ b/shaders/light_common.glsl @@ -2,6 +2,8 @@ uniform sampler2DArrayShadow shadowmap; #endif uniform sampler2D headlightmap; +// cube shadow maps of the lamps, see gl::LOCAL_SHADOW_TEX and opengl46_local_shadows +layout(binding = LOCAL_SHADOW_TEX) uniform samplerCubeArrayShadow local_shadowmap; #include #include @@ -232,10 +234,58 @@ vec2 calc_headlights(light_s light, vec3 fragnormal, vec3 dpdx, vec3 dpdy) // closest distance (squared) the light of a lamp is computed for: nearer the inverse square would grow without a limit const float LOCAL_LIGHT_MIN_DISTANCE2 = 0.01; +// one comparison with the cube shadow map at the point of the space of the map, moved towards the lamp by Bias (metres) +float local_light_shadow_tap(vec3 position, float layer, float near, float bias) +{ + vec3 axes = abs(position); + float major = max(axes.x, max(axes.y, axes.z)); + return texture(local_shadowmap, vec4(position, layer), near / max(major - bias, near)); +} + +// share of the light of a lamp with a cube shadow map (headlight_weights.x is its layer) which reaches the fragment, 0..1. +// the map holds n / d of the closest surface, d the distance along the axis of the face, n the near plane: the farther, the +// smaller (as the depth of the scene, reverse z). the comparison gives 1 where the map holds a value at least as large as +// the reference, i.e. a surface at most as far: in shadow +float local_light_shadow(light_s light, vec3 fragnormal) +{ + // the space of the map: origin at the lamp, axes of the model the shadows are cast by + vec3 position = (light.headlight_projection * vec4(f_pos.xyz, 1.0)).xyz; + vec3 normal = normalize(mat3(light.headlight_projection) * fragnormal); + float distance = length(position); + // a texel of the map is as large as this here. the point is moved along the normal by about a texel, which keeps a surface + // from shadowing itself at grazing angles, and the reference is moved towards the lamp by a part of a texel + float texel = light.headlight_weights.z * distance; + vec3 tolamp = -position / max(distance, 1e-4); + float NdotL = clamp(dot(normal, tolamp), 0.0, 1.0); + position += normal * (texel * (0.5 + 1.5 * (1.0 - NdotL))); + float bias = texel * 1.5; + float layer = light.headlight_weights.x; + float radius = light.headlight_weights.w; + if (radius <= 0.0) + return local_light_shadow_tap(position, layer, light.headlight_weights.y, bias); + // a disc of samples in the plane of the surface, each compared with its own distance: points of the surface itself, which + // samples spread across the direction to the lamp would not be at grazing angles. rotated per pixel to turn the banding into noise + vec3 side = normalize(cross(normal, (abs(normal.y) < 0.99) ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0))); + vec3 up = cross(normal, side); + float angle = 6.2831853 * fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))); + vec2 rotation = vec2(cos(angle), sin(angle)); + float spread = texel * radius; + const vec2 disc[8] = vec2[](vec2(-0.326, -0.406), vec2(-0.840, -0.074), vec2(-0.696, 0.457), vec2(-0.203, 0.621), + vec2(0.962, -0.195), vec2(0.473, -0.480), vec2(0.519, 0.767), vec2(0.185, -0.893)); + int count = (radius > 1.5) ? 8 : 4; + float shadow = 0.0; + for (int i = 0; i < count; i++) + { + vec2 offset = vec2(disc[i].x * rotation.x - disc[i].y * rotation.y, disc[i].x * rotation.y + disc[i].y * rotation.x) * spread; + shadow += local_light_shadow_tap(position + side * offset.x + up * offset.y, layer, light.headlight_weights.y, bias); + } + return shadow / float(count); +} + // light of a lamp (point or spot light: the lamps of models and vehicles, the cab lamp) reaching the fragment, x the diffuse // and y the specular part. it falls off with the square of the distance, and smoothly to nothing at the range of the light, // 1 / sqrt(quadratic), past which the culling of the clusters does not take it. a spot light fades from its inner cone to the -// outer one +// outer one. a lamp with a shadow map is held back by what is between it and the fragment vec2 calc_local_light(light_s light, vec3 fragnormal) { vec3 tolight = light.pos - f_pos.xyz; @@ -245,6 +295,8 @@ vec2 calc_local_light(light_s light, vec3 fragnormal) float atten = edge * edge / max(distance2, LOCAL_LIGHT_MIN_DISTANCE2); if (light.type == LIGHT_SPOT) atten *= smoothstep(light.out_cutoff, light.in_cutoff, dot(-light_dir, normalize(light.dir))); + if ((atten > 0.0) && (light.headlight_weights.x >= 0.0) && (dot(fragnormal, light_dir) > 0.0)) + atten *= 1.0 - local_light_shadow(light, fragnormal); if (atten <= 0.0) return vec2(0.0); vec2 part = calc_light(light_dir, fragnormal); diff --git a/shaders/shadowcube.geom b/shaders/shadowcube.geom new file mode 100644 index 00000000..96b3f821 --- /dev/null +++ b/shaders/shadowcube.geom @@ -0,0 +1,47 @@ +// draws the geometry into the six faces of a cube shadow map at once, one invocation per face. the input is in the space of the +// map, with the lamp at the origin. the faces go in the order of the faces of a cube map (+x, -x, +y, -y, +z, -z), each looks the +// way glm::lookAt() with the usual up vectors of cube maps sets it, with a 90 degree projection of infinite reach and reversed +// depth like the one of the scene: the depth is near / d, d the distance along the axis of the face +layout(triangles, invocations = 6) in; +layout(triangle_strip, max_vertices = 3) out; + +in vec3 g_pos[]; +in vec2 g_coord[]; +out vec2 f_coord; + +// near plane of the map, metres: closer surfaces (the lamp itself) cast no shadows +layout(location = 0) uniform float cube_near; +// layer of the cube map array the first face of the map is in +layout(location = 1) uniform int cube_layer; + +// right, up and forward axes of the faces +const vec3 face_right[6] = vec3[](vec3(0.0, 0.0, -1.0), vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0), vec3(1.0, 0.0, 0.0), vec3(1.0, 0.0, 0.0), vec3(-1.0, 0.0, 0.0)); +const vec3 face_up[6] = vec3[](vec3(0.0, -1.0, 0.0), vec3(0.0, -1.0, 0.0), vec3(0.0, 0.0, 1.0), vec3(0.0, 0.0, -1.0), vec3(0.0, -1.0, 0.0), vec3(0.0, -1.0, 0.0)); +const vec3 face_forward[6] = vec3[](vec3(1.0, 0.0, 0.0), vec3(-1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), vec3(0.0, -1.0, 0.0), vec3(0.0, 0.0, 1.0), vec3(0.0, 0.0, -1.0)); + +void main() +{ + int face = gl_InvocationID; + vec4 clip[3]; + for (int i = 0; i < 3; i++) + clip[i] = vec4(dot(g_pos[i], face_right[face]), dot(g_pos[i], face_up[face]), cube_near, dot(g_pos[i], face_forward[face])); + // triangles wholly outside of the face are not sent further + if ((clip[0].w < cube_near) && (clip[1].w < cube_near) && (clip[2].w < cube_near)) + return; + if ((clip[0].x > clip[0].w) && (clip[1].x > clip[1].w) && (clip[2].x > clip[2].w)) + return; + if ((clip[0].x < -clip[0].w) && (clip[1].x < -clip[1].w) && (clip[2].x < -clip[2].w)) + return; + if ((clip[0].y > clip[0].w) && (clip[1].y > clip[1].w) && (clip[2].y > clip[2].w)) + return; + if ((clip[0].y < -clip[0].w) && (clip[1].y < -clip[1].w) && (clip[2].y < -clip[2].w)) + return; + for (int i = 0; i < 3; i++) + { + gl_Layer = cube_layer + face; + gl_Position = clip[i]; + f_coord = g_coord[i]; + EmitVertex(); + } + EndPrimitive(); +} diff --git a/shaders/shadowcube.vert b/shaders/shadowcube.vert new file mode 100644 index 00000000..c8a2a54d --- /dev/null +++ b/shaders/shadowcube.vert @@ -0,0 +1,15 @@ +layout(location = 0) in vec3 v_vert; +layout(location = 2) in vec2 v_coord; + +// position in the space of the cube shadow map being drawn (origin at the lamp), see shadowcube.geom +out vec3 g_pos; +out vec2 g_coord; + +#include + +void main() +{ + g_pos = (effective_modelview() * vec4(v_vert, 1.0f)).xyz; + g_coord = v_coord; + gl_Position = vec4(g_pos, 1.0f); +} diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 46c06466..1beeaa1e 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1606,6 +1606,16 @@ global_settings::ConfigParse_gfx_sky( cParser &Parser, std::string_view const To else if( Token == "gfx.lights.cab.intensity" ) { readvalue( gfx_lights_cab_intensity, 0.f, 100.f ); } else if( Token == "gfx.lights.cab.bounce" ) { readvalue( gfx_lights_cab_bounce, 0.f, 1.f ); } else if( Token == "gfx.lights.range" ) { readvalue( gfx_lights_range, 0.f, 2000.f ); } + else if( Token == "gfx.lights.shadows" ) { + // no/low/medium/high, or the number of the level + Parser.getTokens( 1 ); + std::string level; + Parser >> level; + gfx_lights_shadows = + ( level == "low" || level == "1" ) ? 1 : + ( level == "medium" || level == "yes" || level == "true" || level == "2" ) ? 2 : + ( level == "high" || level == "3" ) ? 3 : + 0; } else { return false; } return true; @@ -1881,6 +1891,7 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.lights.cab.intensity", gfx_lights_cab_intensity ); export_as_text( Output, "gfx.lights.cab.bounce", gfx_lights_cab_bounce ); export_as_text( Output, "gfx.lights.range", gfx_lights_range ); + export_as_text( Output, "gfx.lights.shadows", std::string{ gfx_lights_shadows == 3 ? "high" : gfx_lights_shadows == 2 ? "medium" : gfx_lights_shadows == 1 ? "low" : "no" } ); export_as_text( Output, "python.enabled", python_enabled ); export_as_text( Output, "python.threadedupload", python_threadedupload ); export_as_text( Output, "python.uploadmain", python_uploadmain ); diff --git a/utilities/Globals.h b/utilities/Globals.h index 56a63522..354cb488 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -343,6 +343,9 @@ struct global_settings { float gfx_lights_cab_intensity { 1.5f }; // brightness of the cab lamp float gfx_lights_cab_bounce { 0.3f }; // share of the former ambient light of the cab lamp kept, for the light which bounces around the cab float gfx_lights_range { 300.f }; // distance from the camera the lights of models and vehicles are taken from, metres + // shadows of the lamps (cube shadow maps), 0 none, 1 low, 2 medium, 3 high; see opengl46_local_shadows. for now the lamp of the + // occupied cab, its shadows cast by the cab + int gfx_lights_shadows { 2 }; float ui_fontsize = 13.0f; float ui_scale = 1.0f;