/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" #include "rendering/common/localshadows.h" #include "rendering/openglcamera.h" #include "simulation/simulation.h" #include "utilities/Globals.h" #include "utilities/Logs.h" #include "utilities/utilities.h" #include "vehicle/DynObj.h" #include "vehicle/Train.h" #include "rendering/common/skyocclusion.h" namespace gfx { namespace { // a lamp which moved farther than this (an animated submodel) has its scenery drawn anew, metres double const LAMP_MOVED{0.01}; // lamps with a shorter reach aren't worth a map, metres float const LAMP_RANGE_MIN{1.f}; // near plane of the map of a lamp: out of its sphere, and well within its reach float lamp_near(gfx::local_lights::light const &Light) { return std::clamp(Light.radius, local_shadows::shadow_near, std::max(local_shadows::shadow_near, Light.range * 0.5f)); } // the same for a light of the cab, which is often right by what it lights and what holds its light back (the lights of the desk // under its edge): only what is within a few centimetres, or within its sphere, casts no shadow float const CAB_SHADOW_NEAR{0.03f}; float cab_near(gfx::local_lights::light const &Light) { return std::clamp(Light.radius, CAB_SHADOW_NEAR, std::max(CAB_SHADOW_NEAR, Light.range * 0.5f)); } // lights of the cab with a shorter reach aren't worth a map of their own, metres float const CAB_RANGE_MIN{0.25f}; // a light of the cab without a map of its own takes the shadows of the map of the closest light within this, metres: the desk, the // walls are where they are for both float const CAB_SHARE_DISTANCE{1.0f}; } // namespace local_shadows::level_setup local_shadows::setup(int const Level) { switch (Level) { case 1: return {512, 4, 0.f, 1, 1024, 2, 8}; case 2: return {1024, 2, 1.f, 1, 1024, 1, 4}; case 3: return {1024, 1, 2.f, 2, 2048, 1, 2}; default: return {0, 0, 0.f, 0, 0, 0, 0}; } } void local_shadows::allocate(int const Count, int const Size) { release(); if ((Count <= 0) || (Size <= 0)) return; if (false == gpu_allocate(Count, Size)) { ErrorLog("lamp shadow map framebuffer setup failed, the lamps have no shadows"); release(); Global.gfx_lights_shadows = 0; return; } m_size = Size; m_cabslots.assign(static_cast(Count), cab_slot{}); WriteLog("cab light shadow maps: " + std::to_string(Count) + " x " + std::to_string(Size) + " x " + std::to_string(Size) + " cube (" + std::to_string(Count * 6 * Size / 1024 * Size * 4 / 1024) + " MB)"); } void local_shadows::release() { gpu_release(); m_size = 0; m_cabslots.clear(); m_cabwanted.clear(); m_cabdraws.clear(); } void local_shadows::allocate_lamps(int const Count, int const Size) { release_lamps(); if ((Count <= 0) || (Size <= 0)) return; // the scenery parts and the working copies if (false == gpu_allocate_lamps(Count, Size)) { ErrorLog("scenery lamp shadow map framebuffer setup failed, the scenery lamps have no shadows"); release_lamps(); Global.gfx_lights_shadows_lamps = 0; return; } m_lampssize = Size; m_slots.assign(static_cast(Count), lamp_slot{}); WriteLog("scenery lamp shadow maps: " + std::to_string(Count) + " x " + std::to_string(Size) + " x " + std::to_string(Size) + " cube (" + std::to_string(2 * Count * 6 * Size / 1024 * Size * 4 / 1024) + " MB)"); } void local_shadows::release_lamps() { gpu_release_lamps(); m_lampssize = 0; m_slots.clear(); m_wanted.clear(); } void local_shadows::allocate_headlights(int const Count, int const Size) { release_headlights(); if ((Count <= 0) || (Size <= 0)) return; if (false == gpu_allocate_headlights(Count, Size)) { ErrorLog("headlight shadow map framebuffer setup failed, the headlights have no shadows"); release_headlights(); Global.gfx_lights_shadows_headlights = 0; return; } m_headlightssize = Size; m_headlightslots.assign(static_cast(Count), headlight_slot{}); WriteLog("headlight shadow maps: " + std::to_string(Count) + " x " + std::to_string(Size) + " x " + std::to_string(Size) + " (" + std::to_string(Count * Size / 1024 * Size * 4 / 1024) + " MB)"); } void local_shadows::release_headlights() { gpu_release_headlights(); m_headlightssize = 0; m_headlightslots.clear(); m_headlightswanted.clear(); } void local_shadows::update_cab(TDynamicObject const *Vehicle, TModel3d const *Cab, std::vector const &Lights, unsigned int const Framestamp) { m_cabwanted.clear(); m_cabdraws.clear(); auto const level{std::clamp(Global.gfx_lights_shadows, 0, 3)}; auto const limit{level > 0 ? std::clamp(Global.gfx_lights_shadows_cab, 0, cab_slots_max) : 0}; if ((level != m_level) || (static_cast(limit) < m_cabslots.size())) { m_level = level; release(); } auto const forget = [this]() { for (auto &slot : m_cabslots) { slot = cab_slot{}; } }; if ((limit == 0) || (Vehicle == nullptr) || (Cab == nullptr)) { // with the lights off nothing reads the maps; once they are on again the cab may have changed forget(); m_cabvehicle = nullptr; m_cabmodel = nullptr; return; } if ((Vehicle != m_cabvehicle) || (Cab != m_cabmodel)) { forget(); m_cabvehicle = Vehicle; m_cabmodel = Cab; } // the brightest lights which cast shadows, as many as there may be maps std::vector picked; for (auto const &light : Lights) { if (picked.size() >= static_cast(limit)) { break; } if (light.source.cast_shadows && (light.source.range >= CAB_RANGE_MIN)) { picked.emplace_back(&light); } } if (picked.empty()) { return; } // the maps are made as they're needed, up to the limit: a cab with its lamp alone takes one if (picked.size() > m_cabslots.size()) { allocate(static_cast(picked.size()), setup(level).size); if (m_cabslots.empty()) { return; } } auto const place = [&](cab_slot &Slot, std::size_t const Index, gfx::local_lights::cab_light const &Light) { // the lamp put under the top of a cab model without lights of its own keeps the near plane it had auto const nearplane{Light.source.part == static_cast(Vehicle) ? lamp_near(Light.source) : cab_near(Light.source)}; if ((Slot.lamp != Light.local) || (Slot.nearplane != nearplane)) { Slot.valid = false; } Slot.lamp = Light.local; Slot.nearplane = nearplane; Slot.wantedframe = Framestamp; m_cabwanted.emplace_back(Index); }; // lights which already have a slot keep it, the others take the slots of lights no longer picked std::vector unplaced; for (auto const *light : picked) { auto placed{false}; for (std::size_t idx = 0; idx < m_cabslots.size(); ++idx) { auto &slot{m_cabslots[idx]}; if ((slot.part == light->source.part) && (slot.wantedframe != Framestamp)) { place(slot, idx, *light); placed = true; break; } } if (false == placed) { unplaced.emplace_back(light); } } for (auto const *light : unplaced) { for (std::size_t idx = 0; idx < m_cabslots.size(); ++idx) { auto &slot{m_cabslots[idx]}; if (slot.wantedframe == Framestamp) { continue; } slot = cab_slot{}; slot.part = light->source.part; place(slot, idx, *light); break; } } // the maps to be drawn: those which don't hold their light yet, at once, and those of the moving parts of the cab, every few // frames, a few of them per frame (the ones drawn longest ago first): a cab may have many lights std::vector refresh; for (auto const idx : m_cabwanted) { auto const &slot{m_cabslots[idx]}; if (false == slot.valid) { m_cabdraws.emplace_back(idx); } else if (Framestamp - slot.drawnframe >= setup(level).interval) { refresh.emplace_back(idx); } } auto const refreshes{static_cast(level >= 3 ? 4 : 2)}; if (refresh.size() > refreshes) { std::partial_sort(std::begin(refresh), std::begin(refresh) + refreshes, std::end(refresh), [this, Framestamp](std::size_t const Left, std::size_t const Right) { return Framestamp - m_cabslots[Left].drawnframe > Framestamp - m_cabslots[Right].drawnframe; }); refresh.resize(refreshes); } m_cabdraws.insert(std::end(m_cabdraws), std::begin(refresh), std::end(refresh)); for (auto const idx : m_cabdraws) { m_cabslots[idx].drawnframe = Framestamp; } } void local_shadows::begin(std::size_t const Slot) { if (m_cabdraws.empty() || (Slot == m_cabdraws.front())) { // the first map of the frame m_framedrawcalls = 0; m_cpustart = std::chrono::steady_clock::now(); } m_cabdrawing = Slot; gpu_begin_cab(Slot); } void local_shadows::end(std::uint32_t const Drawcalls) { gpu_end_cab(); m_cabslots[m_cabdrawing].valid = true; m_framedrawcalls += Drawcalls; ++m_updatecount; if (m_cabdraws.empty() || (m_cabdrawing == m_cabdraws.back())) { // the last map of the frame m_drawcalls = m_framedrawcalls; 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); } } int local_shadows::cab_slot_of(TDynamicObject const *Vehicle, gfx::local_lights::cab_light const &Light) const { if ((Vehicle == nullptr) || (Vehicle != m_cabvehicle)) { return -1; } auto closest{-1}; auto closestdistance{CAB_SHARE_DISTANCE}; for (auto const idx : m_cabwanted) { auto const &slot{m_cabslots[idx]}; if (false == slot.valid) { continue; } if ((slot.part == Light.source.part) && (slot.lamp == Light.local)) { return static_cast(idx); } // a light without a map of its own takes the one closest to it auto const distance{glm::length(slot.lamp - Light.local)}; if (Light.source.cast_shadows && (distance <= closestdistance)) { closest = static_cast(idx); closestdistance = distance; } } return closest; } glm::vec4 local_shadows::weights(std::size_t const Slot) const { // a face spans twice the distance from the lamp return glm::vec4{static_cast(Slot), m_cabslots[Slot].nearplane, 2.f / static_cast(std::max(m_size, 1)), setup(m_level).radius}; } void local_shadows::update_lamps(std::vector const &Lights, glm::dvec3 const &Camera, std::uint64_t const Version, unsigned int const Framestamp) { m_frame = Framestamp; m_wanted.clear(); auto const level{std::clamp(Global.gfx_lights_shadows, 0, 3)}; auto const count{level > 0 ? Global.gfx_lights_shadows_lamps : 0}; auto const size{count > 0 ? Global.gfx_lights_shadows_lamps_size : 0}; if ((static_cast(count) != m_slots.size()) || (size != m_lampssize)) { allocate_lamps(count, size); } if (m_slots.empty()) { return; } m_sceneryleft = setup(level).sceneryperframe; auto const reach{static_cast(Global.gfx_lights_shadows_lamps_range)}; // the lights come the most important first: the first ones within reach get the slots std::vector picked; for (auto const &light : Lights) { if (picked.size() >= m_slots.size()) { break; } if ((light.owner == nullptr) || (false == light.cast_shadows) || (light.range < LAMP_RANGE_MIN) || (glm::length(light.position - Camera) > reach)) { continue; } picked.emplace_back(&light); } // lamps which already have a slot keep it std::vector unplaced; for (auto const *light : picked) { auto placed{false}; for (std::size_t idx = 0; idx < m_slots.size(); ++idx) { auto &slot{m_slots[idx]}; if ((slot.owner == light->owner) && (slot.part == light->part) && (slot.wantedframe != Framestamp)) { if (glm::length(slot.position - light->position) > LAMP_MOVED) { slot.scenery = false; slot.vehicles = false; } slot.position = light->position; slot.range = light->range; slot.nearplane = lamp_near(*light); slot.shade_self = light->shade_self; slot.wantedframe = Framestamp; m_wanted.emplace_back(idx); placed = true; break; } } if (false == placed) { unplaced.emplace_back(light); } } // the others take the slots of lamps no longer picked for (auto const *light : unplaced) { for (std::size_t idx = 0; idx < m_slots.size(); ++idx) { auto &slot{m_slots[idx]}; if (slot.wantedframe == Framestamp) { continue; } slot = lamp_slot{}; slot.owner = light->owner; slot.part = light->part; slot.position = light->position; slot.range = light->range; slot.nearplane = lamp_near(*light); slot.shade_self = light->shade_self; slot.wantedframe = Framestamp; m_wanted.emplace_back(idx); break; } } // the scene changed (events, the editor): the scenery parts are out of date for (auto &slot : m_slots) { if (slot.scenery && (slot.version != Version)) { slot.scenery = false; slot.vehicles = false; } } } void local_shadows::reset_lamps() { for (auto &slot : m_slots) { slot = lamp_slot{}; } m_wanted.clear(); } bool local_shadows::scenery_wanted(std::size_t const Slot) { if (m_slots[Slot].scenery || (m_sceneryleft <= 0)) { return false; } --m_sceneryleft; return true; } bool local_shadows::vehicles_wanted(std::size_t const Slot, unsigned int const Framestamp) const { auto const &slot{m_slots[Slot]}; return slot.scenery && ((false == slot.vehicles) || (Framestamp - slot.vehiclesframe >= setup(m_level).interval)); } void local_shadows::begin_lamp(std::size_t const Slot, bool const Vehicles, std::uint64_t const Version, unsigned int const Framestamp) { auto &slot{m_slots[Slot]}; auto const scenerycube{static_cast(Slot)}; auto const workingcube{static_cast(m_slots.size() + Slot)}; if (Vehicles) { // the vehicles go over the scenery slot.vehicles = true; slot.vehiclesframe = Framestamp; ++m_lampsvehicles; } else { slot.scenery = true; slot.version = Version; slot.vehicles = false; ++m_lampsscenery; } m_lampnear = slot.nearplane; gpu_begin_lamp(scenerycube, workingcube, Vehicles); } void local_shadows::end_lamp(std::uint32_t const Drawcalls) { gpu_end_lamp(); m_lampsframedrawcalls += Drawcalls; } void local_shadows::lamps_timer_begin() { m_lampsframedrawcalls = 0; m_cpustart = std::chrono::steady_clock::now(); gpu_lamps_timer(true); m_lampstiming = true; } void local_shadows::lamps_timer_end() { if (false == m_lampstiming) { return; } gpu_lamps_timer(false); m_lampstiming = false; m_lampsdrawcalls = m_lampsframedrawcalls; auto const elapsed{std::chrono::duration(std::chrono::steady_clock::now() - m_cpustart).count()}; m_lampscputime = (m_lampscputime == 0.0 ? elapsed : m_lampscputime * 0.9 + elapsed * 0.1); } local_shadows::lamp_slot const *local_shadows::lamp(gfx::local_lights::light const &Light, std::size_t &Slot) const { if (Light.owner == nullptr) { return nullptr; } for (auto const idx : m_wanted) { auto const &slot{m_slots[idx]}; if ((slot.owner == Light.owner) && (slot.part == Light.part)) { if (false == slot.scenery) { return nullptr; } Slot = idx; return &slot; } } return nullptr; } glm::vec4 local_shadows::lamp_weights(std::size_t const Slot) const { auto const cube{m_slots[Slot].vehicles ? m_slots.size() + Slot : Slot}; return glm::vec4{static_cast(cube), m_slots[Slot].nearplane, 2.f / static_cast(std::max(m_lampssize, 1)), setup(m_level).radius}; } std::vector const &local_shadows::lamp_vehicles(glm::dvec3 const &Camera) { m_lampvehicles.clear(); auto const vehiclesrange{static_cast(Global.gfx_lights_shadows_vehicles_range)}; if (vehiclesrange > 0.0) { for (auto *vehicle : simulation::Vehicles.sequence()) { if (glm::length(vehicle->GetPosition() - Camera) <= vehiclesrange + vehicle->radius()) m_lampvehicles.emplace_back(vehicle); } } return m_lampvehicles; } std::vector const &local_shadows::slot_vehicles(lamp_slot const &Slot) { m_slotvehicles.clear(); for (auto *vehicle : m_lampvehicles) { if (glm::length(vehicle->GetPosition() - Slot.position) <= Slot.range + vehicle->radius()) m_slotvehicles.emplace_back(vehicle); } return m_slotvehicles; } std::vector const &local_shadows::pick_headlights(light_array const &Lights, opengl_camera const &Camera) { m_headlightpicked.clear(); auto const count{(Global.gfx_lights_headlights && (std::clamp(Global.gfx_lights_shadows, 0, 3) > 0)) ? std::clamp(Global.gfx_lights_shadows_headlights, 0, static_cast(gl::MAX_HEADLIGHT_SHADOWS)) : 0}; if (count > 0) { m_headlightcandidates.clear(); auto const camera{Camera.position()}; auto const range{static_cast(Global.gfx_lights_shadows_headlights_range)}; for (auto const &light : Lights.data) { if (light.intensity == 0.f) continue; // as in the light buffers: by day the light around takes the beam down to nothing auto const luminance{static_cast(Global.fLuminance) * gfx::vehicle_light_level(light.owner)}; auto const beam{light.color - glm::vec3{luminance}}; if (std::max(beam.r, std::max(beam.g, beam.b)) <= 0.f) continue; // the part of the beam the shadows are cast in: from the end of the vehicle to the reach ahead of it auto const &dimensions{light.owner->MoverParameters->Dim}; auto const radius{range * 0.5 + std::max(dimensions.W, dimensions.H) + 2.0}; auto const centre{light.position + glm::dvec3{light.direction} * (range * 0.5 + 2.0)}; if (glm::length(centre - camera) - radius > range) continue; if (false == Camera.visible(scene::bounding_area{centre, static_cast(radius)})) continue; m_headlightcandidates.emplace_back(static_cast(glm::length2(camera - light.position)) / light.intensity, &light); } auto const picked{std::min(m_headlightcandidates.size(), static_cast(count))}; std::partial_sort(std::begin(m_headlightcandidates), std::begin(m_headlightcandidates) + picked, std::end(m_headlightcandidates), [](auto const &Left, auto const &Right) { return Left.first < Right.first; }); for (std::size_t idx = 0; idx < picked; ++idx) { m_headlightpicked.emplace_back(m_headlightcandidates[idx].second); } } return m_headlightpicked; } void local_shadows::update_headlights(std::vector const &Lights, unsigned int const Framestamp) { m_headlightswanted.clear(); auto const level{std::clamp(Global.gfx_lights_shadows, 0, 3)}; auto const count{level > 0 ? std::clamp(Global.gfx_lights_shadows_headlights, 0, static_cast(gl::MAX_HEADLIGHT_SHADOWS)) : 0}; auto const size{count > 0 ? setup(level).headlightsize : 0}; if ((static_cast(count) != m_headlightslots.size()) || (size != m_headlightssize)) { allocate_headlights(count, size); } if (m_headlightslots.empty()) { return; } // sets which already have a slot keep it std::vector unplaced; for (auto const *light : Lights) { if (m_headlightswanted.size() + unplaced.size() >= m_headlightslots.size()) { break; } auto placed{false}; for (std::size_t idx = 0; idx < m_headlightslots.size(); ++idx) { auto &slot{m_headlightslots[idx]}; if ((slot.owner == light->owner) && (slot.index == light->index) && (slot.wantedframe != Framestamp)) { slot.wantedframe = Framestamp; m_headlightswanted.emplace_back(idx); placed = true; break; } } if (false == placed) { unplaced.emplace_back(light); } } // the others take the slots of sets no longer picked for (auto const *light : unplaced) { for (std::size_t idx = 0; idx < m_headlightslots.size(); ++idx) { auto &slot{m_headlightslots[idx]}; if (slot.wantedframe == Framestamp) { continue; } slot = headlight_slot{}; slot.owner = light->owner; slot.index = light->index; slot.wantedframe = Framestamp; m_headlightswanted.emplace_back(idx); break; } } } void local_shadows::reset_headlights() { for (auto &slot : m_headlightslots) { slot = headlight_slot{}; } m_headlightswanted.clear(); } bool local_shadows::headlight_wanted(std::size_t const Slot, bool const Moved, unsigned int const Framestamp) const { auto const &slot{m_headlightslots[Slot]}; auto const level{setup(m_level)}; return (false == slot.drawn) || (Framestamp - slot.drawnframe >= (Moved ? level.headlightmoving : level.headlightstill)); } void local_shadows::begin_headlight(std::size_t const Slot, glm::dvec3 const &Projector, glm::mat3 const &Rotation, glm::mat4 const &Projection, glm::vec3 const &Direction, unsigned int const Framestamp) { auto &slot{m_headlightslots[Slot]}; slot.drawn = true; slot.drawnframe = Framestamp; slot.projector = Projector; slot.rotation = Rotation; slot.projection = Projection; slot.direction = Direction; ++m_headlightsdrawn; gpu_begin_headlight(Slot); } void local_shadows::end_headlight(std::uint32_t const Drawcalls) { gpu_end_headlight(); m_headlightsframedrawcalls += Drawcalls; } void local_shadows::headlights_timer_begin() { m_headlightsframedrawcalls = 0; m_headlightscpustart = std::chrono::steady_clock::now(); gpu_headlights_timer(true); m_headlightstiming = true; } void local_shadows::headlights_timer_end() { if (false == m_headlightstiming) { return; } gpu_headlights_timer(false); m_headlightstiming = false; m_headlightsdrawcalls = m_headlightsframedrawcalls; auto const elapsed{std::chrono::duration(std::chrono::steady_clock::now() - m_headlightscpustart).count()}; m_headlightscputime = (m_headlightscputime == 0.0 ? elapsed : m_headlightscputime * 0.9 + elapsed * 0.1); } int local_shadows::headlight_map(TDynamicObject const *Owner, int const Index) const { for (auto const idx : m_headlightswanted) { auto const &slot{m_headlightslots[idx]}; if ((slot.owner == Owner) && (slot.index == Index)) { return (slot.drawn ? static_cast(idx) : -1); } } return -1; } glm::vec4 local_shadows::headlight_params(std::size_t const Slot) const { // a map spans 2 tan(fov / 2) one metre from the projector, and the projection holds 1 / tan(fov / 2) auto const &projection{m_headlightslots[Slot].projection}; return glm::vec4{2.f / (std::max(projection[1][1], 1e-6f) * static_cast(std::max(m_headlightssize, 1))), setup(m_level).radius, 0.f, 0.f}; } TDynamicObject *local_shadows::shadow_cab() { if (FreeFlyModeFlag || (false == Global.render_cab) || (simulation::Train == nullptr)) return nullptr; auto *cab{simulation::Train->Dynamic()}; if ((cab != nullptr) && ((false == cab->bDisplayCab) || (cab->mdKabina == nullptr) || (cab->mdKabina == cab->mdModel))) cab = nullptr; return cab; } std::string local_shadows::info() const { std::string text; if (m_size != 0) { text += "\ncab light shadows: " + std::to_string(m_cabwanted.size()) + " of " + std::to_string(m_cabslots.size()) + " " + std::to_string(m_size) + " cubes, " + std::to_string(m_drawcalls) + " drawcalls, cpu " + to_string(m_cputime, 2) + " ms"; } if (false == m_slots.empty()) { auto withvehicles{0}; auto ready{0}; for (auto const idx : m_wanted) { ready += (m_slots[idx].scenery ? 1 : 0); withvehicles += (m_slots[idx].vehicles ? 1 : 0); } text += "\nscenery lamp shadows: " + std::to_string(m_wanted.size()) + " of " + std::to_string(m_slots.size()) + " slots (" + std::to_string(ready) + " ready, " + std::to_string(withvehicles) + " with vehicles), drawn " + std::to_string(m_lampsscenery) + " scenery + " + std::to_string(m_lampsvehicles) + " vehicles, " + std::to_string(m_lampsdrawcalls) + " drawcalls, cpu " + to_string(m_lampscputime, 2) + " ms"; } if (false == m_headlightslots.empty()) { auto ready{0}; for (auto const idx : m_headlightswanted) { ready += (m_headlightslots[idx].drawn ? 1 : 0); } text += "\nheadlight shadows: " + std::to_string(m_headlightswanted.size()) + " of " + std::to_string(m_headlightslots.size()) + " slots (" + std::to_string(ready) + " ready), " + std::to_string(m_headlightssize) + " map, drawn " + std::to_string(m_headlightsdrawn) + " times, " + std::to_string(m_headlightsdrawcalls) + " drawcalls, cpu " + to_string(m_headlightscputime, 2) + " ms"; } return text; } void local_shadows::draw_debug_ui() { char const *levels[]{"no", "low (512, every 4th frame)", "medium (1024, every 2nd frame)", "high (1024, every frame, softer)"}; ImGui::Combo("lamp shadows (gfx.lights.shadows)", &Global.gfx_lights_shadows, levels, IM_ARRAYSIZE(levels)); ImGui::SliderInt("cab lights with shadows (gfx.lights.shadows.cab)", &Global.gfx_lights_shadows_cab, 0, cab_slots_max); if (m_size != 0) { ImGui::Text("cab: %u of %u cubes %d x %d, drawn %u times, %u drawcalls, %.2f ms (cpu)", static_cast(m_cabwanted.size()), static_cast(m_cabslots.size()), m_size, m_size, m_updatecount, m_drawcalls, m_cputime); } ImGui::SliderInt("scenery lamps with shadows (gfx.lights.shadows.lamps)", &Global.gfx_lights_shadows_lamps, 0, 32); ImGui::SliderFloat("their reach from the camera, m (.lamps.range)", &Global.gfx_lights_shadows_lamps_range, 0.f, 500.f); ImGui::SliderFloat("vehicles casting them, from the camera, m (.vehicles.range)", &Global.gfx_lights_shadows_vehicles_range, 0.f, 500.f); if (false == m_slots.empty()) { auto const text{info()}; auto const line{text.find("\nscenery lamp shadows:")}; auto const end{(line == std::string::npos) ? std::string::npos : text.find('\n', line + 1)}; if (line != std::string::npos) ImGui::TextUnformatted(text.substr(line + 1, end == std::string::npos ? std::string::npos : end - line - 1).c_str()); } ImGui::SliderInt("headlights with shadows (gfx.lights.shadows.headlights)", &Global.gfx_lights_shadows_headlights, 0, static_cast(gl::MAX_HEADLIGHT_SHADOWS)); ImGui::SliderFloat("their reach ahead of the vehicle, m (.headlights.range)", &Global.gfx_lights_shadows_headlights_range, 10.f, 1000.f); if (false == m_headlightslots.empty()) { auto const text{info()}; auto const line{text.rfind('\n')}; ImGui::TextUnformatted(text.c_str() + (line == std::string::npos ? 0 : line + 1)); } } } // namespace gfx