diff --git a/docs/opengl46-renderer.md b/docs/opengl46-renderer.md index 82e71994..96009c67 100644 --- a/docs/opengl46-renderer.md +++ b/docs/opengl46-renderer.md @@ -148,6 +148,27 @@ into the cab only through the windows. Each such lamp has a cube shadow map in w Lamps of vehicles and lit compartments cast no shadows. The memory taken is 2 maps per lamp: with the defaults (8 lamps, 512 x 512 per face) about 100 MB of video memory. +**Shadows of headlights** (with `gfx.lights.shadows` above `no`): the headlights of the vehicles cast the shadows of what is +in their beam, other vehicles, signals, poles, platforms, the walls of a cutting. The beam of a set of headlights (one end of +a vehicle) is projected from behind the vehicle, as a narrow cone along it which starts at its end; the shadow map is drawn +with that same projection, so it covers the beam and nothing else, and the shadows fall the way the beam does (nearly along +the vehicle). In the map is what lies within `gfx.lights.shadows.headlights.range` ahead of the vehicle (the terrain, +tracks, models, the other vehicles and the occupied cab, only their back faces, as in the shadows of the sun), not the +vehicle itself; past that reach things cast no shadow of the headlights, but what is behind a nearer one is still in its +shadow. Maps go to the most important headlights (the nearest and brightest, as the light buffer orders them), +`gfx.lights.shadows.headlights` sets of them, of those whose beam is in view within that reach. By day, when the beam isn't +seen, nothing is drawn. A map keeps the place the projector was in when it was drawn, so an older one still holds right for +what doesn't move: + +| Level | Map | Drawn, vehicle moving / standing | Edges | +|---|---|---|---| +| `low` (1) | 1024 x 1024, 4 MB | every 2nd / 8th frame | hard (2x2 filter) | +| `medium` (2) | 1024 x 1024, 4 MB | every frame / every 4th | soft, 4 samples | +| `high` (3) | 2048 x 2048, 16 MB | every frame / every 2nd | softer, 8 samples | + +Drawing a map is a shadow pass of the scene in the beam (draw calls in proportion to what is in it, on the CPU like the +shadows of the sun, see `headlight shadows:` in Shift+F12); with the vehicle on the move it is drawn every frame from `medium` on. + | Entry | Default | Meaning | |---|---|---| | `gfx.lights.scenery yes/no` | `yes` | light points of scenery models light their surroundings | @@ -163,6 +184,8 @@ Lamps of vehicles and lit compartments cast no shadows. The memory taken is 2 ma | `gfx.lights.shadows.lamps.size` | `512` | 128..2048: size of a face of the shadow map of a scenery lamp | | `gfx.lights.shadows.lamps.range` | `100` | distance from the camera within which scenery lamps cast shadows, metres | | `gfx.lights.shadows.vehicles.range` | `50` | distance from the camera within which vehicles (and the occupied cab) cast the shadows of scenery lamps, metres; 0: only the scenery does | +| `gfx.lights.shadows.headlights` | `2` | 0..4: how many sets of headlights (vehicle ends) cast shadows; 0: none | +| `gfx.lights.shadows.headlights.range` | `250` | 10..1000: distance ahead of the vehicle within which things cast the shadows of its headlights, metres | ### Models drawn from the GPU @@ -223,7 +246,10 @@ editor goes through the first `dynamiclights` headlights, the reflections throug timings the GPU time of drawing it (`lamp shadows:`); the level can be changed in the window. The `scenery lamp shadows:` row gives the lamps with maps (of the slots, how many ready, how many with vehicles), how many scenery parts and vehicle copies were drawn so far, and the draw calls and cpu time of the last frame which drew any; the timings show their GPU time - (`scenery lamp shadows:`). The count and both ranges can be changed in the window too. + (`scenery lamp shadows:`). The count and both ranges can be changed in the window too. The `headlight shadows:` row gives + the headlights with maps (of the slots, how many ready), the size of the maps, how many were drawn so far, and the draw + calls and cpu time of the last frame which drew any, with their GPU time in the timings (`headlight shadows:`); the count + and the reach 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 d13db0e6..846770d4 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -61,6 +61,8 @@ void gl::glsl_common_setup() "#define SKY_VIEW_TEX " + std::to_string(SKY_VIEW_TEX) + "\n" + "#define LOCAL_SHADOW_TEX " + std::to_string(LOCAL_SHADOW_TEX) + "\n" + "#define LOCAL_SHADOW_LAMPS_TEX " + std::to_string(LOCAL_SHADOW_LAMPS_TEX) + "\n" + + "#define HEADLIGHT_SHADOW_TEX " + std::to_string(HEADLIGHT_SHADOW_TEX) + "\n" + + "#define MAX_HEADLIGHT_SHADOWS " + std::to_string(MAX_HEADLIGHT_SHADOWS) + "U\n" + R"STRING( const uint LIGHT_SPOT = 0U; const uint LIGHT_POINT = 1U; @@ -196,6 +198,9 @@ void gl::glsl_common_setup() vec4 wiper_pos; vec4 wiper_timer_out; vec4 wiper_timer_return; + // shadow maps of the headlights, see gl::scene_ubs + mat4 headlight_shadow[MAX_HEADLIGHT_SHADOWS]; + vec4 headlight_shadow_params[MAX_HEADLIGHT_SHADOWS]; }; #ifdef STAGE_VERTEX diff --git a/gl/ubo.h b/gl/ubo.h index af0871fe..68c63cbd 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -62,6 +62,9 @@ namespace gl const size_t LOCAL_SHADOW_TEX = MAX_TEXTURES + HELPER_TEXTURES + 1; // the same for the lamps of scenery models (a texture of its own: the maps have another size) const size_t LOCAL_SHADOW_LAMPS_TEX = MAX_TEXTURES + HELPER_TEXTURES + 2; + // shadow maps of the headlights of vehicles (2d array, depth compared; opengl46_local_shadows), and how many of them there may be + const size_t HEADLIGHT_SHADOW_TEX = MAX_TEXTURES + HELPER_TEXTURES + 3; + const size_t MAX_HEADLIGHT_SHADOWS = 4; struct scene_ubs { @@ -74,9 +77,14 @@ namespace gl glm::vec4 wiper_pos; glm::vec4 wiper_timer_out; glm::vec4 wiper_timer_return; + // view space of the pass to the space of the shadow map of a set of headlights (HEADLIGHTS lights with headlight_weights.w + // above 0 read the map w - 1): x, y 0..1 across the map, z / w the depth of the map, w the distance along the axis of the + // projector. and x: size of a texel of the map one metre from the projector, y: radius of the filter in texels + glm::mat4 headlight_shadow[MAX_HEADLIGHT_SHADOWS]; + glm::vec4 headlight_shadow_params[MAX_HEADLIGHT_SHADOWS]; }; - static_assert(sizeof(scene_ubs) == 400, "bad size of ubs"); + static_assert(sizeof(scene_ubs) == 400 + 80 * MAX_HEADLIGHT_SHADOWS, "bad size of ubs"); const size_t MAX_PARAMS = 3; @@ -140,7 +148,8 @@ namespace gl float intensity; float ambient; - // HEADLIGHTS: view space to the space of the beam texture, and the weights of its channels. + // HEADLIGHTS: view space to the space of the beam texture, and the weights of its channels; w: 1 + the shadow map of the + // headlights (scene_ubs::headlight_shadow), 0 when they have none. // 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. the map is in the array of diff --git a/rendering/opengl46/opengl46localshadows.cpp b/rendering/opengl46/opengl46localshadows.cpp index 5a00146f..732612d4 100644 --- a/rendering/opengl46/opengl46localshadows.cpp +++ b/rendering/opengl46/opengl46localshadows.cpp @@ -48,6 +48,22 @@ GLuint make_cube_array(int const Size, int const Cubes) return texture; } +// 2d array of depth maps compared to the reference, one level, Layers layers of Size x Size +GLuint make_array(int const Size, int const Layers) +{ + GLuint texture{0}; + glCreateTextures(GL_TEXTURE_2D_ARRAY, 1, &texture); + glTextureStorage3D(texture, 1, GL_DEPTH_COMPONENT32F, Size, Size, Layers); + // one level, as the cube maps above + glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTextureParameteri(texture, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTextureParameteri(texture, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTextureParameteri(texture, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); + glTextureParameteri(texture, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); + return texture; +} + // framebuffer drawing to all layers of the depth texture (the geometry shader picks the face); 0 if it can't be made GLuint make_framebuffer(GLuint const Texture) { @@ -85,8 +101,11 @@ opengl46_local_shadows::~opengl46_local_shadows() { release(); release_lamps(); + release_headlights(); if (m_empty != 0) glDeleteTextures(1, &m_empty); + if (m_emptyheadlights != 0) + glDeleteTextures(1, &m_emptyheadlights); } void opengl46_local_shadows::init() @@ -114,6 +133,10 @@ void opengl46_local_shadows::init() glTextureParameteri(m_empty, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); float const farthest{0.f}; glClearTexImage(m_empty, 0, GL_DEPTH_COMPONENT, GL_FLOAT, &farthest); + m_emptyheadlights = make_array(1, 1); + glTextureParameteri(m_emptyheadlights, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTextureParameteri(m_emptyheadlights, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glClearTexImage(m_emptyheadlights, 0, GL_DEPTH_COMPONENT, GL_FLOAT, &farthest); } void opengl46_local_shadows::set_layer(int const Layer) @@ -127,13 +150,13 @@ opengl46_local_shadows::level_setup opengl46_local_shadows::setup(int const Leve switch (Level) { case 1: - return {512, 4, 0.f, 1}; + return {512, 4, 0.f, 1, 1024, 2, 8}; case 2: - return {1024, 2, 1.f, 1}; + return {1024, 2, 1.f, 1, 1024, 1, 4}; case 3: - return {1024, 1, 2.f, 2}; + return {1024, 1, 2.f, 2, 2048, 1, 2}; default: - return {0, 0, 0.f, 0}; + return {0, 0, 0.f, 0, 0, 0, 0}; } } @@ -201,10 +224,47 @@ void opengl46_local_shadows::release_lamps() m_wanted.clear(); } +void opengl46_local_shadows::allocate_headlights(int const Count, int const Size) +{ + release_headlights(); + if ((Count <= 0) || (Size <= 0)) + return; + m_headlightstexture = make_array(Size, Count); + glCreateFramebuffers(1, &m_headlightsframebuffer); + glNamedFramebufferTextureLayer(m_headlightsframebuffer, GL_DEPTH_ATTACHMENT, m_headlightstexture, 0, 0); + glNamedFramebufferDrawBuffer(m_headlightsframebuffer, GL_NONE); + glNamedFramebufferReadBuffer(m_headlightsframebuffer, GL_NONE); + if (glCheckNamedFramebufferStatus(m_headlightsframebuffer, GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + { + ErrorLog("headlight shadow map framebuffer setup failed, the headlights have no shadows"); + release_headlights(); + Global.gfx_lights_shadows_headlights = 0; + return; + } + m_headlightssize = Size; + m_headlightslots.assign(static_cast(Count), headlight_slot{}); + WriteLog("headlight shadow maps: " + std::to_string(Count) + " x " + std::to_string(Size) + " x " + std::to_string(Size) + " (" + + std::to_string(Count * Size / 1024 * Size * 4 / 1024) + " MB)"); +} + +void opengl46_local_shadows::release_headlights() +{ + if (m_headlightsframebuffer != 0) + glDeleteFramebuffers(1, &m_headlightsframebuffer); + if (m_headlightstexture != 0) + glDeleteTextures(1, &m_headlightstexture); + m_headlightsframebuffer = 0; + m_headlightstexture = 0; + m_headlightssize = 0; + m_headlightslots.clear(); + m_headlightswanted.clear(); +} + void opengl46_local_shadows::bind() { glBindTextureUnit(gl::LOCAL_SHADOW_TEX, (m_texture != 0) ? m_texture : m_empty); glBindTextureUnit(gl::LOCAL_SHADOW_LAMPS_TEX, (m_lampstexture != 0) ? m_lampstexture : m_empty); + glBindTextureUnit(gl::HEADLIGHT_SHADOW_TEX, (m_headlightstexture != 0) ? m_headlightstexture : m_emptyheadlights); } bool opengl46_local_shadows::update_wanted(TDynamicObject const *Vehicle, TModel3d const *Cab, glm::vec3 const &Lamp, unsigned int const Framestamp) @@ -466,6 +526,154 @@ glm::vec4 opengl46_local_shadows::lamp_weights(std::size_t const Slot) const return glm::vec4{static_cast(cube), SHADOW_NEAR, 2.f / static_cast(std::max(m_lampssize, 1)), setup(m_level).radius}; } +void opengl46_local_shadows::update_headlights(std::vector const &Lights, unsigned int const Framestamp) +{ + m_headlightswanted.clear(); + auto const level{std::clamp(Global.gfx_lights_shadows, 0, 3)}; + auto const count{level > 0 ? std::clamp(Global.gfx_lights_shadows_headlights, 0, static_cast(gl::MAX_HEADLIGHT_SHADOWS)) : 0}; + auto const size{count > 0 ? setup(level).headlightsize : 0}; + if ((static_cast(count) != m_headlightslots.size()) || (size != m_headlightssize)) + { + allocate_headlights(count, size); + } + if (m_headlightslots.empty()) + { + return; + } + // sets which already have a slot keep it + std::vector unplaced; + for (auto const *light : Lights) + { + if (m_headlightswanted.size() + unplaced.size() >= m_headlightslots.size()) + { + break; + } + auto placed{false}; + for (std::size_t idx = 0; idx < m_headlightslots.size(); ++idx) + { + auto &slot{m_headlightslots[idx]}; + if ((slot.owner == light->owner) && (slot.index == light->index) && (slot.wantedframe != Framestamp)) + { + slot.wantedframe = Framestamp; + m_headlightswanted.emplace_back(idx); + placed = true; + break; + } + } + if (false == placed) + { + unplaced.emplace_back(light); + } + } + // the others take the slots of sets no longer picked + for (auto const *light : unplaced) + { + for (std::size_t idx = 0; idx < m_headlightslots.size(); ++idx) + { + auto &slot{m_headlightslots[idx]}; + if (slot.wantedframe == Framestamp) + { + continue; + } + slot = headlight_slot{}; + slot.owner = light->owner; + slot.index = light->index; + slot.wantedframe = Framestamp; + m_headlightswanted.emplace_back(idx); + break; + } + } +} + +void opengl46_local_shadows::reset_headlights() +{ + for (auto &slot : m_headlightslots) + { + slot = headlight_slot{}; + } + m_headlightswanted.clear(); +} + +bool opengl46_local_shadows::headlight_wanted(std::size_t const Slot, bool const Moved, unsigned int const Framestamp) const +{ + auto const &slot{m_headlightslots[Slot]}; + auto const level{setup(m_level)}; + return (false == slot.drawn) || (Framestamp - slot.drawnframe >= (Moved ? level.headlightmoving : level.headlightstill)); +} + +void opengl46_local_shadows::begin_headlight(std::size_t const Slot, glm::dvec3 const &Projector, glm::mat3 const &Rotation, glm::mat4 const &Projection, glm::vec3 const &Direction, unsigned int const Framestamp) +{ + auto &slot{m_headlightslots[Slot]}; + slot.drawn = true; + slot.drawnframe = Framestamp; + slot.projector = Projector; + slot.rotation = Rotation; + slot.projection = Projection; + slot.direction = Direction; + ++m_headlightsdrawn; + + gl::framebuffer::unbind(); + glNamedFramebufferTextureLayer(m_headlightsframebuffer, GL_DEPTH_ATTACHMENT, m_headlightstexture, 0, static_cast(Slot)); + glBindFramebuffer(GL_FRAMEBUFFER, m_headlightsframebuffer); + glViewport(0, 0, m_headlightssize, m_headlightssize); + float const farthest{0.f}; + glClearNamedFramebufferfv(m_headlightsframebuffer, GL_DEPTH, 0, &farthest); + glDisable(GL_BLEND); + glDepthMask(GL_TRUE); + glEnable(GL_DEPTH_TEST); + // as in the shadows of the sun: the back faces, so the lit faces (the ground most of all, which the beam skims) don't shadow themselves + glEnable(GL_CULL_FACE); + glCullFace(GL_FRONT); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); +} + +void opengl46_local_shadows::end_headlight(std::uint32_t const Drawcalls) +{ + gl::framebuffer::unbind(); + m_headlightsframedrawcalls += Drawcalls; +} + +void opengl46_local_shadows::headlights_timer_begin() +{ + m_headlightsframedrawcalls = 0; + m_headlightscpustart = std::chrono::steady_clock::now(); + m_headlightstimer.begin(); + m_headlightstiming = true; +} + +void opengl46_local_shadows::headlights_timer_end() +{ + if (false == m_headlightstiming) + { + return; + } + m_headlightstimer.end(); + m_headlightstiming = false; + m_headlightsdrawcalls = m_headlightsframedrawcalls; + auto const elapsed{std::chrono::duration(std::chrono::steady_clock::now() - m_headlightscpustart).count()}; + m_headlightscputime = (m_headlightscputime == 0.0 ? elapsed : m_headlightscputime * 0.9 + elapsed * 0.1); +} + +int opengl46_local_shadows::headlight_map(TDynamicObject const *Owner, int const Index) const +{ + for (auto const idx : m_headlightswanted) + { + auto const &slot{m_headlightslots[idx]}; + if ((slot.owner == Owner) && (slot.index == Index)) + { + return (slot.drawn ? static_cast(idx) : -1); + } + } + return -1; +} + +glm::vec4 opengl46_local_shadows::headlight_params(std::size_t const Slot) const +{ + // a map spans 2 tan(fov / 2) one metre from the projector, and the projection holds 1 / tan(fov / 2) + auto const &projection{m_headlightslots[Slot].projection}; + return glm::vec4{2.f / (std::max(projection[1][1], 1e-6f) * static_cast(std::max(m_headlightssize, 1))), setup(m_level).radius, 0.f, 0.f}; +} + std::string opengl46_local_shadows::info_times() { std::string text; @@ -473,6 +681,8 @@ std::string opengl46_local_shadows::info_times() text += " lamp shadows: " + to_string(static_cast(*time) / 1000000.0, 3) + " ms\n"; if (auto const time = m_lampstimer.result()) text += " scenery lamp shadows: " + to_string(static_cast(*time) / 1000000.0, 3) + " ms\n"; + if (auto const time = m_headlightstimer.result()) + text += " headlight shadows: " + to_string(static_cast(*time) / 1000000.0, 3) + " ms\n"; return text; } @@ -497,6 +707,17 @@ std::string opengl46_local_shadows::info() const std::to_string(withvehicles) + " with vehicles), drawn " + std::to_string(m_lampsscenery) + " scenery + " + std::to_string(m_lampsvehicles) + " vehicles, " + std::to_string(m_lampsdrawcalls) + " drawcalls, cpu " + to_string(m_lampscputime, 2) + " ms"; } + if (false == m_headlightslots.empty()) + { + auto ready{0}; + for (auto const idx : m_headlightswanted) + { + ready += (m_headlightslots[idx].drawn ? 1 : 0); + } + text += "\nheadlight shadows: " + std::to_string(m_headlightswanted.size()) + " of " + std::to_string(m_headlightslots.size()) + " slots (" + std::to_string(ready) + " ready), " + + std::to_string(m_headlightssize) + " map, drawn " + std::to_string(m_headlightsdrawn) + " times, " + std::to_string(m_headlightsdrawcalls) + " drawcalls, cpu " + + to_string(m_headlightscputime, 2) + " ms"; + } return text; } @@ -512,6 +733,16 @@ void opengl46_local_shadows::draw_debug_ui() ImGui::SliderFloat("their reach from the camera, m (.lamps.range)", &Global.gfx_lights_shadows_lamps_range, 0.f, 500.f); ImGui::SliderFloat("vehicles casting them, from the camera, m (.vehicles.range)", &Global.gfx_lights_shadows_vehicles_range, 0.f, 500.f); if (false == m_slots.empty()) + { + auto const text{info()}; + auto const line{text.find("\nscenery lamp shadows:")}; + auto const end{(line == std::string::npos) ? std::string::npos : text.find('\n', line + 1)}; + if (line != std::string::npos) + ImGui::TextUnformatted(text.substr(line + 1, end == std::string::npos ? std::string::npos : end - line - 1).c_str()); + } + ImGui::SliderInt("headlights with shadows (gfx.lights.shadows.headlights)", &Global.gfx_lights_shadows_headlights, 0, static_cast(gl::MAX_HEADLIGHT_SHADOWS)); + ImGui::SliderFloat("their reach ahead of the vehicle, m (.headlights.range)", &Global.gfx_lights_shadows_headlights_range, 10.f, 1000.f); + if (false == m_headlightslots.empty()) { auto const text{info()}; auto const line{text.rfind('\n')}; diff --git a/rendering/opengl46/opengl46localshadows.h b/rendering/opengl46/opengl46localshadows.h index 4d2540b1..84ce8c1d 100644 --- a/rendering/opengl46/opengl46localshadows.h +++ b/rendering/opengl46/opengl46localshadows.h @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "gl/shader.h" #include "gl/query.h" #include "rendering/opengl46/opengl46locallights.h" +#include "rendering/lightarray.h" class TDynamicObject; class TModel3d; @@ -35,7 +36,13 @@ class TModel3d; // the six faces of a map are drawn at once, by a geometry shader (shadowcube.vert/.geom), so a model costs one draw call per // submodel, as in the colour pass. the depth is n / d (d the distance along the axis of the face), see local_light_shadow() in // light_common.glsl, which reads the map for the light with the layer in its headlight_weights.x, from the array of scenery -// lamps when its linear is 1 and from the one of the cab lamp otherwise +// lamps when its linear is 1 and from the one of the cab lamp otherwise. +// - the headlights of the vehicles (Global.gfx_lights_shadows_headlights sets of them, the most important ones): a plain map, +// drawn from the projector of the beam with its projection (the beam is a narrow frustum aimed along the vehicle, see +// opengl46_renderer::Headlight_Projector()), with what is within Global.gfx_lights_shadows_headlights_range ahead of the +// vehicle, but not the vehicle itself. the map keeps the projector it was drawn with, in world space: one a few frames old is +// still right for what doesn't move. read by headlight_beam_shadow() in light_common.glsl, for the headlights with the map in their +// headlight_weights.w (1 + the layer) and the projection to it in gl::scene_ubs::headlight_shadow class opengl46_local_shadows { public: @@ -132,6 +139,52 @@ class opengl46_local_shadows // the headlight_weights of the light of the lamp in the slot, see weights() glm::vec4 lamp_weights(std::size_t const Slot) const; + // headlights of vehicles + + // a set of headlights (one end of a vehicle) with a slot + struct headlight_slot + { + TDynamicObject const *owner{nullptr}; // light_array::light_record::owner and index + int index{-1}; + unsigned int wantedframe{~0u}; // framestamp it was last among the headlights given shadows + bool drawn{false}; // the map holds the shadows of the headlights + unsigned int drawnframe{0}; + // the projector the map was drawn with: world space position, world axes to its axes, projection + glm::dvec3 projector{0.0}; + glm::mat3 rotation{1.f}; + glm::mat4 projection{1.f}; + glm::vec3 direction{0.f}; // of the light when the map was drawn + }; + // picks the headlights which get shadows in this frame: Lights, the most important first, as many as there are slots + // (Global.gfx_lights_shadows_headlights). a set keeps its slot while it is picked. (re)makes the maps when their count or size changed + void update_headlights(std::vector const &Lights, unsigned int const Framestamp); + // forgets the headlights: the vehicles may be about to go (a scenery being loaded) + void reset_headlights(); + // slots of the headlights picked in this frame + std::vector const &headlights() const + { + return m_headlightswanted; + } + headlight_slot const &headlight(std::size_t const Slot) const + { + return m_headlightslots[Slot]; + } + // whether the map of the slot is to be drawn in this frame: one never drawn, then every few frames by the level, more often + // while the headlights Moved (with the vehicle) + bool headlight_wanted(std::size_t const Slot, bool const Moved, unsigned int const Framestamp) const; + // sets up the drawing to the map of the slot (cleared), drawn from the projector given (see headlight_slot). the geometry is + // to be drawn by the shadow pass with this projection, back faces only + void begin_headlight(std::size_t const Slot, glm::dvec3 const &Projector, glm::mat3 const &Rotation, glm::mat4 const &Projection, glm::vec3 const &Direction, unsigned int const Framestamp); + // ends the drawing to the map of a set of headlights, leaves the default framebuffer bound + void end_headlight(std::uint32_t const Drawcalls); + // gpu timer of the drawing of the maps of the headlights in this frame + void headlights_timer_begin(); + void headlights_timer_end(); + // the slot of the headlights with their map ready in this frame, -1 if there is none + int headlight_map(TDynamicObject const *Owner, int const Index) const; + // the headlight_shadow_params of the map of the slot, see gl::scene_ubs + glm::vec4 headlight_params(std::size_t const Slot) const; + // lines for the gpu times and statistics of the debug panel std::string info_times(); std::string info() const; @@ -139,25 +192,32 @@ class opengl46_local_shadows void draw_debug_ui(); private: - // size, refresh interval (frames), filter radius (texels, 0: the 2x2 filter of the hardware) of a level, and the number of - // scenery parts of lamp maps drawn per frame + // size, refresh interval (frames), filter radius (texels, 0: the 2x2 filter of the hardware) of a level, the number of + // scenery parts of lamp maps drawn per frame, and the size and refresh intervals (frames, while the vehicle moves and while + // it doesn't) of the maps of headlights struct level_setup { int size; unsigned int interval; float radius; int sceneryperframe; + int headlightsize; + unsigned int headlightmoving; + unsigned int headlightstill; }; static level_setup setup(int const Level); void allocate(int const Size); void release(); void allocate_lamps(int const Count, int const Size); void release_lamps(); + void allocate_headlights(int const Count, int const Size); + void release_headlights(); void set_layer(int const Layer); std::unique_ptr m_program; std::unique_ptr m_alpha_program; GLuint m_empty{0}; // stand-in: a cube map array of 1x1 faces with the farthest depth + GLuint m_emptyheadlights{0}; // the same, a 2d array of one 1x1 layer int m_level{-1}; // level the maps were made for // cab lamp GLuint m_texture{0}; // cube map array, one cube @@ -179,6 +239,15 @@ class opengl46_local_shadows int m_sceneryleft{0}; // scenery parts which may still be drawn in this frame gl::gpu_timer m_lampstimer; bool m_lampstiming{false}; + // headlights: 2d array, a layer per slot + GLuint m_headlightstexture{0}; + GLuint m_headlightsframebuffer{0}; + int m_headlightssize{0}; + std::vector m_headlightslots; + std::vector m_headlightswanted; + gl::gpu_timer m_headlightstimer; + bool m_headlightstiming{false}; + std::chrono::steady_clock::time_point m_headlightscpustart; // statistics std::uint32_t m_drawcalls{0}; // of the last drawing of the cab map std::uint32_t m_updatecount{0}; // drawings of the cab map so far @@ -189,4 +258,8 @@ class opengl46_local_shadows std::uint32_t m_lampsdrawcalls{0}; // in the drawings of the lamp maps in the last frame with any std::uint32_t m_lampsframedrawcalls{0}; double m_lampscputime{0.0}; // milliseconds per frame, smoothed + std::uint32_t m_headlightsdrawn{0}; // maps of headlights drawn so far + std::uint32_t m_headlightsdrawcalls{0}; // in the drawings of the maps of headlights in the last frame with any + std::uint32_t m_headlightsframedrawcalls{0}; + double m_headlightscputime{0.0}; // milliseconds per frame, smoothed }; diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index f940d452..4a572e79 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -637,6 +637,7 @@ bool opengl46_renderer::Render() // a scenery is being loaded: the cells, instances and models the lights were taken from may be gone m_locallights.reset(); m_localshadows.reset_lamps(); + m_localshadows.reset_headlights(); } // generate new frame opengl_texture::reset_unit_cache(); @@ -988,6 +989,7 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) { Render_local_shadows(); Render_lamp_shadows(); + Render_headlight_shadows(); } glClearColor(0.0f, 0.0f, 0.0f, 0.0f); @@ -3006,6 +3008,9 @@ void opengl46_renderer::Render(cell_sequence::iterator First, cell_sequence::ite { 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); } } @@ -6434,54 +6439,40 @@ void opengl46_renderer::Update_Lights(light_array &Lights, bool const Headlights light->ambient = headlight_config.ambient; light->intensity = headlight_config.intensity; // headlights-to-world 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 worldcam{ glm::mat3{ m_renderpass.pass_camera.modelview() } }; { // headlight projection is 1 km long box aligned with vehicle rotation and aimed slightly downwards - opengl_camera headlights; - auto const &ownerdimensions{ scenelight.owner->MoverParameters->Dim }; - auto const up{ static_cast( scenelight.owner->VectorUp() ) }; - auto const size{ static_cast( std::max( ownerdimensions.W, ownerdimensions.H ) * 1.0 ) }; // ensure square ratio - auto const cone{ 5.f }; - auto const offset{ 75.f * ( 5.f / cone ) }; - headlights.position() = - scenelight.owner->GetPosition() - - glm::dvec3(scenelight.direction * offset) - + up * ( size * 0.5 ); -/* - headlights.projection() = ortho_projection( - -size, size, - -size, size, - ownerdimensions.L * 0.5 - 0.5, 1000.0f ); -*/ - headlights.projection() = perspective_projection_raw( - glm::radians( cone ), - 1.0, - ownerdimensions.L * 0.5 + offset - 0.25, 1000.0f ); - glm::dmat4 viewmatrix{ 1.0 }; - viewmatrix *= - glm::lookAt( - headlights.position(), - headlights.position() - - up * 1.0 - + glm::dvec3{ scenelight.direction * 1000.f }, - glm::dvec3{ 0.f, 1.f, 0.f } ); - headlights.modelview() = viewmatrix; + glm::dvec3 projector; + glm::mat3 lightcam; + glm::mat4 depthproj; + float nearplane, fieldofview; + Headlight_Projector( scenelight, projector, lightcam, depthproj, nearplane, fieldofview ); // calculate world->headlight space projection matrix - glm::mat4 const depthproj{ headlights.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 lightcam{ glm::mat3{ headlights.modelview() } }; - auto const worldcam{ glm::mat3{ m_renderpass.pass_camera.modelview() } }; - light->headlight_projection = coordmove * depthproj * glm::translate( glm::mat4{ lightcam }, - glm::vec3{ m_renderpass.pass_camera.position() - headlights.position() } ) + glm::vec3{ m_renderpass.pass_camera.position() - projector } ) * glm::mat4{ glm::inverse( worldcam ) }; } // headlights weights light->headlight_weights = { scenelight.state, 0.f }; + // the shadow map of the headlights, if they have one: the same projection, from where the projector was when it was drawn + if( auto const shadowmap{ m_localshadows.headlight_map( scenelight.owner, scenelight.index ) }; shadowmap >= 0 ) { + auto const &slot{ m_localshadows.headlight( static_cast( shadowmap ) ) }; + scene_ubs.headlight_shadow[ shadowmap ] = + coordmove + * slot.projection + * glm::translate( + glm::mat4{ slot.rotation }, + glm::vec3{ m_renderpass.pass_camera.position() - slot.projector } ) + * glm::mat4{ glm::inverse( worldcam ) }; + scene_ubs.headlight_shadow_params[ shadowmap ] = m_localshadows.headlight_params( static_cast( shadowmap ) ); + light->headlight_weights.w = static_cast( shadowmap + 1 ); + } ++light_i; } @@ -6704,6 +6695,159 @@ void opengl46_renderer::Render_lamp_shadow(std::size_t const Slot, bool const Ve glPolygonMode(GL_FRONT_AND_BACK, Global.bWireFrame ? GL_LINE : GL_FILL); } +void opengl46_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)}; // ensure square ratio + 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 = ortho_projection( + -size, size, + -size, size, + ownerdimensions.L * 0.5 - 0.5, 1000.0f ); +*/ + 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}}; +} + +void opengl46_renderer::Render_headlight_shadows() +{ + // the headlights which shine, the most important first (in the order of Update_Lights()), whose beam may be seen within the + // reach of the shadows + m_headlightshadowpicked.clear(); + auto const count{(std::clamp(Global.gfx_lights_shadows, 0, 3) > 0) ? std::clamp(Global.gfx_lights_shadows_headlights, 0, static_cast(gl::MAX_HEADLIGHT_SHADOWS)) : 0}; + if (count > 0) + { + m_headlightshadowcandidates.clear(); + auto const camera{m_colorpass.pass_camera.position()}; + auto const range{static_cast(Global.gfx_lights_shadows_headlights_range)}; + for (auto const &light : simulation::Lights.data) + { + if (light.intensity == 0.f) + continue; + // as in Update_Lights(): by day the light around takes the beam down to nothing + auto luminance{static_cast(Global.fLuminance)}; + if (light.owner->fShade > 0.f) + luminance *= light.owner->fShade; + auto const beam{light.color - glm::vec3{luminance}}; + if (std::max(beam.r, std::max(beam.g, beam.b)) <= 0.f) + continue; + // the part of the beam the shadows are cast in: from the end of the vehicle to the reach ahead of it + auto const &dimensions{light.owner->MoverParameters->Dim}; + auto const radius{range * 0.5 + std::max(dimensions.W, dimensions.H) + 2.0}; + auto const centre{light.position + glm::dvec3{light.direction} * (range * 0.5 + 2.0)}; + if (glm::length(centre - camera) - radius > range) + continue; + if (false == m_colorpass.pass_camera.visible(scene::bounding_area{centre, static_cast(radius)})) + continue; + m_headlightshadowcandidates.emplace_back(static_cast(glm::length2(camera - light.position)) / light.intensity, &light); + } + auto const picked{std::min(m_headlightshadowcandidates.size(), static_cast(count))}; + std::partial_sort(std::begin(m_headlightshadowcandidates), std::begin(m_headlightshadowcandidates) + picked, std::end(m_headlightshadowcandidates), + [](auto const &Left, auto const &Right) { return Left.first < Right.first; }); + for (std::size_t idx = 0; idx < picked; ++idx) + { + m_headlightshadowpicked.emplace_back(m_headlightshadowcandidates[idx].second); + } + } + m_localshadows.update_headlights(m_headlightshadowpicked, m_framestamp); + // the maps may have been made anew, in place of the ones bound for this frame + m_localshadows.bind(); + auto const &slots{m_localshadows.headlights()}; + if (slots.empty()) + return; + + // the occupied cab, as in Render_lamp_shadows() + TDynamicObject *cab{nullptr}; + if ((false == FreeFlyModeFlag) && Global.render_cab && (simulation::Train != nullptr)) + { + cab = simulation::Train->Dynamic(); + if ((cab != nullptr) && ((false == cab->bDisplayCab) || (cab->mdKabina == nullptr) || (cab->mdKabina == cab->mdModel))) + cab = nullptr; + } + auto timing{false}; + for (auto const idx : slots) + { + auto const &slot{m_localshadows.headlight(idx)}; + light_array::light_record const *light{nullptr}; + for (auto const *picked : m_headlightshadowpicked) + { + if ((picked->owner == slot.owner) && (picked->index == slot.index)) + { + light = picked; + break; + } + } + if (light == nullptr) + continue; + glm::dvec3 projector; + glm::mat3 rotation; + glm::mat4 projection; + float nearplane, fieldofview; + Headlight_Projector(*light, projector, rotation, projection, nearplane, fieldofview); + // a vehicle on the move needs its map anew, the things around it and the vehicles passing by only now and then + auto const moved{(glm::length(projector - slot.projector) > 0.01) || (glm::dot(light->direction, slot.direction) < 0.99999f)}; + if (false == m_localshadows.headlight_wanted(idx, moved, m_framestamp)) + continue; + if (false == timing) + { + m_localshadows.headlights_timer_begin(); + timing = true; + } + Render_headlight_shadow(idx, *light, cab); + } + if (timing) + m_localshadows.headlights_timer_end(); +} + +void opengl46_renderer::Render_headlight_shadow(std::size_t const Slot, light_array::light_record const &Light, TDynamicObject *Cab) +{ + glDebug("render headlight shadows"); + glm::dvec3 projector; + glm::mat3 rotation; + glm::mat4 projection; + float nearplane, fieldofview; + Headlight_Projector(Light, projector, rotation, projection, nearplane, fieldofview); + auto const reach{nearplane + Global.gfx_lights_shadows_headlights_range}; + // the shadow pass from the projector of the beam, with its projection. the distances which pick the details of the models + // are measured from the real camera, as in the shadows of the sun + m_renderpass = m_colorpass; + m_renderpass.draw_mode = rendermode::shadows; + m_renderpass.draw_range = reach; + m_renderpass.draw_stats = {}; + auto &camera{m_renderpass.pass_camera}; + camera.position() = projector; + camera.modelview() = glm::mat4{glm::dmat4{glm::dmat3{rotation}} * glm::translate(glm::dmat4{1.0}, -projector)}; + camera.projection() = projection; + // the projection goes on to the infinity, what the shadows are cast by ends at their reach + camera.update_frustum(glm::perspective(fieldofview, 1.f, nearplane, reach)); + setup_matrices(); + auto const colorprojection{scene_ubs.projection}; + scene_ubs.projection = projection; + scene_ubo->update(scene_ubs); + + m_localshadows.begin_headlight(Slot, projector, rotation, projection, Light.direction, m_framestamp); + m_shadowskipvehicle = Light.owner; + Render(simulation::Region); + if ((Cab != nullptr) && (Cab != Light.owner) && (glm::length(Cab->GetPosition() - projector) <= reach + Cab->radius())) + { + Render_cab(Cab, 0.0f, false); + } + m_shadowskipvehicle = nullptr; + m_localshadows.end_headlight(static_cast(m_renderpass.draw_stats.drawcalls)); + scene_ubs.projection = colorprojection; + scene_ubo->update(scene_ubs); + 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) { glDebug("Update_Clusters"); diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index 52ca215b..aa372fc2 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -308,6 +308,14 @@ class opengl46_renderer : public gfx_renderer { void Render_lamp_shadows(); // one part of the map of a scenery lamp: the scenery, or the vehicles by it (and Cab, the occupied cab) over a copy of it void Render_lamp_shadow(std::size_t const Slot, bool const Vehicles, TDynamicObject *Cab); + // picks the headlights which get shadow maps in this frame and draws the maps which need it + void Render_headlight_shadows(); + // the map of the headlights in the slot: the scene within reach ahead of them (and Cab, the occupied cab), without their vehicle + void Render_headlight_shadow(std::size_t const Slot, light_array::light_record const &Light, TDynamicObject *Cab); + // the projector of the beam of a set of headlights: a narrow frustum along the vehicle, from behind it, its near plane at the + // end of the vehicle. the beam texture is projected with it (Update_Lights()) and the shadow map of the headlights drawn with it. + // 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); @@ -556,6 +564,11 @@ class opengl46_renderer : public gfx_renderer { // 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; double m_precipitationrotation; diff --git a/shaders/light_common.glsl b/shaders/light_common.glsl index 9086d532..1e435afe 100644 --- a/shaders/light_common.glsl +++ b/shaders/light_common.glsl @@ -6,6 +6,8 @@ uniform sampler2D headlightmap; layout(binding = LOCAL_SHADOW_TEX) uniform samplerCubeArrayShadow local_shadowmap; // the same for the lamps of scenery models, the lights with linear 1 layout(binding = LOCAL_SHADOW_LAMPS_TEX) uniform samplerCubeArrayShadow local_shadowmap_lamps; +// shadow maps of the headlights of vehicles, see gl::HEADLIGHT_SHADOW_TEX and gl::scene_ubs::headlight_shadow +layout(binding = HEADLIGHT_SHADOW_TEX) uniform sampler2DArrayShadow headlight_shadowmap; #include #include @@ -198,6 +200,52 @@ vec2 calc_dir_light(light_s light, vec3 fragnormal) return calc_light(light_dir, fragnormal); } +// samples of the soft shadows of the lamps and headlights: a disc, rotated per pixel +const vec2 SHADOW_DISC[8] = vec2[](vec2(-0.326, -0.406), vec2(-0.840, -0.074), vec2(-0.696, 0.457), vec2(-0.203, 0.621), + vec2(0.962, -0.195), vec2(0.473, -0.480), vec2(0.519, 0.767), vec2(0.185, -0.893)); + +float shadow_disc_angle() +{ + return 6.2831853 * fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))); +} + +// share of the beam of headlights with a shadow map (Map, headlight_weights.w - 1 of the light) held back before the fragment, +// 0..1. the map was drawn from the projector of the beam: x and y of the projection across it, z / w the depth (near plane over +// the distance along the axis, as the reverse z of the scene), w the distance. the comparison gives 1 where the map holds a +// value at least as large as the reference: a surface closer to the projector +float headlight_beam_shadow(uint map, vec3 fragnormal, vec3 light_dir) +{ + mat4 tomap = headlight_shadow[map]; + vec4 params = headlight_shadow_params[map]; + // a texel of the map is as large as this here. the point is moved along the normal by about a texel, and the reference towards + // the projector by a part of one. only the back faces are in the map, which leaves the lit faces little to shadow themselves with + float texel = params.x * max((tomap * vec4(f_pos.xyz, 1.0)).w, 0.0); + float NdotL = clamp(dot(fragnormal, light_dir), 0.0, 1.0); + vec4 position = tomap * vec4(f_pos.xyz + fragnormal * (texel * (0.5 + 1.5 * (1.0 - NdotL))), 1.0); + if (position.w <= 0.0) + return 0.0; + vec2 coords = position.xy / position.w; + if (any(lessThan(coords, vec2(0.0))) || any(greaterThan(coords, vec2(1.0)))) + return 0.0; + // nearer to the projector than its near plane (the vehicle itself) the reference is past anything in the map + float reference = position.z / max(position.w - texel * 1.5, 1e-3); + float layer = float(map); + float radius = params.y; + if (radius <= 0.0) + return texture(headlight_shadowmap, vec4(coords, layer, reference)); + float angle = shadow_disc_angle(); + vec2 rotation = vec2(cos(angle), sin(angle)); + float spread = radius / float(textureSize(headlight_shadowmap, 0).x); + int count = (radius > 1.5) ? 8 : 4; + float shadow = 0.0; + for (int i = 0; i < count; i++) + { + vec2 offset = vec2(SHADOW_DISC[i].x * rotation.x - SHADOW_DISC[i].y * rotation.y, SHADOW_DISC[i].x * rotation.y + SHADOW_DISC[i].y * rotation.x) * spread; + shadow += texture(headlight_shadowmap, vec4(coords + offset, layer, reference)); + } + return shadow / float(count); +} + // dpdx and dpdy are the change of the view space position of the fragment from a pixel to the next one. the beam texture is // sampled with gradients worked out from them instead of the ones of the hardware: those are undefined where the fragments of // a quad do not go through the same lights, which is what happens at the edges of the clusters @@ -230,6 +278,9 @@ vec2 calc_headlights(light_s light, vec3 fragnormal, vec3 dpdx, vec3 dpdy) vec2 dbeamdy = (dheadlightposdy.xy - beamcoords * dheadlightposdy.w) / headlightpos.w; vec3 lights = textureGrad(headlightmap, beamcoords, dbeamdx, dbeamdy).rgb * light.headlight_weights.rgb; float lightintensity = max(max(lights.r, lights.g), lights.b); + // headlights with a shadow map: what is in the way holds the beam back + if ((light.headlight_weights.w > 0.5) && (lightintensity > 0.0) && (part.x > 0.0)) + lightintensity *= 1.0 - headlight_beam_shadow(uint(light.headlight_weights.w - 0.5), fragnormal, light_dir); return part * atten * lightintensity; } @@ -272,16 +323,14 @@ float local_light_shadow(light_s light, vec3 fragnormal) // 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)))); + float angle = shadow_disc_angle(); 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; + vec2 offset = vec2(SHADOW_DISC[i].x * rotation.x - SHADOW_DISC[i].y * rotation.y, SHADOW_DISC[i].x * rotation.y + SHADOW_DISC[i].y * rotation.x) * spread; shadow += local_light_shadow_tap(position + side * offset.x + up * offset.y, layer, light.headlight_weights.y, bias, lamps); } return shadow / float(count); diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 40028f27..cf23c9eb 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1639,6 +1639,11 @@ global_settings::ConfigParse_gfx_sky( cParser &Parser, std::string_view const To gfx_lights_shadows_lamps_size = std::clamp( gfx_lights_shadows_lamps_size, 128, 2048 ); } else if( Token == "gfx.lights.shadows.lamps.range" ) { readvalue( gfx_lights_shadows_lamps_range, 0.f, 1000.f ); } else if( Token == "gfx.lights.shadows.vehicles.range" ) { readvalue( gfx_lights_shadows_vehicles_range, 0.f, 1000.f ); } + else if( Token == "gfx.lights.shadows.headlights" ) { + Parser.getTokens( 1 ); + Parser >> gfx_lights_shadows_headlights; + gfx_lights_shadows_headlights = std::clamp( gfx_lights_shadows_headlights, 0, 4 ); } + else if( Token == "gfx.lights.shadows.headlights.range" ) { readvalue( gfx_lights_shadows_headlights_range, 10.f, 1000.f ); } else { return false; } return true; @@ -1921,6 +1926,8 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.lights.shadows.lamps.size", gfx_lights_shadows_lamps_size ); export_as_text( Output, "gfx.lights.shadows.lamps.range", gfx_lights_shadows_lamps_range ); export_as_text( Output, "gfx.lights.shadows.vehicles.range", gfx_lights_shadows_vehicles_range ); + export_as_text( Output, "gfx.lights.shadows.headlights", gfx_lights_shadows_headlights ); + export_as_text( Output, "gfx.lights.shadows.headlights.range", gfx_lights_shadows_headlights_range ); 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 6ec01bf0..ab5ca4cf 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -357,6 +357,11 @@ struct global_settings { int gfx_lights_shadows_lamps_size { 512 }; float gfx_lights_shadows_lamps_range { 100.f }; float gfx_lights_shadows_vehicles_range { 50.f }; + // shadows of the headlights of vehicles (a map drawn with the projector of the beam), with gfx_lights_shadows above 0: for how + // many sets of headlights at once (the most important ones, 0-4), and the distance ahead of the vehicle within which things + // cast them, metres + int gfx_lights_shadows_headlights { 2 }; + float gfx_lights_shadows_headlights_range { 250.f }; float ui_fontsize = 13.0f; float ui_scale = 1.0f;