From 2a876befb0573b368151f7c3d81f404558e5a585 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Tue, 27 Oct 2026 16:02:36 +0100 Subject: [PATCH] Shade the sky and the sun by the scenery around the camera instead of the track environment flags --- CMakeLists.txt | 3 + docs/opengl46-renderer.md | 48 + gl/glsl_common.cpp | 2 + gl/ubo.h | 16 +- rendering/common/locallights.cpp | 3 +- rendering/common/localshadows.cpp | 5 +- rendering/common/scenewalk.cpp | 59 +- rendering/common/skyocclusion.cpp | 1314 +++++++++++++++++++ rendering/common/skyocclusion.h | 244 ++++ rendering/opengl46/opengl46renderer.cpp | 21 +- rendering/opengl46/opengl46renderer.h | 3 + rendering/opengl46/opengl46skyocclusion.cpp | 97 ++ rendering/opengl46/opengl46skyocclusion.h | 37 + rendering/vulkan14/vulkan14bindings.h | 6 +- rendering/vulkan14/vulkan14context.h | 4 + rendering/vulkan14/vulkan14debug.cpp | 6 + rendering/vulkan14/vulkan14frame.cpp | 23 +- rendering/vulkan14/vulkan14scene.cpp | 33 + rendering/vulkan14/vulkan14shaders.cpp | 2 + rendering/vulkan14/vulkan14skyocclusion.cpp | 160 +++ rendering/vulkan14/vulkan14skyocclusion.h | 79 ++ shaders/ogl46/common.glsl | 6 + shaders/ogl46/fog_inject.comp | 7 +- shaders/ogl46/light_common.glsl | 9 +- shaders/ogl46/particle_light.glsl | 8 +- shaders/ogl46/particles_precipitation.comp | 2 + shaders/ogl46/sky_occlusion.glsl | 73 ++ shaders/vk14/common.glsl | 8 +- shaders/vk14/fog_inject.comp | 7 +- shaders/vk14/light_common.glsl | 9 +- shaders/vk14/particle_light.glsl | 8 +- shaders/vk14/particles_precipitation.comp | 2 + shaders/vk14/sky_occlusion.glsl | 73 ++ utilities/Globals.cpp | 17 + utilities/Globals.h | 12 + 35 files changed, 2351 insertions(+), 55 deletions(-) create mode 100644 rendering/common/skyocclusion.cpp create mode 100644 rendering/common/skyocclusion.h create mode 100644 rendering/opengl46/opengl46skyocclusion.cpp create mode 100644 rendering/opengl46/opengl46skyocclusion.h create mode 100644 rendering/vulkan14/vulkan14skyocclusion.cpp create mode 100644 rendering/vulkan14/vulkan14skyocclusion.h create mode 100644 shaders/ogl46/sky_occlusion.glsl create mode 100644 shaders/vk14/sky_occlusion.glsl diff --git a/CMakeLists.txt b/CMakeLists.txt index 7eccd8b9..6d697ae4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -329,6 +329,7 @@ set(SOURCES "rendering/common/shadowfit.cpp" "rendering/common/localshadows.cpp" "rendering/common/cabprobe.cpp" +"rendering/common/skyocclusion.cpp" "rendering/common/terrainmaterials.cpp" "rendering/common/grassfield.cpp" "rendering/common/gpuparticles.cpp" @@ -426,6 +427,7 @@ if (WITH_OPENGL_MODERN) "rendering/opengl46/opengl46ao.cpp" "rendering/opengl46/opengl46localshadows.cpp" "rendering/opengl46/opengl46cabprobe.cpp" + "rendering/opengl46/opengl46skyocclusion.cpp" "rendering/opengl46/opengl46instancecull.cpp" "rendering/opengl46/opengl46hiz.cpp" "rendering/opengl46/opengl46shadowfit.cpp" @@ -476,6 +478,7 @@ if (WITH_VULKAN) "rendering/vulkan14/vulkan14shadows.cpp" "rendering/vulkan14/vulkan14lamps.cpp" "rendering/vulkan14/vulkan14cabprobe.cpp" + "rendering/vulkan14/vulkan14skyocclusion.cpp" "rendering/vulkan14/vulkan14terrain.cpp" "rendering/vulkan14/vulkan14grass.cpp" "rendering/vulkan14/vulkan14particles.cpp" diff --git a/docs/opengl46-renderer.md b/docs/opengl46-renderer.md index e1e2782a..ea751a36 100644 --- a/docs/opengl46-renderer.md +++ b/docs/opengl46-renderer.md @@ -269,6 +269,54 @@ the reflections of the cab. | `gfx.cabprobe.bounce` | `1.0` | 0..4: share of the light of the lamp the cab reflects which lights it again | | `gfx.cabprobe.reflections yes/no` | `yes` | the cab reflects itself where the screen space reflections find nothing; `no`: the sky, as before | +### Tunnels, bridges and roofs: the occlusion of the sky + +The light of the sky and of the sun is held back by the geometry around, not by the environment flags of the tracks +(`tunnel`, `canyon`): the tracks and the vehicles on them are drawn alike wherever they are, and a tunnel is dark because the +hill and the vault over it hide the sky. A track with the `tunnel` flag and nothing built over it is lit as in the open; the +flag stays in the scenery files (the sound in the tunnel, the driver turning the lights on). `gfx.tunnelflags yes` brings the +old darkening by the flags back. + +The scenery around the camera is laid out seen from above, in tiles, in the background (a thread of its own): for each texel +the height of the highest surface (the top) and of the lowest one facing down (the ceiling: the vault of a tunnel, the deck of a +bridge seen from below, a roof). Taken are the shapes (the terrain, the heightmap terrain, roads, platforms) and the opaque parts +of the models at their nearest level of detail; not the vehicles, the tracks, the catenary, nor what is see-through (trees). +Things narrower than about three texels (poles, masts, fences) are left out. From the heights around, each texel gets: +- the share of the sky a surface facing up sees above the top, and under the ceiling (light comes in under the vault from the + portal, along the tunnel); +- the heights between which the sun (the moon at night) comes in: above the top what is higher than the shadow of the hills + and buildings towards it, under the ceiling what the rays reach under the vault from the portal (a low sun shines a little way + into a tunnel). + +The shading takes the ambient light, the reflection of the sky and the sun by them, wherever a surface is (the half of a train +in the tunnel is dark, the other half in the light, with the passage over a few metres at the portal). The headlights, the lamps +and the light of the cab are direct light and are not held back; the headlights fade in daylight by the light around the +vehicle, which is read from the same maps, so in a tunnel they light by day as by night. The rain and the snow don't fall under +the roofs, bridges and in tunnels, and the light of the volumetric fog and of the smoke is held back as well. A tile is laid out +once, when it comes into the square around the camera, and again only when its scenery changes (the editor, the streamed +terrain, models shown or hidden by events); the sun is followed every quarter of a degree. + +| Level | Square around the camera | Texel | Searched around a texel (sky / sun) | +|---|---|---|---| +| `low` (1) | 256 m (at least 96 m on each side) | 1 m | 32 m in 8 directions / 64 m | +| `medium` (2) | 512 m (at least 192 m) | 1 m | 48 m in 12 directions / 128 m | +| `high` (3) | 512 m (at least 192 m) | 0.5 m | 64 m in 16 directions / 128 m | + +Cost: on the main thread, the triangles of a tile gathered when it comes in (one tile a frame, more at the start), a fraction +of a millisecond; in the background, a few to some tens of milliseconds per tile, which doesn't hold the frame up. On the GPU, +two textures (`medium`: 3 MB, `high`: 12 MB) and five texture reads per pixel in the shading. With `no` nothing of it is made +and the scenery is lit as in the open everywhere. + +| Entry | Default | Meaning | +|---|---|---| +| `gfx.skyvisibility` | `medium` | `no`/`low`/`medium`/`high` (or 0..3), see above | +| `gfx.skyvisibility.sun yes/no` | `yes` | the sun is held back too (beyond the reach of the shadow map, and in the tunnels whatever the shadow map holds); `no`: the sun only by the shadow map | +| `gfx.skyvisibility.fog yes/no` | `yes` | the light of the volumetric fog is held back too | +| `gfx.tunnelflags yes/no` | `no` | `yes`: the flags `tunnel`/`canyon` of the tracks darken the tracks and the vehicles on them, as before (on top of the occlusion) | + +All of them can be changed while the simulator runs, in the **Sky and exposure** debug window, which also shows views of the +maps around the camera (the share of the sky above the top and under the ceilings, the sun, the heights). + ### Contact shadows Short shadows where things touch or nearly touch: the rails on the sleepers, bolts and edges of panels, a wheel on the rail, diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index a6b6ef33..3c3f0b5e 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -100,6 +100,8 @@ void gl::glsl_common_setup() "#define SCENE_AO_TEX " + std::to_string(SCENE_AO_TEX) + "\n" + "#define CAB_PROBE_TEX " + std::to_string(CAB_PROBE_TEX) + "\n" + "#define CAB_PROBE_ALBEDO_TEX " + std::to_string(CAB_PROBE_ALBEDO_TEX) + "\n" + + "#define SKY_OCCLUSION_HEIGHT_TEX " + std::to_string(SKY_OCCLUSION_HEIGHT_TEX) + "\n" + + "#define SKY_OCCLUSION_TEX " + std::to_string(SKY_OCCLUSION_TEX) + "\n" + "#define CAB_PROBE_SSBO_BINDING " + std::to_string(CAB_PROBE_SSBO_BINDING) + "\n" + "#define CAB_PROBE_SH_SIZE " + std::to_string(CAB_PROBE_SH_SIZE) + "\n" + "#define CAB_PROBE_SH_GROUP_SIZE " + std::to_string(CAB_PROBE_SH_GROUP_SIZE) + "\n" + diff --git a/gl/ubo.h b/gl/ubo.h index 8fcc5e1b..695044c8 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -91,6 +91,11 @@ namespace gl // the cab and of its albedo. bound directly, like the ones above const size_t CAB_PROBE_TEX = MAX_TEXTURES + HELPER_TEXTURES + 15; const size_t CAB_PROBE_ALBEDO_TEX = MAX_TEXTURES + HELPER_TEXTURES + 16; + // the occlusion of the sky and of the sun by the scenery around the camera (gfx::sky_occlusion, gl::scene_ubs::sky_occlusion_*): + // the height of the top of the scenery (R32F) and the shares of the sky and the limits of the key light (RGBA16F), toroidal maps + // read texel by texel with textureGather. bound directly, like the ones above + const size_t SKY_OCCLUSION_HEIGHT_TEX = MAX_TEXTURES + HELPER_TEXTURES + 17; + const size_t SKY_OCCLUSION_TEX = MAX_TEXTURES + HELPER_TEXTURES + 18; struct scene_ubs { @@ -154,9 +159,18 @@ namespace gl glm::vec4 cab_probe_lamp; glm::vec4 cab_probe_lamp_color; glm::vec4 cab_probe_lamp_shadow; + // the occlusion of the sky and of the sun by the scenery around the camera (gfx.skyvisibility, gfx::sky_occlusion), zero when + // there are no maps: x, y the world x, z of the camera of the pass wrapped to the size of the maps (the place of the camera on + // them), metres; z the world height of the camera; w 1 with the maps, 2 when the volumetric fog reads them too + glm::vec4 sky_occlusion_origin; + // x the size of the maps, metres, y their texels across, z the distance from the middle of the maps (the larger of the ones + // along x and z) the occlusion fades out from, w the distance it fades over + glm::vec4 sky_occlusion_params; + // xy the middle of the maps (the main camera they follow) less the camera of the pass, world x and z, metres; zw unused + glm::vec4 sky_occlusion_center; }; - static_assert(sizeof(scene_ubs) == 864 + 80 * MAX_HEADLIGHT_SHADOWS, "bad size of ubs"); + static_assert(sizeof(scene_ubs) == 912 + 80 * MAX_HEADLIGHT_SHADOWS, "bad size of ubs"); const size_t MAX_PARAMS = 3; diff --git a/rendering/common/locallights.cpp b/rendering/common/locallights.cpp index e6f2a759..2e1eafc0 100644 --- a/rendering/common/locallights.cpp +++ b/rendering/common/locallights.cpp @@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/. #include #include "utilities/Globals.h" +#include "rendering/common/skyocclusion.h" #include "utilities/utilities.h" #include "scene/scene.h" #include "model/AnimModel.h" @@ -320,7 +321,7 @@ void gfx::local_lights::gather_cab_lamp(gfx::geometrybank_manager const &Geometr return; } // as bright as the ambient light it used to add to the cab: lost in the daylight - auto const luminance{Global.fLuminance * (vehicle->fShade > 0.0f ? vehicle->fShade : 1.0f)}; + auto const luminance{Global.fLuminance * gfx::vehicle_light_level(vehicle)}; auto const level{vehicle->InteriorLightLevel * std::clamp(1.25f - static_cast(luminance), 0.f, 1.f)}; if (level <= 0.f) { diff --git a/rendering/common/localshadows.cpp b/rendering/common/localshadows.cpp index e7785c42..59c03a2a 100644 --- a/rendering/common/localshadows.cpp +++ b/rendering/common/localshadows.cpp @@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/utilities.h" #include "vehicle/DynObj.h" #include "vehicle/Train.h" +#include "rendering/common/skyocclusion.h" namespace gfx { @@ -409,9 +410,7 @@ std::vector const &local_shadows::pick_headli if (light.intensity == 0.f) continue; // as in the light buffers: by day the light around takes the beam down to nothing - auto luminance{static_cast(Global.fLuminance)}; - if (light.owner->fShade > 0.f) - luminance *= light.owner->fShade; + auto const 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; diff --git a/rendering/common/scenewalk.cpp b/rendering/common/scenewalk.cpp index c49b803e..5acf150f 100644 --- a/rendering/common/scenewalk.cpp +++ b/rendering/common/scenewalk.cpp @@ -26,10 +26,23 @@ http://mozilla.org/MPL/2.0/. #include "world/TrackSleepers.h" #include "model/AnimModel.h" #include "model/Model3d.h" +#include "rendering/common/skyocclusion.h" namespace gfx { +namespace +{ + +// the environment of a track the drawing takes into its light: the flag of the track (a tunnel, a canyon) only with +// Global.gfx_tunnelflags. otherwise the light is the one of the geometry around it (gfx::sky_occlusion) +TEnvironmentType track_environment(TTrack const *Track) +{ + return (Global.gfx_tunnelflags ? Track->eEnvironment : e_flat); +} + +} // namespace + // the top view of the editor shows everything this close to the camera, the corners of the screen included float scene_walk::editor_ortho_reach() { @@ -150,13 +163,11 @@ bool scene_walk::Render_lowpoly( TDynamicObject *Dynamic, float const Squaredist * ( std::max( 0.3, ( Global.fLuminance - Global.Overcast ) - * ( Dynamic->fShade > 0.0 ? - Dynamic->fShade : - 1.0 ) ) ) ) }; + * gfx::vehicle_light_level( Dynamic ) ) ) ) }; walk_sunlight( std::clamp( ( - Dynamic->fShade > 0.f ? - Dynamic->fShade : + gfx::vehicle_shade( Dynamic ) > 0.f ? + gfx::vehicle_shade( Dynamic ) : 1.f ) - luminance, 0.f, 1.f ) ); @@ -166,7 +177,7 @@ bool scene_walk::Render_lowpoly( TDynamicObject *Dynamic, float const Squaredist // renderowanie nieprzezroczystego Ĺ‚adunku Render( Dynamic->mdLoad, Dynamic->Material(), Squaredistance, { 0.f, Dynamic->LoadOffset, 0.f }, {} ); } - walk_sunlight( Dynamic->fShade > 0.f ? Dynamic->fShade : 1.f ); + walk_sunlight( gfx::vehicle_shade( Dynamic ) > 0.f ? gfx::vehicle_shade( Dynamic ) : 1.f ); } else { // Render_Alpha( Dynamic->mdLowPolyInt, Dynamic->Material(), Squaredistance ); @@ -179,7 +190,7 @@ bool scene_walk::Render_lowpoly( TDynamicObject *Dynamic, float const Squaredist if( Setup ) { - if( Dynamic->fShade > 0.0f ) { + if( gfx::vehicle_shade( Dynamic ) > 0.0f ) { // restore regular light level walk_sunlight(); } @@ -1659,10 +1670,10 @@ bool scene_walk::Render(TDynamicObject *Dynamic) case rendermode::color: case rendermode::reflections: { - if (Dynamic->fShade > 0.0f) + if (gfx::vehicle_shade(Dynamic) > 0.0f) { // change light level based on light level of the occupied track - walk_sunlight(Dynamic->fShade); + walk_sunlight(gfx::vehicle_shade(Dynamic)); } // render @@ -1690,7 +1701,7 @@ bool scene_walk::Render(TDynamicObject *Dynamic) Render_coupler_adapter( Dynamic, squaredistance, end::rear ); // post-render cleanup - if (Dynamic->fShade > 0.0f) + if (gfx::vehicle_shade(Dynamic) > 0.0f) { // restore regular light level walk_sunlight(); @@ -1782,14 +1793,14 @@ bool scene_walk::Render_cab(TDynamicObject const *Dynamic, float const Lightleve case rendermode::color: { // render path specific setup: - if (Dynamic->fShade > 0.0f) + if (gfx::vehicle_shade(Dynamic) > 0.0f) { // change light level based on light level of the occupied track - walk_sunlight(Dynamic->fShade); + walk_sunlight(gfx::vehicle_shade(Dynamic)); } walk_ambient_save(); - auto const luminance { Global.fLuminance * ( Dynamic->fShade > 0.0f ? Dynamic->fShade : 1.0f ) }; + auto const luminance { Global.fLuminance * gfx::vehicle_light_level( Dynamic ) }; if( Lightlevel > 0.f ) { // crude way to light the cabin, until we have something more complete in place. with the lamp of the cab among the // lights a share of it is kept for the light which bounces around the cab, unless the probe of the cab gives that light @@ -2174,11 +2185,11 @@ void scene_walk::Render(scene::basic_cell::path_sequence::const_iterator First, case rendermode::color: case rendermode::reflections: { - if (track->eEnvironment != e_flat) + if (track_environment(track) != e_flat) { // track with light level of its own is drawn on its own flush_batch(); - walk_environment(track->eEnvironment); + walk_environment(track_environment(track)); walk_material(track->m_blendmaterial1 != null_handle ? track->m_blendmaterial1 : track->m_material1); walk_draw(std::begin(track->Geometry1), std::end(track->Geometry1)); // restore default lighting @@ -2244,10 +2255,10 @@ void scene_walk::Render(scene::basic_cell::path_sequence::const_iterator First, case rendermode::color: case rendermode::reflections: { - if (track->eEnvironment != e_flat) + if (track_environment(track) != e_flat) { flush_batch(); - walk_environment(track->eEnvironment); + walk_environment(track_environment(track)); walk_material(track->m_blendmaterial2 != null_handle ? track->m_blendmaterial2 : track->m_material2); walk_draw(std::begin(track->Geometry2), std::end(track->Geometry2)); // restore default lighting @@ -2311,10 +2322,10 @@ void scene_walk::Render(scene::basic_cell::path_sequence::const_iterator First, case rendermode::color: case rendermode::reflections: { - if (track->eEnvironment != e_flat) + if (track_environment(track) != e_flat) { flush_batch(); - walk_environment(track->eEnvironment); + walk_environment(track_environment(track)); walk_material(track->m_blendmaterial3 != null_handle ? track->m_blendmaterial3 : track->SwitchExtension->m_material3); walk_draw(track->SwitchExtension->Geometry3); // restore default lighting @@ -2373,10 +2384,10 @@ void scene_walk::Render(scene::basic_cell::path_sequence::const_iterator First, { continue; } - if (track->eEnvironment != e_flat) + if (track_environment(track) != e_flat) { flush_batch(); - walk_environment(track->eEnvironment); + walk_environment(track_environment(track)); walk_material(cuts.first); walk_draw(std::begin(*cuts.second), std::end(*cuts.second)); walk_environment(); @@ -2762,10 +2773,10 @@ bool scene_walk::Render_Alpha(TDynamicObject *Dynamic) ::glTranslated(originoffset.x, originoffset.y, originoffset.z); ::glMultMatrixd(glm::value_ptr(Dynamic->mMatrix)); - if (Dynamic->fShade > 0.0f) + if (gfx::vehicle_shade(Dynamic) > 0.0f) { // change light level based on light level of the occupied track - walk_sunlight(Dynamic->fShade); + walk_sunlight(gfx::vehicle_shade(Dynamic)); } // render @@ -2790,7 +2801,7 @@ bool scene_walk::Render_Alpha(TDynamicObject *Dynamic) } // post-render cleanup - if (Dynamic->fShade > 0.0f) + if (gfx::vehicle_shade(Dynamic) > 0.0f) { // restore regular light level walk_sunlight(); diff --git a/rendering/common/skyocclusion.cpp b/rendering/common/skyocclusion.cpp new file mode 100644 index 00000000..9553a0fb --- /dev/null +++ b/rendering/common/skyocclusion.cpp @@ -0,0 +1,1314 @@ +/* +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/skyocclusion.h" + +#include +#include + +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "rendering/renderer.h" +#include "scene/scene.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/utilities.h" +#include "vehicle/DynObj.h" + +namespace gfx +{ + +namespace +{ + +// the heights of a texel nothing covers: no top (lower than any scenery), no ceiling +float const NONE_TOP{-1.0e4f}; +float const NONE_CEILING{1.0e4f}; +bool has_top(float const Top) +{ + return Top > 0.5f * NONE_TOP; +} +bool has_ceiling(float const Ceiling) +{ + return Ceiling < 0.5f * NONE_CEILING; +} +// the limits of the heights between which the key light is let through, relative to the top: lit always, never +float const LIT_ABOVE_ALWAYS{-1000.f}; +float const LIT_BELOW_ALWAYS{1000.f}; +float const LIT_NEVER_BELOW{-1000.f}; +// how high above the top the share of the sky above it is worked out (a surface on the ground isn't held back by the slope it's on), +// and how far below the ceiling the share under it (the middle of the height under a vault of a tunnel or a deck of a bridge) +float const ABOVE_TOP{1.f}; +float const BELOW_CEILING{2.5f}; +// a fragment this far below the top of its texel is under it, fully this much further. as in light_common.glsl, sky_occlusion() +float const COVER_START{0.75f}; +float const COVER_SPAN{1.f}; +// half the height over which the key light comes in at the edge of a shadow, metres. as in light_common.glsl +float const SUN_SOFTNESS{0.6f}; +// a key light moving more than this (the cosine of the angle) has the occlusion of the key light worked out anew +double const KEYLIGHT_MOVED{0.99999}; // ~0.26 degree + +sky_occlusion *g_current{nullptr}; + +int wrap(int const Value, int const Size) +{ + auto const result{Value % Size}; + return (result < 0 ? result + Size : result); +} + +double wrap(double const Value, double const Size) +{ + return Value - std::floor(Value / Size) * Size; +} + +std::uint64_t hash_mix(std::uint64_t Hash, std::uint64_t const Value) +{ + Hash ^= Value + 0x9e3779b97f4a7c15ull + (Hash << 6) + (Hash >> 2); + return Hash; +} + +std::uint64_t hash_mix_double(std::uint64_t Hash, double const Value) +{ + std::uint64_t bits; + std::memcpy(&bits, &Value, sizeof(bits)); + return hash_mix(Hash, bits); +} + +// the share of the sky of an elevation band, weighed by the cosine on a surface facing up, from the tangents of its ends +float sky_share(float const Tangent) +{ + if (Tangent <= 0.f) + return 0.f; + if (Tangent >= 1.0e6f) + return 1.f; + auto const squared{Tangent * Tangent}; + return squared / (1.f + squared); +} + +float smoothstep(float const Edge0, float const Edge1, float const Value) +{ + auto const t{std::clamp((Value - Edge0) / (Edge1 - Edge0), 0.f, 1.f)}; + return t * t * (3.f - 2.f * t); +} + +// the occlusion of a point at Height, over a texel of the given top and occlusion values: the share of the sky and of the key +// light. the same as light_common.glsl, sky_occlusion_texel() +glm::vec2 texel_occlusion(float const Top, glm::vec4 const &Occlusion, float const Height) +{ + auto const cover{std::clamp((Top - Height - COVER_START) / COVER_SPAN, 0.f, 1.f)}; + auto const sky{Occlusion.x + (Occlusion.y - Occlusion.x) * cover}; + auto const suntop{smoothstep(-SUN_SOFTNESS, SUN_SOFTNESS, Height - (Top + Occlusion.z))}; + auto const sununder{smoothstep(-SUN_SOFTNESS, SUN_SOFTNESS, (Top + Occlusion.w) - Height)}; + return {sky, suntop + (sununder - suntop) * cover}; +} + +// the geometry of a section made (and its binary terrain loaded) as the walk of the scene makes it when the section is drawn first, +// so what is gathered and signed is the scenery as it's drawn. the radii of the shapes are worked out before, from their vertices, +// which go to the geometry bank (a radius asked for after that would be 0) +void ready_section(scene::basic_section *Section) +{ + if (false == Section->m_geometrycreated) + { + for (auto &shape : Section->m_shapes) + shape.radius(); + for (auto &cell : Section->m_cells) + { + for (auto &shape : cell.m_shapesopaque) + shape.radius(); + } + } + Section->create_geometry(); +} + +// the reach of a shape for the tests against a tile: none known (0, the radius worked out too late) means the shape is tested +// triangle by triangle +double shape_reach(scene::shape_node &Shape) +{ + auto const radius{static_cast(Shape.radius())}; + return (radius > 0.0 ? radius : std::numeric_limits::max()); +} + +} // namespace + +sky_occlusion::layout sky_occlusion::layout_of(int const Level) +{ + layout result; + switch (Level) + { + case 1: + // 256 texels of 1 m, the occlusion of the sky searched 32 m around + result.tile = 32.f; + result.tile_texels = 32; + result.azimuths = 8; + result.steps = 8; + result.reach = 32.f; + result.sun_steps = 12; + result.sun_reach = 64.f; + break; + case 3: + // 1024 texels of 0.5 m + result.tile = 64.f; + result.tile_texels = 128; + result.azimuths = 16; + result.steps = 12; + result.reach = 64.f; + result.sun_steps = 24; + result.sun_reach = 128.f; + break; + default: + // 512 texels of 1 m + result.tile = 64.f; + result.tile_texels = 64; + result.azimuths = 12; + result.steps = 10; + result.reach = 48.f; + result.sun_steps = 16; + result.sun_reach = 128.f; + break; + } + result.tiles = 8; + return result; +} + +sky_occlusion::~sky_occlusion() +{ + // the backend lets go of its textures itself, it's gone already + if (m_thread.joinable()) + { + { + std::lock_guard lock{m_mutex}; + m_quit = true; + } + m_wake.notify_all(); + m_thread.join(); + } + if (g_current == this) + g_current = nullptr; +} + +sky_occlusion const *sky_occlusion::current() +{ + return (g_current != nullptr && g_current->active() ? g_current : nullptr); +} + +void sky_occlusion::start(int const Level) +{ + m_layout = layout_of(Level); + auto const side{m_layout.side()}; + if (false == gpu_allocate(side)) + { + ErrorLog("sky occlusion: the maps can't be made, no occlusion of the sky"); + gpu_release(); + Global.gfx_skyvisibility = 0; + m_level = 0; + m_side = 0; + return; + } + m_side = side; + m_level = Level; + m_sun = Global.gfx_skyvisibility_sun; + auto const count{static_cast(m_layout.tiles * m_layout.tiles)}; + m_slots.assign(count, slot{}); + auto const texels{static_cast(side) * side}; + m_heights.assign(texels, NONE_TOP); + m_occlusion.assign(texels, glm::vec4{1.f, 1.f, LIT_ABOVE_ALWAYS, LIT_BELOW_ALWAYS}); + auto const tiletexels{static_cast(m_layout.tile_texels) * m_layout.tile_texels}; + m_neutralheights.assign(tiletexels, NONE_TOP); + m_neutralocclusion.resize(tiletexels * 4); + for (std::size_t i = 0; i < tiletexels; ++i) + { + m_neutralocclusion[i * 4 + 0] = glm::packHalf1x16(1.f); + m_neutralocclusion[i * 4 + 1] = glm::packHalf1x16(1.f); + m_neutralocclusion[i * 4 + 2] = glm::packHalf1x16(LIT_ABOVE_ALWAYS); + m_neutralocclusion[i * 4 + 3] = glm::packHalf1x16(LIT_BELOW_ALWAYS); + } + for (int index = 0; index < static_cast(count); ++index) + { + gpu_upload((index % m_layout.tiles) * m_layout.tile_texels, (index / m_layout.tiles) * m_layout.tile_texels, m_layout.tile_texels, m_neutralheights.data(), m_neutralocclusion.data()); + } + m_rawtop.assign(texels, NONE_TOP); + m_rawceiling.assign(texels, NONE_CEILING); + m_top.assign(texels, NONE_TOP); + m_ceiling.assign(texels, NONE_CEILING); + m_workertiles.assign(count, glm::ivec2{std::numeric_limits::min()}); + m_workergenerations.assign(count, 0); + m_workerraster.assign(count, false); + m_workerdirty.assign(count, false); + m_jobs.clear(); + m_results.clear(); + m_quit = false; + m_workersun = m_sun; + m_keylight = glm::vec3{0.f, 1.f, 0.f}; + m_workerkeylight = m_keylight; + m_checkslot = 0; + m_thread = std::thread{[this]() { work(); }}; + g_current = this; + WriteLog("sky occlusion: " + std::to_string(side) + "x" + std::to_string(side) + " texels of " + to_string(m_layout.texel(), 2) + " m, tiles of " + to_string(m_layout.tile, 0) + " m"); +} + +void sky_occlusion::stop() +{ + if (m_thread.joinable()) + { + { + std::lock_guard lock{m_mutex}; + m_quit = true; + } + m_wake.notify_all(); + m_thread.join(); + } + if (m_side > 0) + gpu_release(); + m_side = 0; + m_level = 0; + m_slots.clear(); + m_heights.clear(); + m_occlusion.clear(); + m_rawtop.clear(); + m_rawceiling.clear(); + m_top.clear(); + m_ceiling.clear(); + m_jobs.clear(); + m_results.clear(); + if (g_current == this) + g_current = nullptr; +} + +void sky_occlusion::clear() +{ + // once, until a scenery is there again + if ((m_side <= 0) || (m_region == nullptr)) + return; + auto const level{m_level}; + stop(); + start(level); + m_region = nullptr; +} + +int sky_occlusion::slot_of(glm::ivec2 const &Tile) const +{ + return wrap(Tile.y, m_layout.tiles) * m_layout.tiles + wrap(Tile.x, m_layout.tiles); +} + +void sky_occlusion::neutral(int const Slot) +{ + auto const px{m_layout.tile_texels}; + auto const x0{(Slot % m_layout.tiles) * px}; + auto const z0{(Slot / m_layout.tiles) * px}; + for (int z = 0; z < px; ++z) + { + auto const row{static_cast(z0 + z) * m_side + x0}; + std::fill_n(m_heights.begin() + row, px, NONE_TOP); + std::fill_n(m_occlusion.begin() + row, px, glm::vec4{1.f, 1.f, LIT_ABOVE_ALWAYS, LIT_BELOW_ALWAYS}); + } + gpu_upload(x0, z0, px, m_neutralheights.data(), m_neutralocclusion.data()); + ++m_uploaded; +} + +void sky_occlusion::apply(tile_result &Result) +{ + auto &target{m_slots[Result.slot]}; + if (target.generation != Result.generation) + return; + target.state = slot_state::ready; + auto const px{m_layout.tile_texels}; + auto const x0{(Result.slot % m_layout.tiles) * px}; + auto const z0{(Result.slot / m_layout.tiles) * px}; + for (int z = 0; z < px; ++z) + { + auto const row{static_cast(z0 + z) * m_side + x0}; + for (int x = 0; x < px; ++x) + { + auto const source{static_cast(z) * px + x}; + m_heights[row + x] = Result.heights[source]; + m_occlusion[row + x] = glm::vec4{glm::unpackHalf1x16(Result.occlusion[source * 4 + 0]), glm::unpackHalf1x16(Result.occlusion[source * 4 + 1]), + glm::unpackHalf1x16(Result.occlusion[source * 4 + 2]), glm::unpackHalf1x16(Result.occlusion[source * 4 + 3])}; + } + } + gpu_upload(x0, z0, px, Result.heights.data(), Result.occlusion.data()); + ++m_uploaded; +} + +void sky_occlusion::update(scene::basic_region *Region, glm::dvec3 const &Camera, glm::vec3 const &Keylight) +{ + auto const level{std::clamp(Global.gfx_skyvisibility, 0, 3)}; + if ((level != m_level) || ((m_side > 0) && (m_sun != Global.gfx_skyvisibility_sun))) + { + stop(); + if (level > 0) + start(level); + } + if ((m_side <= 0) || (Region == nullptr)) + return; + if (Region != m_region) + { + clear(); + m_region = Region; + if (m_side <= 0) + return; + } + + // what the thread worked out + { + std::deque results; + { + std::lock_guard lock{m_mutex}; + results.swap(m_results); + } + for (auto &result : results) + { + apply(result); + } + } + + auto const tiles{m_layout.tiles}; + auto const tilesize{static_cast(m_layout.tile)}; + glm::ivec2 const cameratile{static_cast(std::floor(Camera.x / tilesize)), static_cast(std::floor(Camera.z / tilesize))}; + m_cameratile = cameratile; + m_camera = Camera; + // the tiles of the square around the camera: the slot of each holds the one of the tiles of the square it falls on + glm::ivec2 const first{cameratile.x - tiles / 2, cameratile.y - tiles / 2}; + bool reassigned{false}; + for (int index = 0; index < tiles * tiles; ++index) + { + glm::ivec2 const wanted{first.x + wrap((index % tiles) - first.x, tiles), first.y + wrap((index / tiles) - first.y, tiles)}; + auto &target{m_slots[index]}; + if (target.tile == wanted) + continue; + target.tile = wanted; + ++target.generation; + target.state = slot_state::empty; + target.regather = false; + target.signature = 0; + neutral(index); + reassigned = true; + } + + // the key light, when it moved enough + auto keylight{glm::length(Keylight) > 0.f ? glm::normalize(Keylight) : glm::vec3{0.f, 1.f, 0.f}}; + auto const keylightmoved{glm::dot(glm::dvec3{keylight}, glm::dvec3{m_keylight}) < KEYLIGHT_MOVED}; + if (keylightmoved) + m_keylight = keylight; + + // the tiles which came in or whose scenery changed, the nearest first, one in a frame so the frame isn't held up; all at once when + // the square is mostly empty (the start, a jump of the camera), so the thread lays them all out before it works out the occlusion + // (which reads the tiles around) and does it once for each + std::vector> wanted; + auto empty{0}; + for (int index = 0; index < tiles * tiles; ++index) + { + auto const &candidate{m_slots[index]}; + if (candidate.state == slot_state::empty) + ++empty; + if ((candidate.state == slot_state::empty) || candidate.regather) + { + auto const offset{candidate.tile - cameratile}; + wanted.emplace_back(offset.x * offset.x + offset.y * offset.y, index); + } + } + std::sort(wanted.begin(), wanted.end()); + int queued{0}; + { + std::lock_guard lock{m_mutex}; + queued = static_cast(m_jobs.size()); + } + auto const bulk{empty > tiles * tiles / 2}; + auto const budget{bulk ? tiles * tiles : 1}; + std::vector jobs; + for (auto const &entry : wanted) + { + if ((static_cast(jobs.size()) >= budget) || ((false == bulk) && (queued + static_cast(jobs.size()) >= 8))) + break; + auto &target{m_slots[entry.second]}; + auto const gatherstart{std::chrono::steady_clock::now()}; + raster_job job; + job.slot = entry.second; + job.generation = target.generation; + target.signature = signature(Region, target.tile); + gather(Region, target.tile, job.triangles); + m_gathertime += std::chrono::duration(std::chrono::steady_clock::now() - gatherstart).count(); + m_triangles = static_cast(job.triangles.size()); + if (target.regather) + ++m_regathered; + ++m_gathered; + if (target.state == slot_state::empty) + target.state = slot_state::queued; + target.regather = false; + jobs.emplace_back(std::move(job)); + } + // the scenery of a tile worked out already, checked for changes a little at a time (the editor, the streamed terrain, the + // models shown and hidden by the events) + if ((jobs.empty()) && (false == m_slots.empty())) + { + for (int check = 0; check < 2; ++check) + { + m_checkslot = (m_checkslot + 1) % m_slots.size(); + auto &target{m_slots[m_checkslot]}; + if ((target.state == slot_state::ready) && (false == target.regather)) + { + if (signature(Region, target.tile) != target.signature) + target.regather = true; + } + } + } + + { + std::lock_guard lock{m_mutex}; + if (reassigned) + { + for (int index = 0; index < tiles * tiles; ++index) + { + if ((m_workertiles[index] == m_slots[index].tile) && (m_workergenerations[index] == m_slots[index].generation)) + continue; + m_workertiles[index] = m_slots[index].tile; + m_workergenerations[index] = m_slots[index].generation; + // the thread clears what it kept of the tile which left, before anything else + m_workerraster[index] = false; + m_workerdirty[index] = true; + } + } + for (auto &job : jobs) + { + m_jobs.emplace_back(std::move(job)); + } + m_workercamera = cameratile; + if (keylightmoved || (m_workersun != m_sun)) + { + m_workerkeylight = m_keylight; + m_workersun = m_sun; + for (std::size_t index = 0; index < m_workerdirty.size(); ++index) + { + if (m_workerraster[index]) + m_workerdirty[index] = true; + } + } + m_pending = static_cast(m_jobs.size()); + } + m_wake.notify_one(); +} + +void sky_occlusion::setup_scene(gl::scene_ubs &Scene, glm::dvec3 const &Camera) const +{ + if (m_side <= 0) + { + clear_scene(Scene); + return; + } + auto const size{static_cast(m_layout.tile) * m_layout.tiles}; + // w: 1 with the maps, 2 when the fog reads them too + Scene.sky_occlusion_origin = glm::vec4{static_cast(wrap(Camera.x, size)), static_cast(wrap(Camera.z, size)), static_cast(Camera.y), Global.gfx_skyvisibility_fog ? 2.f : 1.f}; + // the square reaches at least this far from the camera it follows on every side + auto const reach{m_layout.tile * static_cast(m_layout.tiles / 2 - 1)}; + Scene.sky_occlusion_params = glm::vec4{static_cast(size), static_cast(m_side), reach * 0.8f, reach * 0.2f}; + // the camera of the pass may be another one (an extra view): what is away from the camera the maps follow fades out + Scene.sky_occlusion_center = glm::vec4{static_cast(m_camera.x - Camera.x), static_cast(m_camera.z - Camera.z), 0.f, 0.f}; +} + +void sky_occlusion::clear_scene(gl::scene_ubs &Scene) +{ + Scene.sky_occlusion_origin = glm::vec4{0.f}; + Scene.sky_occlusion_params = glm::vec4{0.f}; + Scene.sky_occlusion_center = glm::vec4{0.f}; +} + +glm::vec2 sky_occlusion::sample(glm::dvec3 const &Position) const +{ + if (m_side <= 0) + return {1.f, 1.f}; + // faded out towards the edges of the square, as the shading does + auto const reach{m_layout.tile * static_cast(m_layout.tiles / 2 - 1)}; + auto const distance{static_cast(std::max(std::abs(Position.x - m_camera.x), std::abs(Position.z - m_camera.z)))}; + auto const fade{std::clamp((distance - reach * 0.8f) / (reach * 0.2f), 0.f, 1.f)}; + if (fade >= 1.f) + return {1.f, 1.f}; + auto const texel{static_cast(m_layout.texel())}; + auto const u{Position.x / texel - 0.5}; + auto const v{Position.z / texel - 0.5}; + auto const i0{static_cast(std::floor(u))}; + auto const j0{static_cast(std::floor(v))}; + auto const fu{static_cast(u - static_cast(i0))}; + auto const fv{static_cast(v - static_cast(j0))}; + auto const height{static_cast(Position.y)}; + glm::vec2 result{0.f}; + for (int j = 0; j < 2; ++j) + { + for (int i = 0; i < 2; ++i) + { + auto const x{static_cast(((i0 + i) % m_side + m_side) % m_side)}; + auto const z{static_cast(((j0 + j) % m_side + m_side) % m_side)}; + auto const index{static_cast(z) * m_side + x}; + auto const weight{(i == 0 ? 1.f - fu : fu) * (j == 0 ? 1.f - fv : fv)}; + result += texel_occlusion(m_heights[index], m_occlusion[index], height) * weight; + } + } + return glm::mix(result, glm::vec2{1.f}, fade); +} + +// what the scenery over a tile is made of, without its triangles: the geometry of the shapes, the instances with their places and +// whether they're shown. sections and cells are the ones gather() goes through +std::uint64_t sky_occlusion::signature(scene::basic_region *Region, glm::ivec2 const &Tile) +{ + auto const tilesize{static_cast(m_layout.tile)}; + glm::dvec2 const corner{Tile.x * tilesize, Tile.y * tilesize}; + glm::dvec2 const far{corner + glm::dvec2{tilesize}}; + glm::dvec3 const center{corner.x + 0.5 * tilesize, 0.0, corner.y + 0.5 * tilesize}; + auto const overlaps = [&](scene::bounding_area const &Area, double const Radius) { + return (Area.center.x + Radius >= corner.x) && (Area.center.x - Radius <= far.x) && (Area.center.z + Radius >= corner.y) && (Area.center.z - Radius <= far.y); + }; + std::uint64_t hash{0x5ca1ab1e}; + auto const &found{Region->sections(center, static_cast(tilesize * 0.75))}; + std::vector const sections{found.begin(), found.end()}; + for (auto *section : sections) + { + ready_section(section); + hash = hash_mix(hash, static_cast(section->m_shapes.size())); + for (auto &shape : section->m_shapes) + { + auto const &data{shape.data()}; + if (overlaps(data.area, shape_reach(shape))) + hash = hash_mix(hash, static_cast(data.geometry) ^ static_cast(data.visible ? 1 : 0)); + } + for (auto &cell : section->m_cells) + { + if (false == overlaps(cell.area(), static_cast(cell.area().radius))) + continue; + hash = hash_mix(hash, static_cast(cell.m_shapesopaque.size())); + hash = hash_mix(hash, static_cast(cell.m_instancesopaque.size())); + for (auto &shape : cell.m_shapesopaque) + { + auto const &data{shape.data()}; + if (overlaps(data.area, shape_reach(shape))) + hash = hash_mix(hash, static_cast(data.geometry) ^ static_cast(data.visible ? 1 : 0)); + } + for (auto const *instance : cell.m_instancesopaque) + { + hash = hash_mix(hash, static_cast(reinterpret_cast(instance)) ^ static_cast(instance->visible() ? 1 : 0)); + auto const location{instance->location()}; + hash = hash_mix_double(hash, location.x); + hash = hash_mix_double(hash, location.y); + hash = hash_mix_double(hash, location.z); + hash = hash_mix_double(hash, static_cast(instance->vAngle.y)); + } + } + } + return hash; +} + +void sky_occlusion::gather(scene::basic_region *Region, glm::ivec2 const &Tile, std::vector &Triangles) +{ + auto const tilesize{static_cast(m_layout.tile)}; + glm::dvec2 const corner{Tile.x * tilesize, Tile.y * tilesize}; + glm::dvec2 const far{corner + glm::dvec2{tilesize}}; + glm::dvec3 const center{corner.x + 0.5 * tilesize, 0.0, corner.y + 0.5 * tilesize}; + auto const overlaps = [&](scene::bounding_area const &Area, double const Radius) { + return (Area.center.x + Radius >= corner.x) && (Area.center.x - Radius <= far.x) && (Area.center.z + Radius >= corner.y) && (Area.center.z - Radius <= far.y); + }; + auto const add = [&](glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C, glm::vec3 const &Normal) { + // the bounds of the triangle against the tile + auto const minx{std::min(A.x, std::min(B.x, C.x))}; + auto const maxx{std::max(A.x, std::max(B.x, C.x))}; + auto const minz{std::min(A.z, std::min(B.z, C.z))}; + auto const maxz{std::max(A.z, std::max(B.z, C.z))}; + if ((maxx < corner.x) || (minx > far.x) || (maxz < corner.y) || (minz > far.y)) + return; + // whether it faces down: the side of the normals of its vertices (the side it's seen from), its winding taken where they + // don't tell + auto const geometric{glm::cross(glm::vec3{B - A}, glm::vec3{C - A})}; + auto down{false}; + if (std::abs(Normal.y) > 0.1f) + down = (Normal.y < 0.f); + else + down = (geometric.y < 0.f); + Triangles.push_back(triangle{glm::vec3{static_cast(A.x - corner.x), static_cast(A.y), static_cast(A.z - corner.y)}, + glm::vec3{static_cast(B.x - corner.x), static_cast(B.y), static_cast(B.z - corner.y)}, + glm::vec3{static_cast(C.x - corner.x), static_cast(C.y), static_cast(C.z - corner.y)}, down}); + }; + auto const shape = [&](scene::shape_node &Shape) { + auto const &data{Shape.data()}; + if ((false == data.visible) || data.translucent) + return; + if (false == overlaps(data.area, shape_reach(Shape))) + return; + if (data.geometry != gfx::geometry_handle{0, 0}) + { + auto const &vertices{GfxRenderer->Vertices(data.geometry)}; + auto const &indices{GfxRenderer->Indices(data.geometry)}; + if ((GfxRenderer->Primitive(data.geometry) != GL_TRIANGLES) && (GfxRenderer->Primitive(data.geometry) != -1)) + return; + auto const vertex = [&](std::size_t const Index) -> gfx::basic_vertex const & { return vertices[Index]; }; + auto const count{indices.empty() ? vertices.size() : indices.size()}; + for (std::size_t i = 0; i + 2 < count; i += 3) + { + auto const &a{vertex(indices.empty() ? i : indices[i])}; + auto const &b{vertex(indices.empty() ? i + 1 : indices[i + 1])}; + auto const &c{vertex(indices.empty() ? i + 2 : indices[i + 2])}; + add(data.origin + glm::dvec3{a.position}, data.origin + glm::dvec3{b.position}, data.origin + glm::dvec3{c.position}, a.normal + b.normal + c.normal); + } + } + else + { + // not made into geometry yet: the source of it, in world space + for (std::size_t i = 0; i + 2 < data.vertices.size(); i += 3) + { + add(data.vertices[i].position, data.vertices[i + 1].position, data.vertices[i + 2].position, data.vertices[i].normal + data.vertices[i + 1].normal + data.vertices[i + 2].normal); + } + } + }; + auto const §ions{Region->sections(center, static_cast(tilesize * 0.75))}; + std::vector const sectionlist{sections.begin(), sections.end()}; + for (auto *section : sectionlist) + { + ready_section(section); + for (auto &entry : section->m_shapes) + shape(entry); + for (auto &cell : section->m_cells) + { + if (false == overlaps(cell.area(), static_cast(cell.area().radius))) + continue; + for (auto &entry : cell.m_shapesopaque) + shape(entry); + for (auto *instance : cell.m_instancesopaque) + { + if ((instance == nullptr) || (false == instance->visible()) || (instance->pModel == nullptr)) + continue; + if (false == overlaps(instance->m_area, static_cast(instance->radius()))) + continue; + // placed as the walk of the scene places it: translated, turned about y, x, z, scaled + auto transform{glm::translate(glm::dmat4{1.0}, instance->location())}; + auto const &angle{instance->vAngle}; + if (angle.y != 0.f) + transform = glm::rotate(transform, glm::radians(static_cast(angle.y)), glm::dvec3{0.0, 1.0, 0.0}); + if (angle.x != 0.f) + transform = glm::rotate(transform, glm::radians(static_cast(angle.x)), glm::dvec3{1.0, 0.0, 0.0}); + if (angle.z != 0.f) + transform = glm::rotate(transform, glm::radians(static_cast(angle.z)), glm::dvec3{0.0, 0.0, 1.0}); + transform = glm::scale(transform, glm::dvec3{instance->Scale()}); + auto alpha{static_cast(instance->Material() != nullptr ? instance->Material()->textures_alpha : 0x30300030)}; + alpha ^= 0x0F0F000F; + gather_model(instance->pModel, alpha, transform, corner, far, Triangles); + } + } + } +} + +void sky_occlusion::gather_model(TModel3d *Model, std::uint32_t const Alpha, glm::dmat4 const &Transform, glm::dvec2 const &Corner, glm::dvec2 const &Far, std::vector &Triangles) +{ + if ((Model == nullptr) || (Model->Root == nullptr)) + return; + if (0 == (Alpha & Model->iFlags & 0x1F1F001F)) + return; + gather_submodel(Model->Root, Alpha, Transform, Corner, Far, Triangles); +} + +void sky_occlusion::gather_submodel(TSubModel *Submodel, std::uint32_t const Alpha, glm::dmat4 const &Transform, glm::dvec2 const &Corner, glm::dvec2 const &Far, std::vector &Triangles) +{ + for (auto *part{Submodel}; part != nullptr; part = part->Next) + { + // the nearest level of detail, as it stands without its animations + if ((0 == part->iVisible) || (part->fSquareMinDist > 0.f)) + continue; + auto transform{Transform}; + if ((part->iFlags & 0xC000) && (part->fMatrix != nullptr)) + transform = transform * glm::dmat4{glm::make_mat4(part->fMatrix->readArray())}; + if ((part->eType == GL_TRIANGLES) && (Alpha & part->iFlags & 0x1F) && (part->m_geometry.handle != gfx::geometry_handle{0, 0}) && (part->fVisible > 0.f)) + { + auto const &vertices{GfxRenderer->Vertices(part->m_geometry.handle)}; + auto const &indices{GfxRenderer->Indices(part->m_geometry.handle)}; + auto const count{indices.empty() ? vertices.size() : indices.size()}; + glm::dmat3 const normalmatrix{glm::transpose(glm::inverse(glm::dmat3{transform}))}; + for (std::size_t i = 0; i + 2 < count; i += 3) + { + auto const &a{vertices[indices.empty() ? i : indices[i]]}; + auto const &b{vertices[indices.empty() ? i + 1 : indices[i + 1]]}; + auto const &c{vertices[indices.empty() ? i + 2 : indices[i + 2]]}; + glm::dvec3 const pa{transform * glm::dvec4{glm::dvec3{a.position}, 1.0}}; + glm::dvec3 const pb{transform * glm::dvec4{glm::dvec3{b.position}, 1.0}}; + glm::dvec3 const pc{transform * glm::dvec4{glm::dvec3{c.position}, 1.0}}; + auto const minx{std::min(pa.x, std::min(pb.x, pc.x))}; + auto const maxx{std::max(pa.x, std::max(pb.x, pc.x))}; + auto const minz{std::min(pa.z, std::min(pb.z, pc.z))}; + auto const maxz{std::max(pa.z, std::max(pb.z, pc.z))}; + if ((maxx < Corner.x) || (minx > Far.x) || (maxz < Corner.y) || (minz > Far.y)) + continue; + glm::vec3 const normal{normalmatrix * glm::dvec3{a.normal + b.normal + c.normal}}; + auto const geometric{glm::cross(glm::vec3{pb - pa}, glm::vec3{pc - pa})}; + auto const down{std::abs(normal.y) > 0.1f ? (normal.y < 0.f) : (geometric.y < 0.f)}; + Triangles.push_back(triangle{glm::vec3{static_cast(pa.x - Corner.x), static_cast(pa.y), static_cast(pa.z - Corner.y)}, + glm::vec3{static_cast(pb.x - Corner.x), static_cast(pb.y), static_cast(pb.z - Corner.y)}, + glm::vec3{static_cast(pc.x - Corner.x), static_cast(pc.y), static_cast(pc.z - Corner.y)}, down}); + } + } + if (part->Child != nullptr) + gather_submodel(part->Child, Alpha, transform, Corner, Far, Triangles); + } +} + +// the thread + +bool sky_occlusion::worker_slot_valid(glm::ivec2 const &Tile) const +{ + auto const index{slot_of(Tile)}; + return m_workerraster[index] && (m_workertiles[index] == Tile); +} + +void sky_occlusion::work() +{ + std::unique_lock lock{m_mutex}; + while (false == m_quit) + { + m_wake.wait(lock, [this]() { + if (m_quit || (false == m_jobs.empty())) + return true; + for (std::size_t index = 0; index < m_workerdirty.size(); ++index) + { + if (m_workerdirty[index]) + return true; + } + return false; + }); + if (m_quit) + break; + // the tiles which left: what was kept of them goes + for (std::size_t index = 0; index < m_workerdirty.size(); ++index) + { + if (m_workerdirty[index] && (false == m_workerraster[index])) + { + m_workerdirty[index] = false; + auto const px{m_layout.tile_texels}; + auto const x0{static_cast(index % m_layout.tiles) * px}; + auto const z0{static_cast(index / m_layout.tiles) * px}; + for (int z = 0; z < px; ++z) + { + auto const row{static_cast(z0 + z) * m_side + x0}; + std::fill_n(m_rawtop.begin() + row, px, NONE_TOP); + std::fill_n(m_rawceiling.begin() + row, px, NONE_CEILING); + std::fill_n(m_top.begin() + row, px, NONE_TOP); + std::fill_n(m_ceiling.begin() + row, px, NONE_CEILING); + } + } + } + if (false == m_jobs.empty()) + { + // the scenery of a tile + auto job{std::move(m_jobs.front())}; + m_jobs.pop_front(); + if (job.generation != m_workergenerations[job.slot]) + continue; + lock.unlock(); + auto const start{std::chrono::steady_clock::now()}; + rasterize(job); + lock.lock(); + if (job.generation != m_workergenerations[job.slot]) + continue; + m_workerraster[job.slot] = true; + // the opening of the tile and of the ones around it, which reach into it, and their occlusion worked out anew + auto const tile{m_workertiles[job.slot]}; + std::vector around; + for (int dz = -1; dz <= 1; ++dz) + { + for (int dx = -1; dx <= 1; ++dx) + { + auto const neighbour{tile + glm::ivec2{dx, dz}}; + if (worker_slot_valid(neighbour)) + around.push_back(slot_of(neighbour)); + } + } + lock.unlock(); + for (auto const index : around) + filter(index); + lock.lock(); + for (auto const index : around) + { + if (m_workerraster[index]) + m_workerdirty[index] = true; + } + m_workertime += std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + continue; + } + // the occlusion of the dirty tile nearest to the camera + auto best{-1}; + auto bestdistance{std::numeric_limits::max()}; + for (std::size_t index = 0; index < m_workerdirty.size(); ++index) + { + if (false == m_workerdirty[index]) + continue; + auto const offset{m_workertiles[index] - m_workercamera}; + auto const distance{offset.x * offset.x + offset.y * offset.y}; + if (distance < bestdistance) + { + bestdistance = distance; + best = static_cast(index); + } + } + if (best < 0) + continue; + m_workerdirty[best] = false; + tile_result result; + result.slot = best; + result.generation = m_workergenerations[best]; + lock.unlock(); + auto const start{std::chrono::steady_clock::now()}; + occlude(best, result); + auto const elapsed{std::chrono::duration(std::chrono::steady_clock::now() - start).count()}; + lock.lock(); + m_workertime += elapsed; + ++m_workertiles_done; + if (result.generation == m_workergenerations[best]) + m_results.emplace_back(std::move(result)); + } +} + +// the triangles laid out over the texels of the tile from above: the highest of them and the lowest of those facing down +void sky_occlusion::rasterize(raster_job &Job) +{ + auto const px{m_layout.tile_texels}; + auto const texel{m_layout.texel()}; + auto const x0{(Job.slot % m_layout.tiles) * px}; + auto const z0{(Job.slot / m_layout.tiles) * px}; + for (int z = 0; z < px; ++z) + { + auto const row{static_cast(z0 + z) * m_side + x0}; + std::fill_n(m_rawtop.begin() + row, px, NONE_TOP); + std::fill_n(m_rawceiling.begin() + row, px, NONE_CEILING); + } + for (auto const &entry : Job.triangles) + { + // texel space, the centres of the texels at whole numbers + glm::vec2 const a{entry.a.x / texel - 0.5f, entry.a.z / texel - 0.5f}; + glm::vec2 const b{entry.b.x / texel - 0.5f, entry.b.z / texel - 0.5f}; + glm::vec2 const c{entry.c.x / texel - 0.5f, entry.c.z / texel - 0.5f}; + auto const area{(b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)}; + if (std::abs(area) < 1e-6f) + continue; // seen edge on + auto const minx{std::max(0, static_cast(std::ceil(std::min(a.x, std::min(b.x, c.x)))))}; + auto const maxx{std::min(px - 1, static_cast(std::floor(std::max(a.x, std::max(b.x, c.x)))))}; + auto const minz{std::max(0, static_cast(std::ceil(std::min(a.y, std::min(b.y, c.y)))))}; + auto const maxz{std::min(px - 1, static_cast(std::floor(std::max(a.y, std::max(b.y, c.y)))))}; + if ((minx > maxx) || (minz > maxz)) + continue; + auto const inverse{1.f / area}; + auto const tolerance{-1e-4f}; + for (int z = minz; z <= maxz; ++z) + { + auto const row{static_cast(z0 + z) * m_side + x0}; + for (int x = minx; x <= maxx; ++x) + { + glm::vec2 const p{static_cast(x), static_cast(z)}; + auto const w0{((b.x - p.x) * (c.y - p.y) - (b.y - p.y) * (c.x - p.x)) * inverse}; + auto const w1{((c.x - p.x) * (a.y - p.y) - (c.y - p.y) * (a.x - p.x)) * inverse}; + auto const w2{1.f - w0 - w1}; + if ((w0 < tolerance) || (w1 < tolerance) || (w2 < tolerance)) + continue; + auto const height{w0 * entry.a.y + w1 * entry.b.y + w2 * entry.c.y}; + auto &top{m_rawtop[row + x]}; + top = std::max(top, height); + if (entry.down) + { + auto &ceiling{m_rawceiling[row + x]}; + ceiling = std::min(ceiling, height); + } + } + } + } +} + +// the opening of the top and of the ceiling of a tile: what is narrower than three texels (poles, masts, fences, beams) goes, the +// rest keeps its shape. the texels of the tiles around it are read where they hold their tiles, nothing elsewhere +void sky_occlusion::filter(int const Slot) +{ + glm::ivec2 tile; + std::vector tiles; + std::vector raster; + { + std::lock_guard lock{m_mutex}; + tile = m_workertiles[Slot]; + tiles = m_workertiles; + raster = m_workerraster; + } + auto const px{m_layout.tile_texels}; + auto const margin{2}; + auto const span{px + 2 * margin}; + std::vector top(static_cast(span) * span), ceiling(top.size()); + for (int z = -margin; z < px + margin; ++z) + { + for (int x = -margin; x < px + margin; ++x) + { + glm::ivec2 const world{tile.x * px + x, tile.y * px + z}; + glm::ivec2 const worldtile{static_cast(std::floor(static_cast(world.x) / px)), static_cast(std::floor(static_cast(world.y) / px))}; + auto const slot{slot_of(worldtile)}; + auto const index{static_cast(z + margin) * span + (x + margin)}; + if (raster[slot] && (tiles[slot] == worldtile)) + { + auto const map{static_cast(wrap(world.y, m_side)) * m_side + wrap(world.x, m_side)}; + top[index] = m_rawtop[map]; + ceiling[index] = m_rawceiling[map]; + } + else + { + top[index] = NONE_TOP; + ceiling[index] = NONE_CEILING; + } + } + } + // erosion then dilation of the top (the least of the largest), dilation then erosion of the ceiling + std::vector top2(top.size(), NONE_TOP), ceiling2(top.size(), NONE_CEILING); + for (int z = 1; z < span - 1; ++z) + { + for (int x = 1; x < span - 1; ++x) + { + auto lowest{std::numeric_limits::max()}; + auto highest{-std::numeric_limits::max()}; + for (int dz = -1; dz <= 1; ++dz) + { + for (int dx = -1; dx <= 1; ++dx) + { + auto const index{static_cast(z + dz) * span + (x + dx)}; + lowest = std::min(lowest, top[index]); + highest = std::max(highest, ceiling[index]); + } + } + top2[static_cast(z) * span + x] = lowest; + ceiling2[static_cast(z) * span + x] = highest; + } + } + auto const x0{(Slot % m_layout.tiles) * px}; + auto const z0{(Slot / m_layout.tiles) * px}; + for (int z = 0; z < px; ++z) + { + for (int x = 0; x < px; ++x) + { + auto highest{-std::numeric_limits::max()}; + auto lowest{std::numeric_limits::max()}; + for (int dz = -1; dz <= 1; ++dz) + { + for (int dx = -1; dx <= 1; ++dx) + { + auto const index{static_cast(z + margin + dz) * span + (x + margin + dx)}; + highest = std::max(highest, top2[index]); + lowest = std::min(lowest, ceiling2[index]); + } + } + auto const map{static_cast(z0 + z) * m_side + (x0 + x)}; + // the opening keeps what it keeps of the raster as it was (never higher), the ceiling under the top + auto const opened{std::min(highest, m_rawtop[map])}; + m_top[map] = opened; + m_ceiling[map] = (has_top(opened) && has_ceiling(lowest)) ? std::min(std::max(lowest, m_rawceiling[map]), opened) : NONE_CEILING; + } + } +} + +// the occlusion of the texels of a tile, from the opened heights around it +void sky_occlusion::occlude(int const Slot, tile_result &Result) +{ + glm::ivec2 tile; + std::vector tiles; + std::vector raster; + glm::vec3 keylight; + bool sun; + { + std::lock_guard lock{m_mutex}; + tile = m_workertiles[Slot]; + tiles = m_workertiles; + raster = m_workerraster; + keylight = m_workerkeylight; + sun = m_workersun; + } + auto const px{m_layout.tile_texels}; + auto const texel{m_layout.texel()}; + auto const margin{static_cast(std::ceil(std::max(m_layout.reach, m_layout.sun_reach) / texel)) + 1}; + auto const span{px + 2 * margin}; + // the heights around the tile, nothing where the tiles aren't known + std::vector top(static_cast(span) * span), ceiling(top.size()); + for (int z = -margin; z < px + margin; ++z) + { + for (int x = -margin; x < px + margin; ++x) + { + glm::ivec2 const world{tile.x * px + x, tile.y * px + z}; + glm::ivec2 const worldtile{static_cast(std::floor(static_cast(world.x) / px)), static_cast(std::floor(static_cast(world.y) / px))}; + auto const slot{slot_of(worldtile)}; + auto const index{static_cast(z + margin) * span + (x + margin)}; + if (raster[slot] && (tiles[slot] == worldtile)) + { + auto const map{static_cast(wrap(world.y, m_side)) * m_side + wrap(world.x, m_side)}; + top[index] = m_top[map]; + ceiling[index] = m_ceiling[map]; + } + else + { + top[index] = NONE_TOP; + ceiling[index] = NONE_CEILING; + } + } + } + auto const at = [&](int const X, int const Z) { return static_cast(Z + margin) * span + (X + margin); }; + + // the directions and the distances of the samples, in texels; the first one past the texel itself + std::vector directions(m_layout.azimuths); + for (int a = 0; a < m_layout.azimuths; ++a) + { + auto const angle{(static_cast(a) + 0.5f) * glm::two_pi() / static_cast(m_layout.azimuths)}; + directions[a] = glm::vec2{std::cos(angle), std::sin(angle)}; + } + auto const first{std::max(2.f, 1.5f * texel)}; + std::vector distances(m_layout.steps); + for (int k = 0; k < m_layout.steps; ++k) + { + auto const share{static_cast(k + 1) / static_cast(m_layout.steps)}; + distances[k] = first + (m_layout.reach - first) * share * share; + } + std::vector sundistances(m_layout.sun_steps); + for (int k = 0; k < m_layout.sun_steps; ++k) + { + auto const share{static_cast(k + 1) / static_cast(m_layout.sun_steps)}; + sundistances[k] = first + (m_layout.sun_reach - first) * share * share; + } + // the key light: its direction across the ground, and the rise of a ray towards it per metre across + auto const across{std::sqrt(keylight.x * keylight.x + keylight.z * keylight.z)}; + auto const keyup{keylight.y > 0.01f}; + auto const overhead{across < 1e-4f}; + glm::vec2 const keydirection{overhead ? glm::vec2{1.f, 0.f} : glm::vec2{keylight.x, keylight.z} / std::max(across, 1e-4f)}; + auto const rise{overhead ? 1e6f : keylight.y / across}; + + // the share of the sky seen from the height Origin over a texel, the search around it. a ceiling above the origin lets the sky + // through below its underside, what has none blocks it from the horizon up to its top + auto const sky = [&](int const X, int const Z, float const Origin) { + float sum{0.f}; + for (auto const &direction : directions) + { + float low{0.f}; + float high{1.0e7f}; + for (auto const distance : distances) + { + auto const sx{X + static_cast(std::lround(direction.x * distance / texel))}; + auto const sz{Z + static_cast(std::lround(direction.y * distance / texel))}; + auto const index{at(sx, sz)}; + auto const t{top[index]}; + auto const c{ceiling[index]}; + if (has_ceiling(c) && (c > Origin + 0.05f)) + high = std::min(high, (c - Origin) / distance); + else if (t > Origin) + low = std::max(low, (t - Origin) / distance); + if (low >= high) + break; + } + sum += std::max(0.f, sky_share(high) - sky_share(low)); + } + return sum / static_cast(directions.size()); + }; + + Result.heights.resize(static_cast(px) * px); + Result.occlusion.resize(static_cast(px) * px * 4); + for (int z = 0; z < px; ++z) + { + for (int x = 0; x < px; ++x) + { + auto const index{at(x, z)}; + auto const t0{top[index]}; + auto const c0{ceiling[index]}; + glm::vec4 value{1.f, 1.f, LIT_ABOVE_ALWAYS, LIT_BELOW_ALWAYS}; + if (has_top(t0)) + { + auto const origin{t0 + ABOVE_TOP}; + value.x = sky(x, z, origin); + // under a top with no ceiling is the inside of what is solid (a hill, a wall seen edge on): no sky, no sun + value.y = 0.f; + // the key light above the top: the height above which the rays towards it pass over all + if (false == sun) + value.z = LIT_ABOVE_ALWAYS; + else if (false == keyup) + value.z = LIT_BELOW_ALWAYS; + else if (false == overhead) + { + auto shadow{NONE_TOP}; + for (auto const distance : sundistances) + { + auto const sx{x + static_cast(std::lround(keydirection.x * distance / texel))}; + auto const sz{z + static_cast(std::lround(keydirection.y * distance / texel))}; + auto const sample{at(sx, sz)}; + auto const drop{distance * rise}; + // under a ceiling the ray passes + if (has_ceiling(ceiling[sample]) && (origin + drop < ceiling[sample])) + continue; + shadow = std::max(shadow, top[sample] - drop); + } + value.z = has_top(shadow) ? std::clamp(shadow - t0, LIT_ABOVE_ALWAYS, LIT_BELOW_ALWAYS) : LIT_ABOVE_ALWAYS; + } + else + value.z = LIT_ABOVE_ALWAYS; + if (has_ceiling(c0)) + { + // under the ceiling + auto const under{c0 - BELOW_CEILING}; + value.y = sky(x, z, under); + if (false == sun) + value.w = LIT_BELOW_ALWAYS; + else if (false == keyup) + value.w = LIT_NEVER_BELOW; + else + { + // the height below which the rays towards the key light pass under the ceilings, out to the open + auto limit{c0}; + auto out{false}; + for (auto const distance : sundistances) + { + auto const sx{x + static_cast(std::lround(keydirection.x * distance / texel))}; + auto const sz{z + static_cast(std::lround(keydirection.y * distance / texel))}; + auto const sample{at(sx, sz)}; + auto const drop{distance * rise}; + if (top[sample] <= under + drop) + { + out = true; + break; + } + if (has_ceiling(ceiling[sample]) && (under + drop < ceiling[sample])) + limit = std::min(limit, ceiling[sample] - drop); + else + { + limit = NONE_TOP; + break; // into the solid + } + } + // the rays which don't come out within the reach stay in the tunnel + value.w = (out && has_top(limit)) ? std::clamp(limit - t0, LIT_NEVER_BELOW, LIT_BELOW_ALWAYS) : LIT_NEVER_BELOW; + } + } + else + value.w = (sun ? LIT_NEVER_BELOW : LIT_BELOW_ALWAYS); + } + auto const target{static_cast(z) * px + x}; + Result.heights[target] = t0; + Result.occlusion[target * 4 + 0] = glm::packHalf1x16(value.x); + Result.occlusion[target * 4 + 1] = glm::packHalf1x16(value.y); + Result.occlusion[target * 4 + 2] = glm::packHalf1x16(value.z); + Result.occlusion[target * 4 + 3] = glm::packHalf1x16(value.w); + } + } +} + +std::string sky_occlusion::info() const +{ + if (m_side <= 0) + return "sky occlusion: off"; + int ready{0}; + for (auto const &entry : m_slots) + { + if (entry.state == slot_state::ready) + ++ready; + } + double workertime; + std::uint32_t done; + { + auto &mutex{const_cast(m_mutex)}; + std::lock_guard lock{mutex}; + workertime = m_workertime; + done = m_workertiles_done; + } + return "sky occlusion: " + std::to_string(m_side) + "x" + std::to_string(m_side) + " of " + to_string(m_layout.texel(), 2) + " m, tiles " + std::to_string(ready) + "/" + + std::to_string(m_slots.size()) + " ready, " + std::to_string(m_pending) + " queued; gathered " + std::to_string(m_gathered) + " (" + std::to_string(m_regathered) + " again, last " + + std::to_string(m_triangles) + " triangles), " + to_string(m_gathertime / std::max(m_gathered, 1), 2) + " ms each on the main thread; " + std::to_string(done) + + " worked out in the background, " + to_string(workertime / std::max(done, 1), 1) + " ms each; " + std::to_string(m_uploaded) + " uploads"; +} + +void sky_occlusion::draw_debug_ui() +{ + char const *levels[]{"off (no occlusion)", "low (256 m, 1 m texels)", "medium (512 m, 1 m texels)", "high (512 m, 0.5 m texels)"}; + ImGui::Combo("occlusion of the sky by the scenery (gfx.skyvisibility)", &Global.gfx_skyvisibility, levels, IM_ARRAYSIZE(levels)); + ImGui::Checkbox("the sun held back too (gfx.skyvisibility.sun)", &Global.gfx_skyvisibility_sun); + ImGui::Checkbox("in the volumetric fog (gfx.skyvisibility.fog)", &Global.gfx_skyvisibility_fog); + ImGui::Checkbox("environment flags of the tracks darken as before (gfx.tunnelflags)", &Global.gfx_tunnelflags); + if (m_side <= 0) + return; + ImGui::TextWrapped("%s", info().c_str()); + if (ImGui::Button("work the tiles out again")) + clear(); + auto const light{sample(Global.pCamera.Pos)}; + ImGui::Text("at the camera: sky %.2f, key light %.2f", light.x, light.y); + // a view of the square: the share of the sky, the key light or the top relative to the camera + char const *modes[]{"no view", "sky above the top", "sky under the ceilings", "key light on the top", "height of the top"}; + ImGui::Combo("view", &m_previewmode, modes, IM_ARRAYSIZE(modes)); + if ((m_previewmode <= 0) || (GfxRenderer == nullptr) || (GfxRenderer->GetImguiRenderer() == nullptr)) + return; + // the square as the camera sees it from above, the camera in the middle, north (+z) up + auto const size{std::min(m_side, 256)}; + auto const step{static_cast(m_layout.tile) * m_layout.tiles / size}; + m_previewrgba.resize(static_cast(size) * size * 4); + auto const camera{Global.pCamera.Pos}; + auto const texel{static_cast(m_layout.texel())}; + for (int row = 0; row < size; ++row) + { + for (int column = 0; column < size; ++column) + { + auto const wx{camera.x + (column - size / 2) * step}; + auto const wz{camera.z + (size / 2 - row) * step}; + auto const x{wrap(static_cast(std::floor(wx / texel)), m_side)}; + auto const z{wrap(static_cast(std::floor(wz / texel)), m_side)}; + auto const index{static_cast(z) * m_side + x}; + auto const top{m_heights[index]}; + auto const &occlusion{m_occlusion[index]}; + float value{0.f}; + switch (m_previewmode) + { + case 1: + value = occlusion.x; + break; + case 2: + value = occlusion.y; + break; + case 3: + value = texel_occlusion(top, occlusion, top + 0.1f).y; + break; + default: + value = has_top(top) ? std::clamp(0.5f + static_cast(top - camera.y) / 40.f, 0.f, 1.f) : 0.f; + break; + } + auto const level{static_cast(std::clamp(value, 0.f, 1.f) * 255.f)}; + auto *pixel{&m_previewrgba[(static_cast(row) * size + column) * 4]}; + pixel[0] = level; + pixel[1] = level; + pixel[2] = (has_top(top) || (m_previewmode != 4)) ? level : static_cast(96); + pixel[3] = 255; + if ((row == size / 2) && (column == size / 2)) + { + pixel[0] = 255; + pixel[1] = 0; + pixel[2] = 0; + } + } + } + imgui_renderer::image_options options; + options.srgb = false; + options.nearest = true; + m_previewimage = GfxRenderer->GetImguiRenderer()->Update_Image(m_previewimage, m_previewrgba.data(), size, size, options); + if (m_previewimage != 0) + ImGui::Image(static_cast(m_previewimage), ImVec2{static_cast(size), static_cast(size)}); +} + +float vehicle_shade(TDynamicObject const *Vehicle) +{ + if ((Vehicle == nullptr) || (false == Global.gfx_tunnelflags)) + return 0.f; + return Vehicle->fShade; +} + +float vehicle_light_level(TDynamicObject const *Vehicle) +{ + if (Vehicle == nullptr) + return 1.f; + if (Global.gfx_tunnelflags) + return (Vehicle->fShade > 0.f ? Vehicle->fShade : 1.f); + auto const *occlusion{sky_occlusion::current()}; + if (occlusion == nullptr) + return 1.f; + // at the height of the windows + auto const light{occlusion->sample(Vehicle->GetPosition() + glm::dvec3{0.0, 1.5, 0.0})}; + return std::clamp(0.35f * light.x + 0.65f * light.y, 0.f, 1.f); +} + +} // namespace gfx diff --git a/rendering/common/skyocclusion.h b/rendering/common/skyocclusion.h new file mode 100644 index 00000000..f7c04e23 --- /dev/null +++ b/rendering/common/skyocclusion.h @@ -0,0 +1,244 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gl/ubo.h" + +class TDynamicObject; +class TModel3d; +class TSubModel; + +namespace scene +{ +class basic_region; +} + +namespace gfx +{ + +// the occlusion of the sky and of the sun by the scenery around the camera (Global.gfx_skyvisibility, the track T of +// claude/renderers-plan.md), the part shared by the renderers; the backend (opengl46_sky_occlusion, vk14::sky_occlusion_maps) keeps +// two textures the shading reads and copies to them what this part works out, through the gpu_* methods. +// tunnels, the ground under bridges and roofs, cuttings and the foot of slopes are dark because the geometry around holds back the +// sky and the sun, not because of the environment flags of the tracks (tunnel, canyon), which the renderers leave alone now +// (Global.gfx_tunnelflags brings them back). the maps cover a square around the camera, in tiles of the world; a tile is worked out +// once when it comes into the square and kept while it stays there (the scenery doesn't change, the editor and the streamed terrain +// are noticed by a signature of what the tile holds): +// - the main thread gathers the triangles of the static opaque scenery over a tile: the shapes (the terrain, the heightmap +// terrain, the roads and the platforms) and the opaque submodels of the models at their nearest level of detail; not the +// vehicles, the tracks, the traction, the lines, nor what is see-through (the trees); +// - a thread of its own lays them out seen from above: the height of the highest surface over each texel (the top) and of the +// lowest one facing down (the ceiling: the vault of a tunnel, the deck of a bridge seen from below, a roof); things narrower than +// a few texels (poles, masts, fences) are taken out of both by an opening (an erosion and a dilation); +// - and works out for each texel, from the heights around it: the share of the sky a surface facing up sees (the irradiance +// weighed by the cosine) above the top, and under the ceiling, where there's one, a little below it (light comes in under the +// vault along the tunnel from the portal); and the heights between which the key light (the sun, the moon at night) is let +// through: above the top it lights what is higher than a height worked out by marching towards it, under the ceiling what is +// lower than one; +// - the shading (light_common.glsl, sky_occlusion()) reads the height of the top (GL: SKY_OCCLUSION_HEIGHT_TEX, R32F) and the four +// values above (SKY_OCCLUSION_TEX, RGBA16F) where the fragment is, decides from its height whether it's under the top, and holds +// back the ambient light, the reflection of the sky and the key light by them. the lamps, the headlights and the light of the cab +// are direct light and are left as they are; +// - the light around a vehicle (the headlights fade by day, the light of its interior, the cab lamp) is taken from the same maps +// on the cpu (vehicle_light_level()), with no delay. +// the maps are toroidal: the texel of a world position is its world texel index modulo the size of the map, so a tile which comes +// into the square takes the place of the one which left it on the other side and nothing is moved +class sky_occlusion +{ + public: + sky_occlusion() = default; + virtual ~sky_occlusion(); + sky_occlusion(const sky_occlusion &) = delete; + sky_occlusion &operator=(const sky_occlusion &) = delete; + + // the layout of a level (Global.gfx_skyvisibility 1..3) + struct layout + { + float tile{64.f}; // side of a tile, metres + int tile_texels{64}; // texels across a tile + int tiles{8}; // tiles across the map + int azimuths{12}; // directions the sky is searched in from a texel + int steps{10}; // samples along each direction + float reach{48.f}; // how far, metres + int sun_steps{16}; // samples towards the key light + float sun_reach{128.f}; // how far, metres + float texel() const + { + return tile / static_cast(tile_texels); + } + int side() const + { + return tile_texels * tiles; + } + }; + static layout layout_of(int const Level); + + // the frame: the square follows the camera (world space), tiles come in and go out, what the thread worked out goes to the + // textures. Keylight: the direction towards the key light (the sun, the moon at night), world space; it is followed when it + // moved enough. called by the renderer once a frame, before the passes which read the maps, with the region of the scenery + void update(scene::basic_region *Region, glm::dvec3 const &Camera, glm::vec3 const &Keylight); + // the fields of the block of the scene the shading reads the maps with, for a pass seen from Camera (world space); zero when + // there are no maps + void setup_scene(gl::scene_ubs &Scene, glm::dvec3 const &Camera) const; + static void clear_scene(gl::scene_ubs &Scene); + // the share of the sky (x) and of the key light (y) a point of the world gets, 0..1, as the shading works them out; 1 where + // there are no maps + glm::vec2 sample(glm::dvec3 const &Position) const; + // whether the maps are there + bool active() const + { + return m_side > 0; + } + // the maps of the renderer in use, nullptr when there are none + static sky_occlusion const *current(); + // a scenery is being loaded or let go: the tiles and the signatures go + void clear(); + // line for the statistics of the debug panel + std::string info() const; + // controls, counters and a view of the maps for the debug window + void draw_debug_ui(); + + protected: + // the textures of Side x Side texels: the height of the top (one 32 bit float per texel) and the occlusion (four 16 bit floats); + // false when they can't be made + virtual bool gpu_allocate(int const Side) = 0; + virtual void gpu_release() = 0; + // copies a square of Size x Size texels at X, Y of the textures: Heights one float per texel, Occlusion four halves per texel, + // rows along x, one after another along z + virtual void gpu_upload(int const X, int const Y, int const Size, float const *Heights, std::uint16_t const *Occlusion) = 0; + + int m_side{0}; + + private: + // the triangle of the gathered scenery, x and z relative to the corner of its tile, y in the world; Down: it faces down + struct triangle + { + glm::vec3 a, b, c; + bool down; + }; + // what the thread is asked to do + struct raster_job + { + int slot{0}; + std::uint32_t generation{0}; + std::vector triangles; + }; + // what it gives back: the top and the occlusion of the texels of a tile + struct tile_result + { + int slot{0}; + std::uint32_t generation{0}; + std::vector heights; + std::vector occlusion; + double milliseconds{0.0}; + }; + enum class slot_state + { + empty, // nothing asked yet: neutral values + queued, // the triangles went to the thread + ready // the thread gave its result + }; + struct slot + { + glm::ivec2 tile{std::numeric_limits::min()}; // world tile index + std::uint32_t generation{0}; + slot_state state{slot_state::empty}; + std::uint64_t signature{0}; + bool regather{false}; // the scenery of the tile changed: gathered again, the old result kept until the new one comes + }; + + void start(int const Level); + void stop(); + // the slot of a world tile + int slot_of(glm::ivec2 const &Tile) const; + // the triangles of the scenery over a tile + void gather(scene::basic_region *Region, glm::ivec2 const &Tile, std::vector &Triangles); + void gather_model(TModel3d *Model, std::uint32_t const Alpha, glm::dmat4 const &Transform, glm::dvec2 const &Corner, glm::dvec2 const &Far, std::vector &Triangles); + void gather_submodel(TSubModel *Submodel, std::uint32_t const Alpha, glm::dmat4 const &Transform, glm::dvec2 const &Corner, glm::dvec2 const &Far, std::vector &Triangles); + // what the scenery over a tile is made of, cheap to work out: a change of it means the tile is gathered again + std::uint64_t signature(scene::basic_region *Region, glm::ivec2 const &Tile); + // the values of a tile nothing is known of yet, in the textures and in the copy of the cpu + void neutral(int const Slot); + void apply(tile_result &Result); + + // the thread + void work(); + void rasterize(raster_job &Job); + void filter(int const Slot); + void occlude(int const Slot, tile_result &Result); + bool worker_slot_valid(glm::ivec2 const &Tile) const; + + layout m_layout; + int m_level{0}; + bool m_sun{true}; + std::vector m_slots; + glm::ivec2 m_cameratile{0}; + glm::dvec3 m_camera{0.0}; + glm::vec3 m_keylight{0.f, 1.f, 0.f}; // the direction the thread was given last + // the copy of the maps the cpu samples (sample()): the top, and the occlusion as floats + std::vector m_heights; + std::vector m_occlusion; + // neutral tiles, uploaded when a tile comes into the square + std::vector m_neutralheights; + std::vector m_neutralocclusion; + std::size_t m_checkslot{0}; // the slot whose signature is checked next + scene::basic_region const *m_region{nullptr}; + + // shared with the thread, under m_mutex + std::mutex m_mutex; + std::condition_variable m_wake; + bool m_quit{false}; + std::deque m_jobs; + std::deque m_results; + std::vector m_workertiles; // the world tile of each slot, as the thread knows it + std::vector m_workergenerations; + std::vector m_workerraster; // the slot holds the rasterized scenery of its tile + std::vector m_workerdirty; // the occlusion of the slot is to be worked out again + glm::ivec2 m_workercamera{0}; + glm::vec3 m_workerkeylight{0.f, 1.f, 0.f}; + bool m_workersun{true}; + std::thread m_thread; + // the maps of the thread: the top and the ceiling as rasterized, and opened + std::vector m_rawtop, m_rawceiling, m_top, m_ceiling; + + // statistics + std::uint32_t m_gathered{0}; + std::uint32_t m_triangles{0}; + std::uint32_t m_regathered{0}; + std::uint32_t m_uploaded{0}; + double m_gathertime{0.0}; + double m_workertime{0.0}; // under m_mutex + std::uint32_t m_workertiles_done{0}; // under m_mutex + int m_pending{0}; + std::uint64_t m_previewimage{0}; + int m_previewmode{0}; + std::vector m_previewrgba; +}; + +// the strength the shading of a vehicle gives the sun and the ambient light (walk_sunlight()), from the environment flags of the +// tracks under it: only with Global.gfx_tunnelflags; 0 otherwise (the light isn't changed) +float vehicle_shade(TDynamicObject const *Vehicle); +// the light around a vehicle, 0 (none: the depth of a tunnel) .. 1 (the open sky), which fades its headlights by day and the +// lights of its interior: with Global.gfx_tunnelflags from the environment flags of the tracks under it (TDynamicObject::fShade), +// otherwise from the maps of the occlusion around the camera (1 without them, or away from the camera) +float vehicle_light_level(TDynamicObject const *Vehicle); + +} // namespace gfx diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 07ea963a..2a039a25 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -218,6 +218,7 @@ bool opengl46_renderer::Init(GLFWwindow *Window) m_ao.init(); m_localshadows.init(); m_cabprobe.init(); + m_skyocclusion.init(); } catch (gl::shader_exception const &e) { @@ -684,11 +685,15 @@ bool opengl46_renderer::Render() // lamps of models and vehicles around the camera and the cab lamp, for the views of the frame to take into their light buffers if ((false == Global.gfx_skiprendering) && (simulation::Region != nullptr)) m_locallights.gather(Global.pCamera.Pos, *simulation::Region, m_geometry, m_framestamp); + // the occlusion of the sky by the scenery around the camera: the tiles which came in, what the background worked out + if (false == Global.gfx_skiprendering) + m_skyocclusion.update(simulation::Region, Global.pCamera.Pos, -m_sunlight.direction); } else { // a scenery is being loaded: the cells, instances and models the lights were taken from may be gone m_locallights.reset(); + m_skyocclusion.clear(); m_localshadows.reset_lamps(); m_localshadows.reset_headlights(); // and so may the tracks the sleeper records were made for, and the models drawn on their own @@ -1055,6 +1060,7 @@ void opengl46_renderer::SwapBuffers() ", off in this view\n" ) : std::string{} ) + ( Global.gfx_cabprobe > 0 ? " " + m_cabprobe.info() + "\n" : std::string{} ) + + ( Global.gfx_skyvisibility > 0 ? " " + m_skyocclusion.info() + "\n" : std::string{} ) + shadowcascadestext + " instance buffer: " + std::to_string( m_instancestream->used() / static_cast( sizeof( glm::mat4 ) ) ) + " matrices, " + std::to_string( m_instancestream->size() / 1024 ) + " KiB\n" @@ -1471,6 +1477,8 @@ void opengl46_renderer::draw_debug_ui() m_ao.draw_debug_ui(!m_viewports.empty() && m_viewports.front()->ssao_tex != nullptr); m_cabprobe.draw_debug_ui(); ImGui::Separator(); + m_skyocclusion.draw_debug_ui(); + ImGui::Separator(); m_ssr.draw_debug_ui(nullptr); } ImGui::End(); @@ -1655,6 +1663,8 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) // pass of the main view only scene_ubs.env_params = glm::vec4{Global.gfx_ambient_groundalbedo, 0.f, 0.f, 0.f}; gfx::screen_reflections::clear(scene_ubs); + // the occlusion of the sky by the scenery, seen from the camera of the view + m_skyocclusion.setup_scene(scene_ubs, m_colorpass.pass_camera.position()); scene_ubs.projection = OpenGLMatrices.data(GL_PROJECTION); scene_ubs.inv_view = glm::inverse( glm::mat4{ glm::mat3{ m_colorpass.pass_camera.modelview() } } ); scene_ubo->update(scene_ubs); @@ -1675,6 +1685,8 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) m_localshadows.bind(); // the probe of the cab, or the stand-ins m_cabprobe.bind(); + // the maps of the occlusion of the sky, or the stand-ins + m_skyocclusion.bind(); if (Global.gfx_shadowmap_enabled && vp.main) { @@ -5730,6 +5742,8 @@ void opengl46_renderer::Preview_Color(TAnimModel *Instance, float const Squaredi scene_ubs.cascade_end[idx] = std::numeric_limits::max(); } scene_ubs.shadow_fit = glm::vec4{0.f}; + // the model stands on its own, nothing around it + gfx::sky_occlusion::clear_scene(scene_ubs); scene_ubo->update(scene_ubs); scene_ubo->bind_uniform(); // the sun is the only light, and there's no fog @@ -6113,13 +6127,8 @@ void opengl46_renderer::Update_Lights(light_array &Lights, bool const Headlights renderlight.position = lightoffset; renderlight.direction = scenelight.direction; - auto luminance = static_cast(Global.fLuminance); // adjust luminance level based on vehicle's location, e.g. tunnels - auto const environment = scenelight.owner->fShade; - if (environment > 0.f) - { - luminance *= environment; - } + auto const luminance = static_cast(Global.fLuminance) * gfx::vehicle_light_level(scenelight.owner); renderlight.diffuse = glm::vec4{glm::max(glm::vec3{colors::none}, scenelight.color - glm::vec3{luminance}), renderlight.diffuse[3]}; renderlight.ambient = glm::vec4{glm::max(glm::vec3{colors::none}, scenelight.color * glm::vec3{scenelight.intensity} - glm::vec3{luminance}), renderlight.ambient[3]}; diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index e2450366..4822a623 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -44,6 +44,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/common/locallights.h" #include "rendering/opengl46/opengl46localshadows.h" #include "rendering/opengl46/opengl46cabprobe.h" +#include "rendering/opengl46/opengl46skyocclusion.h" #include "rendering/opengl46/opengl46instancecull.h" #include "rendering/opengl46/opengl46hiz.h" #include "rendering/opengl46/opengl46shadowfit.h" @@ -783,6 +784,8 @@ class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { // the probe of the occupied cab, and the colour mode draws the cab into it with its programs (Render_cab_probe()) opengl46_cab_probe m_cabprobe; bool m_cabprobepass = false; + // the occlusion of the sky and of the sun by the scenery around the camera (tunnels, bridges, roofs: the track T) + opengl46_sky_occlusion m_skyocclusion; double m_precipitationrotation; diff --git a/rendering/opengl46/opengl46skyocclusion.cpp b/rendering/opengl46/opengl46skyocclusion.cpp new file mode 100644 index 00000000..a8fb9138 --- /dev/null +++ b/rendering/opengl46/opengl46skyocclusion.cpp @@ -0,0 +1,97 @@ +/* +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/opengl46skyocclusion.h" + +#include "gl/ubo.h" + +namespace +{ + +// a toroidal map read texel by texel (textureGather): nearest, repeated +GLuint make_map(GLenum const Format, int const Side) +{ + GLuint texture{0}; + glCreateTextures(GL_TEXTURE_2D, 1, &texture); + glTextureStorage2D(texture, 1, Format, Side, Side); + glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTextureParameteri(texture, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTextureParameteri(texture, GL_TEXTURE_WRAP_T, GL_REPEAT); + return texture; +} + +} // namespace + +opengl46_sky_occlusion::~opengl46_sky_occlusion() +{ + gpu_release(); + if (m_emptyheights != 0) + glDeleteTextures(1, &m_emptyheights); + if (m_emptyocclusion != 0) + glDeleteTextures(1, &m_emptyocclusion); +} + +void opengl46_sky_occlusion::init() +{ + // a sampler with no texture to read from is undefined, also where the shading doesn't read it + m_emptyheights = make_map(GL_R32F, 1); + float const none{-1.0e4f}; + glClearTexImage(m_emptyheights, 0, GL_RED, GL_FLOAT, &none); + m_emptyocclusion = make_map(GL_RGBA16F, 1); + float const open[4]{1.f, 1.f, -1000.f, 1000.f}; + glClearTexImage(m_emptyocclusion, 0, GL_RGBA, GL_FLOAT, open); + bind(); +} + +void opengl46_sky_occlusion::bind() const +{ + glBindTextureUnit(gl::SKY_OCCLUSION_HEIGHT_TEX, (m_heights != 0) ? m_heights : m_emptyheights); + glBindTextureUnit(gl::SKY_OCCLUSION_TEX, (m_occlusion != 0) ? m_occlusion : m_emptyocclusion); +} + +bool opengl46_sky_occlusion::gpu_allocate(int const Side) +{ + gpu_release(); + m_heights = make_map(GL_R32F, Side); + m_occlusion = make_map(GL_RGBA16F, Side); + if ((m_heights == 0) || (m_occlusion == 0)) + return false; + bind(); + return true; +} + +void opengl46_sky_occlusion::gpu_release() +{ + if (m_heights != 0) + glDeleteTextures(1, &m_heights); + if (m_occlusion != 0) + glDeleteTextures(1, &m_occlusion); + m_heights = 0; + m_occlusion = 0; + if (m_emptyheights != 0) + bind(); +} + +void opengl46_sky_occlusion::gpu_upload(int const X, int const Y, int const Size, float const *Heights, std::uint16_t const *Occlusion) +{ + if ((m_heights == 0) || (m_occlusion == 0)) + return; + // from the memory of the process: the buffer the texture streaming may have bound and the alignment are put back after + GLint unpack{0}, alignment{4}; + glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &unpack); + glGetIntegerv(GL_UNPACK_ALIGNMENT, &alignment); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glTextureSubImage2D(m_heights, 0, X, Y, Size, Size, GL_RED, GL_FLOAT, Heights); + glTextureSubImage2D(m_occlusion, 0, X, Y, Size, Size, GL_RGBA, GL_HALF_FLOAT, Occlusion); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast(unpack)); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignment); +} diff --git a/rendering/opengl46/opengl46skyocclusion.h b/rendering/opengl46/opengl46skyocclusion.h new file mode 100644 index 00000000..e9ffebe1 --- /dev/null +++ b/rendering/opengl46/opengl46skyocclusion.h @@ -0,0 +1,37 @@ +/* +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 "rendering/common/skyocclusion.h" + +// the maps of the occlusion of the sky by the scenery (gfx::sky_occlusion) for the opengl 4.6 renderer: two textures bound to their +// units (gl::SKY_OCCLUSION_HEIGHT_TEX, gl::SKY_OCCLUSION_TEX) for every pass, stand-ins of one texel while there are none +class opengl46_sky_occlusion : public gfx::sky_occlusion +{ + public: + opengl46_sky_occlusion() = default; + ~opengl46_sky_occlusion() override; + + // the stand-ins + void init(); + // the textures (or the stand-ins) bound to their units + void bind() const; + + protected: + bool gpu_allocate(int const Side) override; + void gpu_release() override; + void gpu_upload(int const X, int const Y, int const Size, float const *Heights, std::uint16_t const *Occlusion) override; + + private: + GLuint m_heights{0}; + GLuint m_occlusion{0}; + GLuint m_emptyheights{0}; + GLuint m_emptyocclusion{0}; +}; diff --git a/rendering/vulkan14/vulkan14bindings.h b/rendering/vulkan14/vulkan14bindings.h index f092d980..42fc109e 100644 --- a/rendering/vulkan14/vulkan14bindings.h +++ b/rendering/vulkan14/vulkan14bindings.h @@ -80,7 +80,11 @@ constexpr std::uint32_t SCENE_AO_TEX = 27; constexpr std::uint32_t CAB_PROBE_TEX = 28; constexpr std::uint32_t CAB_PROBE_ALBEDO_TEX = 29; constexpr std::uint32_t CAB_PROBE_SSBO = 30; -constexpr std::uint32_t PASS_BINDING_COUNT = 31; +// the occlusion of the sky and of the sun by the scenery around the camera (gfx::sky_occlusion, the track T): the height of the top of +// the scenery and the shares of the sky and the limits of the key light, read texel by texel +constexpr std::uint32_t SKY_OCCLUSION_HEIGHT_TEX = 31; +constexpr std::uint32_t SKY_OCCLUSION_TEX = 32; +constexpr std::uint32_t PASS_BINDING_COUNT = 33; // the offsets of the bindings above with dynamic offsets, in the order of the bindings constexpr std::uint32_t PASS_DYNAMIC_OFFSETS = 6; // the textures of a material take the bindings of their units (#pragma texture(name, unit, ...)), 0 .. MAX_TEXTURES - 1 diff --git a/rendering/vulkan14/vulkan14context.h b/rendering/vulkan14/vulkan14context.h index 29c7d6d0..b07cb50d 100644 --- a/rendering/vulkan14/vulkan14context.h +++ b/rendering/vulkan14/vulkan14context.h @@ -29,6 +29,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/vulkan14/vulkan14cull.h" #include "rendering/vulkan14/vulkan14shadows.h" #include "rendering/vulkan14/vulkan14cabprobe.h" +#include "rendering/vulkan14/vulkan14skyocclusion.h" #include "rendering/vulkan14/vulkan14terrain.h" #include "rendering/vulkan14/vulkan14grass.h" #include "rendering/vulkan14/vulkan14particles.h" @@ -399,6 +400,9 @@ struct scene_state compute_kernel cabprobe_sh; buffer cabprobe_bounce; std::shared_ptr cabprobe_empty; + // the occlusion of the sky by the scenery around the camera (the track T): its maps, and their stand-ins while there are none + sky_occlusion_maps skyocclusion; + std::shared_ptr skyocclusion_heights_empty, skyocclusion_empty; // the materials of the heightmap terrain (the stage P6), made with the bank of the textures std::unique_ptr terrain; // the grass of the heightmap terrain, and its program, made when it's first drawn diff --git a/rendering/vulkan14/vulkan14debug.cpp b/rendering/vulkan14/vulkan14debug.cpp index 8332a60a..ef28945a 100644 --- a/rendering/vulkan14/vulkan14debug.cpp +++ b/rendering/vulkan14/vulkan14debug.cpp @@ -168,6 +168,10 @@ std::string vulkan14_renderer::scene_info() { text += " " + scene.cabprobe.info() + "\n"; } + if (Global.gfx_skyvisibility > 0) + { + text += " " + scene.skyocclusion.info() + "\n"; + } text += cascades + line(" submodels:", colour.submodels, shadowstats.submodels) + line(" paths: ", colour.paths, shadowstats.paths) + line(" shapes: ", colour.shapes, shadowstats.shapes) + " traction: " + to_string(colour.traction, 7) + "\n" + " lines: " + to_string(colour.lines, 7) + "\n" + "particles: " + to_string(colour.particles, 7) + "\nlights: " + to_string(static_cast(m_lights_lit), 7) + " (" + std::to_string(m_lights_dropped) + " over capacity), " + @@ -333,6 +337,8 @@ void vulkan14_renderer::draw_debug_ui() } } ImGui::Separator(); + scene.skyocclusion.draw_debug_ui(); + ImGui::Separator(); std::string raytracing; if (m_context->scene.raytracing) { diff --git a/rendering/vulkan14/vulkan14frame.cpp b/rendering/vulkan14/vulkan14frame.cpp index d5a8f6ff..87e0aed2 100644 --- a/rendering/vulkan14/vulkan14frame.cpp +++ b/rendering/vulkan14/vulkan14frame.cpp @@ -610,6 +610,7 @@ bool vulkan14_renderer::Render() } scene.lamps.begin_frame(ctx.commands, value); scene.cabprobe.begin_frame(value); + scene.skyocclusion.begin_frame(value); if (scene.terrain) { // textures of new heightmap terrain layers go to the arrays the terrain shader reads them from @@ -680,8 +681,9 @@ bool vulkan14_renderer::Render() if (false == scene.images_ready) { scene.images_ready = true; - std::vector const standins{scene.shadow_empty.get(), scene.env_empty.get(), scene.volume_empty.get(), scene.cube_shadow_empty.get(), - scene.headlight_shadow_empty.get(), scene.depth_empty.get(), scene.cabprobe_empty.get()}; + std::vector const standins{scene.shadow_empty.get(), scene.env_empty.get(), scene.volume_empty.get(), scene.cube_shadow_empty.get(), + scene.headlight_shadow_empty.get(), scene.depth_empty.get(), scene.cabprobe_empty.get(), scene.skyocclusion_heights_empty.get(), + scene.skyocclusion_empty.get()}; std::vector clears; for (auto *image : standins) { @@ -769,11 +771,17 @@ bool vulkan14_renderer::Render() { m_locallights.gather(Global.pCamera.Pos, *simulation::Region, m_geometry, m_framestamp); } + // the occlusion of the sky by the scenery around the camera: the tiles which came in, what the background worked out + if (false == Global.gfx_skiprendering) + { + scene.skyocclusion.update(simulation::Region, Global.pCamera.Pos, -m_sunlight.direction); + } } else { // a scenery is being loaded: the cells, instances and models the lights were taken from may be gone m_locallights.reset(); + scene.skyocclusion.clear(); scene.lamps.reset_lamps(); scene.lamps.reset_headlights(); // and so may the tracks the sleeper records were made for, and the models drawn on their own @@ -791,6 +799,8 @@ bool vulkan14_renderer::Render() ++m_instancerecordsversion; m_nodestateversion = scene::node_state_version; } + // what the maps of the occlusion of the sky were given, copied to them + scene.skyocclusion.flush(ctx.commands); // the sleeper records of tracks out of sight for a while, and the records of the models drawn on their own Expire_Indirect_Records(); // scenery lamps which get shadows in this frame. their maps hold the scenery as of the version of the instance records @@ -1547,6 +1557,8 @@ void vulkan14_renderer::render_viewport(std::size_t const Index) // the clouds of the view and the fog volume, which take the lights of the clusters and the shadow map of the pass { auto const fogtarget{glm::ivec2{vp.width, vp.height}}; + // the occlusion of the sky by the scenery, seen from the camera of the view + scene.skyocclusion.setup_scene(scene_ubs, m_colorpass.pass_camera.position()); scene_ubs.projection = OpenGLMatrices.data(GL_PROJECTION); scene_ubs.inv_view = glm::inverse(glm::mat4{glm::mat3{m_colorpass.pass_camera.modelview()}}); setup_shadow_matrices(); @@ -3737,12 +3749,7 @@ void vulkan14_renderer::update_lights(light_array &Lights, bool const Headlights } renderlight.position = lightoffset; renderlight.direction = scenelight.direction; - auto luminance = static_cast(Global.fLuminance); - auto const environment = scenelight.owner->fShade; - if (environment > 0.f) - { - luminance *= environment; - } + auto const luminance = static_cast(Global.fLuminance) * gfx::vehicle_light_level(scenelight.owner); renderlight.diffuse = glm::vec4{glm::max(glm::vec3{colors::none}, scenelight.color - glm::vec3{luminance}), renderlight.diffuse[3]}; renderlight.ambient = glm::vec4{glm::max(glm::vec3{colors::none}, scenelight.color * glm::vec3{scenelight.intensity} - glm::vec3{luminance}), renderlight.ambient[3]}; renderlight.factor = (scenelight.count * 0.5) * (scenelight.owner->DimHeadlights ? 0.5 : 1.0); diff --git a/rendering/vulkan14/vulkan14scene.cpp b/rendering/vulkan14/vulkan14scene.cpp index aaad743b..f8752353 100644 --- a/rendering/vulkan14/vulkan14scene.cpp +++ b/rendering/vulkan14/vulkan14scene.cpp @@ -202,6 +202,11 @@ std::array, 3> pass_ auto const probeempty{scene.cabprobe_empty ? scene.cabprobe_empty->view() : scene.env_empty->view()}; views[CAB_PROBE_TEX] = (colour && (probelight != nullptr) ? probelight->view() : probeempty); views[CAB_PROBE_ALBEDO_TEX] = (colour && (probealbedo != nullptr) ? probealbedo->view() : probeempty); + // the occlusion of the sky by the scenery (the track T), for every pass which shades + auto const *occlusionheights{scene.skyocclusion.heights()}; + auto const *occlusion{scene.skyocclusion.occlusion()}; + views[SKY_OCCLUSION_HEIGHT_TEX] = (occlusionheights != nullptr ? occlusionheights->view() : scene.skyocclusion_heights_empty->view()); + views[SKY_OCCLUSION_TEX] = (occlusion != nullptr ? occlusion->view() : scene.skyocclusion_empty->view()); } return result; } @@ -663,6 +668,29 @@ bool vulkan14_renderer::init_scene(std::string &Reason) { return false; } + // the occlusion of the sky by the scenery: the stand-ins of its maps of one texel, cleared with the other stand-ins in the first + // frame (nothing reads them: the block of the scene tells there are no maps) + { + auto standin = [&](std::shared_ptr &Image, VkFormat const Format, char const *Name) { + vk14::image_desc desc; + desc.format = Format; + desc.extent = {1, 1, 1}; + desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + Image = std::make_shared(); + if (false == Image->create(ctx.memory, desc, Name)) + { + Image.reset(); + return false; + } + return true; + }; + if ((false == standin(scene.skyocclusion_heights_empty, VK_FORMAT_R32_SFLOAT, "empty sky occlusion heights")) || (false == standin(scene.skyocclusion_empty, VK_FORMAT_R16G16B16A16_SFLOAT, "empty sky occlusion"))) + { + Reason = "no stand-ins of the maps of the occlusion of the sky"; + return false; + } + scene.skyocclusion.init(ctx.memory, ctx.deletions); + } scene.lamps.init(device, ctx.memory, ctx.deletions); // the layer maps of the heightmap terrain drawn into the layers of its arrays by the copy shader @@ -832,6 +860,9 @@ void vulkan14_renderer::destroy_scene() destroy_weather(); destroy_ao(); destroy_cab_probe(); + scene.skyocclusion.destroy(); + scene.skyocclusion_heights_empty.reset(); + scene.skyocclusion_empty.reset(); for (auto *pool : scene.occlusion_pools) { vkDestroyQueryPool(device, pool, nullptr); @@ -1032,6 +1063,8 @@ void vulkan14_renderer::write_pass_sets(vk14::instance_cull *Cull) write(handle, CAB_PROBE_TEX, nullptr, image(ctx.sampling.clamp_linear, views[CAB_PROBE_TEX])); write(handle, CAB_PROBE_ALBEDO_TEX, nullptr, image(ctx.sampling.clamp_linear, views[CAB_PROBE_ALBEDO_TEX])); write(handle, CAB_PROBE_SSBO, &cabprobe, nullptr); + write(handle, SKY_OCCLUSION_HEIGHT_TEX, nullptr, image(ctx.sampling.repeat_nearest, views[SKY_OCCLUSION_HEIGHT_TEX])); + write(handle, SKY_OCCLUSION_TEX, nullptr, image(ctx.sampling.repeat_nearest, views[SKY_OCCLUSION_TEX])); } vkUpdateDescriptorSets(device, static_cast(writes.size()), writes.data(), 0, nullptr); } diff --git a/rendering/vulkan14/vulkan14shaders.cpp b/rendering/vulkan14/vulkan14shaders.cpp index 97e36ba4..2a94bd0d 100644 --- a/rendering/vulkan14/vulkan14shaders.cpp +++ b/rendering/vulkan14/vulkan14shaders.cpp @@ -807,6 +807,8 @@ std::string binding_preamble(bool const Raytracing) define("CAB_PROBE_TEX", CAB_PROBE_TEX); define("CAB_PROBE_ALBEDO_TEX", CAB_PROBE_ALBEDO_TEX); define("CAB_PROBE_SSBO", CAB_PROBE_SSBO); + define("SKY_OCCLUSION_HEIGHT_TEX", SKY_OCCLUSION_HEIGHT_TEX); + define("SKY_OCCLUSION_TEX", SKY_OCCLUSION_TEX); define("PASS_BINDING_COUNT", PASS_BINDING_COUNT); define("MATERIAL_TEXTURE_FIRST", MATERIAL_TEXTURE_FIRST); define("CL_CLUSTER_UBO", CL_CLUSTER_UBO); diff --git a/rendering/vulkan14/vulkan14skyocclusion.cpp b/rendering/vulkan14/vulkan14skyocclusion.cpp new file mode 100644 index 00000000..60746d78 --- /dev/null +++ b/rendering/vulkan14/vulkan14skyocclusion.cpp @@ -0,0 +1,160 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14skyocclusion.h" + +#include "rendering/vulkan14/vulkan14commands.h" +#include "utilities/Logs.h" + +namespace vk14 +{ + +namespace +{ + +// the shading and the compute passes (the fog, the particles) read them +VkPipelineStageFlags2 const READ_STAGES{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT}; + +} // namespace + +sky_occlusion_maps::~sky_occlusion_maps() +{ + let_go(); +} + +void sky_occlusion_maps::init(allocator &Memory, deletion_queue &Deletions) +{ + m_memory = &Memory; + m_deletions = &Deletions; +} + +void sky_occlusion_maps::destroy() +{ + // the device is idle: the maps go at once + m_deletions = nullptr; + let_go(); + m_memory = nullptr; +} + +bool sky_occlusion_maps::gpu_allocate(int const Side) +{ + let_go(); + if ((m_memory == nullptr) || (Side <= 0)) + { + return false; + } + auto make = [&](std::shared_ptr &Map, VkFormat const Format, char const *Name) { + image_desc desc; + desc.format = Format; + desc.extent = {static_cast(Side), static_cast(Side), 1}; + desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + Map = std::make_shared(); + if (false == Map->create(*m_memory, desc, Name)) + { + Map.reset(); + return false; + } + return true; + }; + if ((false == make(m_heights, VK_FORMAT_R32_SFLOAT, "sky occlusion heights")) || (false == make(m_occlusion, VK_FORMAT_R16G16B16A16_SFLOAT, "sky occlusion"))) + { + let_go(); + return false; + } + m_ready = false; + return true; +} + +void sky_occlusion_maps::gpu_release() +{ + let_go(); +} + +void sky_occlusion_maps::let_go() +{ + m_ready = false; + m_squares.clear(); + m_data.clear(); + if ((m_deletions != nullptr) && (m_heights || m_occlusion)) + { + // the frames in flight may still read them + m_deletions->push(m_value, [heights = std::move(m_heights), occlusion = std::move(m_occlusion)]() {}); + } + m_heights.reset(); + m_occlusion.reset(); +} + +void sky_occlusion_maps::gpu_upload(int const X, int const Y, int const Size, float const *Heights, std::uint16_t const *Occlusion) +{ + if (false == bool(m_heights)) + { + return; + } + auto const texels{static_cast(Size) * Size}; + auto const offset{m_data.size()}; + m_data.resize(offset + texels * (sizeof(float) + 4 * sizeof(std::uint16_t))); + std::memcpy(m_data.data() + offset, Heights, texels * sizeof(float)); + std::memcpy(m_data.data() + offset + texels * sizeof(float), Occlusion, texels * 4 * sizeof(std::uint16_t)); + m_squares.push_back(square{X, Y, Size, offset}); +} + +void sky_occlusion_maps::flush(command_stream *Commands) +{ + if ((Commands == nullptr) || (false == bool(m_heights)) || m_squares.empty()) + { + return; + } + auto staging{std::make_shared()}; + if ((false == staging->create(*m_memory, static_cast(m_data.size()), VK_BUFFER_USAGE_TRANSFER_SRC_BIT, memory_kind::upload, "sky occlusion upload")) || (staging->mapped() == nullptr)) + { + ErrorLog("sky occlusion: no staging buffer for the maps"); + return; + } + std::memcpy(staging->mapped(), m_data.data(), m_data.size()); + staging->flush(0, static_cast(m_data.size())); + transition(Commands, *m_heights, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT); + transition(Commands, *m_occlusion, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT); + std::vector heights, occlusion; + for (auto const &entry : m_squares) + { + VkBufferImageCopy2 region{}; + region.sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2; + region.bufferOffset = static_cast(entry.offset); + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageOffset = {entry.x, entry.y, 0}; + region.imageExtent = {static_cast(entry.size), static_cast(entry.size), 1}; + heights.push_back(region); + region.bufferOffset += static_cast(entry.size) * entry.size * sizeof(float); + occlusion.push_back(region); + } + auto copy = [&](image &Target, std::vector const &Regions) { + VkCopyBufferToImageInfo2 info{}; + info.sType = VK_STRUCTURE_TYPE_COPY_BUFFER_TO_IMAGE_INFO_2; + info.srcBuffer = staging->handle(); + info.dstImage = Target.handle(); + info.dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + info.regionCount = static_cast(Regions.size()); + info.pRegions = Regions.data(); + Commands->copy_buffer_to_image2(&info); + }; + copy(*m_heights, heights); + copy(*m_occlusion, occlusion); + transition(Commands, *m_heights, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, READ_STAGES, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); + transition(Commands, *m_occlusion, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, READ_STAGES, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT); + if (m_deletions != nullptr) + { + m_deletions->push(m_value, [staging]() {}); + } + m_squares.clear(); + m_data.clear(); + m_ready = true; +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14skyocclusion.h b/rendering/vulkan14/vulkan14skyocclusion.h new file mode 100644 index 00000000..3df7880c --- /dev/null +++ b/rendering/vulkan14/vulkan14skyocclusion.h @@ -0,0 +1,79 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +// the maps of the occlusion of the sky by the scenery in the vulkan renderer (the track T, opengl46_sky_occlusion): the images of +// gfx::sky_occlusion, which works out what goes into them. the squares of the tiles it gives are gathered in a frame and copied at +// once from one staging buffer, with the commands of the frame (flush()) + +#pragma once + +#include "rendering/common/skyocclusion.h" +#include "rendering/vulkan14/vulkan14memory.h" + +#include + +namespace vk14 +{ + +class command_stream; + +class sky_occlusion_maps : public gfx::sky_occlusion +{ + public: + sky_occlusion_maps() = default; + ~sky_occlusion_maps() override; + + // Memory, Deletions: what the maps are made and let go with; called once + void init(allocator &Memory, deletion_queue &Deletions); + // the maps go (the device is about to) + void destroy(); + // the value of the timeline the gpu reaches when it's done with the frame being recorded + void begin_frame(std::uint64_t const Value) + { + m_value = Value; + } + // the squares given in the frame copied to the maps, which are left in the layout the shading reads them in + void flush(command_stream *Commands); + // the maps once they're filled, for the pass sets; null: the stand-ins are read + image *heights() const + { + return m_ready ? m_heights.get() : nullptr; + } + image *occlusion() const + { + return m_ready ? m_occlusion.get() : nullptr; + } + + protected: + bool gpu_allocate(int const Side) override; + void gpu_release() override; + void gpu_upload(int const X, int const Y, int const Size, float const *Heights, std::uint16_t const *Occlusion) override; + + private: + void let_go(); + + allocator *m_memory{nullptr}; + deletion_queue *m_deletions{nullptr}; + std::uint64_t m_value{0}; + std::shared_ptr m_heights; + std::shared_ptr m_occlusion; + bool m_ready{false}; + // the squares waiting for the copy: where, and their data one after another (the heights, then the occlusion of each) + struct square + { + int x; + int y; + int size; + std::size_t offset; // of the heights in m_data + }; + std::vector m_squares; + std::vector m_data; +}; + +} // namespace vk14 diff --git a/shaders/ogl46/common.glsl b/shaders/ogl46/common.glsl index 98cdfbbe..1aad1367 100644 --- a/shaders/ogl46/common.glsl +++ b/shaders/ogl46/common.glsl @@ -109,6 +109,8 @@ #define SCENE_AO_TEX 26 #define CAB_PROBE_TEX 27 #define CAB_PROBE_ALBEDO_TEX 28 +#define SKY_OCCLUSION_HEIGHT_TEX 29 +#define SKY_OCCLUSION_TEX 30 #define CAB_PROBE_SSBO_BINDING 17 #define CAB_PROBE_SH_SIZE 16 #define CAB_PROBE_SH_GROUP_SIZE 64 @@ -329,6 +331,10 @@ layout(std140, binding = SCENE_UBO_BINDING) uniform scene_ubo vec4 cab_probe_lamp; vec4 cab_probe_lamp_color; vec4 cab_probe_lamp_shadow; + // the occlusion of the sky and of the sun by the scenery around the camera, see gl::scene_ubs + vec4 sky_occlusion_origin; + vec4 sky_occlusion_params; + vec4 sky_occlusion_center; }; #ifdef STAGE_VERTEX diff --git a/shaders/ogl46/fog_inject.comp b/shaders/ogl46/fog_inject.comp index 9dcb0c43..c5452c19 100644 --- a/shaders/ogl46/fog_inject.comp +++ b/shaders/ogl46/fog_inject.comp @@ -12,6 +12,7 @@ #include "common.glsl" #include "clouds_common.glsl" +#include "sky_occlusion.glsl" layout(local_size_x = WEATHER_GROUP_SIZE, local_size_y = WEATHER_GROUP_SIZE, local_size_z = 1) in; @@ -253,14 +254,16 @@ void main() { vec3 direction = normalize(world); // the light of the sky and the sun, held back by the shadows - float sunlit = cloud_shadow(world); + // the scenery around holds back the sky and the key light (tunnels, under bridges: gfx.skyvisibility.fog) + vec2 skyoccl = (sky_occlusion_origin.w > 1.5) ? sky_occlusion_world(world, vec3(0.0)) : vec2(1.0); + float sunlit = cloud_shadow(world) * skyoccl.y; if (options.z > 0.5 && sunlit > 0.0) { // two points of the segment, one in each half: a shaft of light no longer comes and goes with the one point sampled float shadow = sun_shadow(start + raydirection * (span * 0.5 * offset.z)) + sun_shadow(start + raydirection * (span * 0.5 * (offset.z + 1.0))); sunlit *= 1.0 - 0.5 * shadow; } - vec3 light = fog_whiten(fog_color_world(direction), (ground + clouds) / extinction) * fog_light_factor(sunlit); + vec3 light = fog_whiten(fog_color_world(direction), (ground + clouds) / extinction) * ((1.0 - fog_light.x) * skyoccl.x + fog_light.x * sunlit); // lamps and headlights of the clusters the segment of the cell goes through, each part of the segment with the lights of // its own cluster. the lights of the cluster of the point sampled would change with the point, frame to frame, where the // cell reaches into two clusters which don't hold the same lights (a full cluster leaves the least important out) diff --git a/shaders/ogl46/light_common.glsl b/shaders/ogl46/light_common.glsl index ebd83acc..274eaba7 100644 --- a/shaders/ogl46/light_common.glsl +++ b/shaders/ogl46/light_common.glsl @@ -24,6 +24,7 @@ layout(std430, binding = CAB_PROBE_SSBO_BINDING) readonly buffer cab_probe_ssbo #include "ssr.glsl" #include "conversion.glsl" #include "weather_common.glsl" +#include "sky_occlusion.glsl" float glossiness = 1.0; float metalic = 0.0; @@ -860,6 +861,10 @@ vec3 shade_surface(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float ref // attenuated by AMBIENT_SCALE - this only dims the indirect term. fragcolor *= AMBIENT_SCALE; } + // the scenery around holds back the sky (x) and the key light (y): tunnels, the ground under bridges and roofs, cuttings. not + // the light of the lamps and the headlights, nor of the cab lamp bounced around the cab, which come in below + vec2 skyoccl = sky_occlusion_world(mat3(inv_view) * f_pos.xyz, normalize(mat3(inv_view) * fragnormal)); + fragcolor *= skyoccl.x; vec3 emissioncolor = vec3(emission); vec3 view_dir = normalize(-f_pos.xyz); @@ -919,7 +924,8 @@ vec3 shade_surface(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float ref vec4 scenecolor = vec4(0.0); if (!matte) { - envcolor = reflection_surroundings(fragnormal, env_roughness); + // the sky the surface would reflect is held back as the sky which lights it + envcolor = reflection_surroundings(fragnormal, env_roughness) * skyoccl.x; // inside the cab the cab is what the surroundings are, where the probe holds it if (cabprobe && (cab_probe_params.y > 0.0)) envcolor = cab_probe_reflection(envcolor, fragnormal, env_roughness, cabambient); @@ -942,6 +948,7 @@ vec3 shade_surface(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float ref // the clouds hold back the sun (the moon at night) where their shadow falls float cloudsun = cloud_shadow(mat3(inv_view) * f_pos.xyz); // Sun HDR scale -> SUN_DIFFUSE_SCALE, the same for the diffuse and the specular light + cloudsun *= skyoccl.y; vec3 sunlight = lights[0].color * (SUN_DIFFUSE_SCALE * lights[0].intensity * cloudsun); float shadow = 0.0; diff --git a/shaders/ogl46/particle_light.glsl b/shaders/ogl46/particle_light.glsl index 42e4db24..0cceda65 100644 --- a/shaders/ogl46/particle_light.glsl +++ b/shaders/ogl46/particle_light.glsl @@ -8,6 +8,8 @@ #ifndef PARTICLE_LIGHT_GLSL #define PARTICLE_LIGHT_GLSL +#include "sky_occlusion.glsl" + layout(binding = SHADOW_TEX) uniform sampler2DArrayShadow shadowmap; layout(binding = HEADLIGHT_TEX) uniform sampler2D headlightmap; layout(binding = HEADLIGHT_SHADOW_TEX) uniform sampler2DArrayShadow headlight_shadowmap; @@ -112,10 +114,12 @@ uint particle_cluster(vec3 position) vec3 particle_light(vec3 position, vec3 world, float forward) { vec3 tocamera = -normalize(position); - vec3 light = ambient * PARTICLE_AMBIENT_SCALE; + // the scenery around holds back the sky and the key light (tunnels, bridges, roofs, gfx::sky_occlusion) + vec2 skyoccl = sky_occlusion_world(world, vec3(0.0)); + vec3 light = ambient * (PARTICLE_AMBIENT_SCALE * skyoccl.x); if (lights_count > 0U) { - float sunlit = cloud_shadow(world) * (1.0 - particle_sun_shadow(position)); + float sunlit = cloud_shadow(world) * skyoccl.y * (1.0 - particle_sun_shadow(position)); if (sunlit > 0.0) light += lights[0].color * (0.5 * PARTICLE_SUN_SCALE * lights[0].intensity * sunlit * particle_phase(dot(normalize(lights[0].dir), tocamera), forward)); } diff --git a/shaders/ogl46/particles_precipitation.comp b/shaders/ogl46/particles_precipitation.comp index 4973cd53..bb450975 100644 --- a/shaders/ogl46/particles_precipitation.comp +++ b/shaders/ogl46/particles_precipitation.comp @@ -64,6 +64,8 @@ void main() vec3 walls = abs(world) / (0.5 * size); opacity *= 1.0 - smoothstep(0.7, 1.0, max(max(walls.x, walls.y), walls.z)); opacity *= smoothstep(0.15, 0.6, length(world)); + // none under the roofs, the bridges, in the tunnels + opacity *= 1.0 - sky_occlusion_cover(world); if (opacity <= 0.002) return; vec3 tail = -move * timing.y; diff --git a/shaders/ogl46/sky_occlusion.glsl b/shaders/ogl46/sky_occlusion.glsl new file mode 100644 index 00000000..be800702 --- /dev/null +++ b/shaders/ogl46/sky_occlusion.glsl @@ -0,0 +1,73 @@ +// the occlusion of the sky and of the sun by the scenery around the camera (gfx.skyvisibility, gfx::sky_occlusion, the track T): +// toroidal maps of the square around the camera, read texel by texel. SKY_OCCLUSION_HEIGHT_TEX holds the height of the top of the +// scenery over a texel (the highest surface, world space), SKY_OCCLUSION_TEX for the texel: x the share of the sky a surface facing +// up sees above the top, y the same under the ceiling (the vault of a tunnel, the deck of a bridge), z the height above the top over +// which the key light (the sun, the moon) comes, w the height above the top under which it comes in under the ceilings. +// the same as gfx::sky_occlusion::sample() of rendering/common/skyocclusion.cpp, which the cpu works out the light around the +// vehicles with: a change here goes there as well +#ifndef SKY_OCCLUSION_GLSL +#define SKY_OCCLUSION_GLSL + +layout(binding = SKY_OCCLUSION_HEIGHT_TEX) uniform sampler2D sky_occlusion_height; +layout(binding = SKY_OCCLUSION_TEX) uniform sampler2D sky_occlusion_map; + +// a point this far below the top of a texel is under it, fully this much further; half the height over which the key light comes +// in at the edge of its shadow +const float SKY_OCCLUSION_COVER_START = 0.75; +const float SKY_OCCLUSION_COVER_SPAN = 1.0; +const float SKY_OCCLUSION_SUN_SOFTNESS = 0.6; + +// the share of the sky (x) and of the key light (y) a point gets, 0..1. World: its offset from the camera along the world axes, +// Worldnormal: the normal of its surface in the world space (zero for a point in the air), the texels are read a little in front of +// the surface (a wall reads the ground in front of it, not the roof above it) +vec2 sky_occlusion_world(vec3 world, vec3 worldnormal) +{ + if (sky_occlusion_origin.w <= 0.0) + return vec2(1.0); + float fade = clamp((max(abs(world.x - sky_occlusion_center.x), abs(world.z - sky_occlusion_center.y)) - sky_occlusion_params.z) / max(sky_occlusion_params.w, 1e-3), 0.0, 1.0); + if (fade >= 1.0) + return vec2(1.0); + float texel = sky_occlusion_params.x / sky_occlusion_params.y; + vec3 point = world + worldnormal * (0.75 * texel); + vec2 coords = (point.xz + sky_occlusion_origin.xy) / texel - 0.5; + vec2 base = floor(coords); + vec2 f = coords - base; + vec2 uv = (base + 1.0) / sky_occlusion_params.y; + // the four texels around the point, each with its own top: under one of them a point gets what is under it, above it what is + // above, and the four are blended as bilinear filtering would + vec4 tops = textureGather(sky_occlusion_height, uv, 0); + vec4 skytop = textureGather(sky_occlusion_map, uv, 0); + vec4 skyunder = textureGather(sky_occlusion_map, uv, 1); + vec4 litabove = textureGather(sky_occlusion_map, uv, 2); + vec4 litbelow = textureGather(sky_occlusion_map, uv, 3); + float height = point.y + sky_occlusion_origin.z; + vec4 cover = clamp((tops - vec4(height) - SKY_OCCLUSION_COVER_START) / SKY_OCCLUSION_COVER_SPAN, 0.0, 1.0); + vec4 sky = mix(skytop, skyunder, cover); + vec4 suntop = smoothstep(vec4(-SKY_OCCLUSION_SUN_SOFTNESS), vec4(SKY_OCCLUSION_SUN_SOFTNESS), vec4(height) - (tops + litabove)); + vec4 sununder = smoothstep(vec4(-SKY_OCCLUSION_SUN_SOFTNESS), vec4(SKY_OCCLUSION_SUN_SOFTNESS), (tops + litbelow) - vec4(height)); + vec4 sun = mix(suntop, sununder, cover); + // the order of the texels of a gather: (0, 1), (1, 1), (1, 0), (0, 0) + vec4 weights = vec4((1.0 - f.x) * f.y, f.x * f.y, f.x * (1.0 - f.y), (1.0 - f.x) * (1.0 - f.y)); + return mix(vec2(dot(sky, weights), dot(sun, weights)), vec2(1.0), fade); +} + +// how much of a point in the air is under the scenery (0 in the open, 1 under a roof, a bridge, in a tunnel), for what falls from +// the sky. World: its offset from the camera along the world axes +float sky_occlusion_cover(vec3 world) +{ + if (sky_occlusion_origin.w <= 0.0) + return 0.0; + float fade = clamp((max(abs(world.x - sky_occlusion_center.x), abs(world.z - sky_occlusion_center.y)) - sky_occlusion_params.z) / max(sky_occlusion_params.w, 1e-3), 0.0, 1.0); + if (fade >= 1.0) + return 0.0; + float texel = sky_occlusion_params.x / sky_occlusion_params.y; + vec2 coords = (world.xz + sky_occlusion_origin.xy) / texel - 0.5; + vec2 base = floor(coords); + vec2 f = coords - base; + vec4 tops = textureGather(sky_occlusion_height, (base + 1.0) / sky_occlusion_params.y, 0); + vec4 cover = clamp((tops - vec4(world.y + sky_occlusion_origin.z) - SKY_OCCLUSION_COVER_START) / SKY_OCCLUSION_COVER_SPAN, 0.0, 1.0); + vec4 weights = vec4((1.0 - f.x) * f.y, f.x * f.y, f.x * (1.0 - f.y), (1.0 - f.x) * (1.0 - f.y)); + return dot(cover, weights) * (1.0 - fade); +} + +#endif diff --git a/shaders/vk14/common.glsl b/shaders/vk14/common.glsl index ebbdadc9..78a5af3b 100644 --- a/shaders/vk14/common.glsl +++ b/shaders/vk14/common.glsl @@ -65,7 +65,9 @@ #define CAB_PROBE_TEX 28 #define CAB_PROBE_ALBEDO_TEX 29 #define CAB_PROBE_SSBO 30 -#define PASS_BINDING_COUNT 31 +#define SKY_OCCLUSION_HEIGHT_TEX 31 +#define SKY_OCCLUSION_TEX 32 +#define PASS_BINDING_COUNT 33 #define MATERIAL_TEXTURE_FIRST 0 #define CL_CLUSTER_UBO 0 #define CL_AABB_SSBO 1 @@ -296,6 +298,10 @@ layout(std140, set = PASS_SET, binding = SCENE_UBO) uniform scene_ubo vec4 cab_probe_lamp; vec4 cab_probe_lamp_color; vec4 cab_probe_lamp_shadow; + // the occlusion of the sky and of the sun by the scenery around the camera, see gl::scene_ubs + vec4 sky_occlusion_origin; + vec4 sky_occlusion_params; + vec4 sky_occlusion_center; }; #ifndef LAYOUT_COMPUTE diff --git a/shaders/vk14/fog_inject.comp b/shaders/vk14/fog_inject.comp index bf21ed6d..ff952120 100644 --- a/shaders/vk14/fog_inject.comp +++ b/shaders/vk14/fog_inject.comp @@ -13,6 +13,7 @@ #define LAYOUT_COMPUTE 1 #include "common.glsl" #include "clouds_common.glsl" +#include "sky_occlusion.glsl" layout(local_size_x = WEATHER_GROUP_SIZE, local_size_y = WEATHER_GROUP_SIZE, local_size_z = 1) in; @@ -258,14 +259,16 @@ void main() { vec3 direction = normalize(world); // the light of the sky and the sun, held back by the shadows - float sunlit = cloud_shadow(world); + // the scenery around holds back the sky and the key light (tunnels, under bridges: gfx.skyvisibility.fog) + vec2 skyoccl = (sky_occlusion_origin.w > 1.5) ? sky_occlusion_world(world, vec3(0.0)) : vec2(1.0); + float sunlit = cloud_shadow(world) * skyoccl.y; if (options.z > 0.5 && sunlit > 0.0) { // two points of the segment, one in each half: a shaft of light no longer comes and goes with the one point sampled float shadow = sun_shadow(start + raydirection * (span * 0.5 * offset.z)) + sun_shadow(start + raydirection * (span * 0.5 * (offset.z + 1.0))); sunlit *= 1.0 - 0.5 * shadow; } - vec3 light = fog_whiten(fog_color_world(direction), (ground + clouds) / extinction) * fog_light_factor(sunlit); + vec3 light = fog_whiten(fog_color_world(direction), (ground + clouds) / extinction) * ((1.0 - fog_light.x) * skyoccl.x + fog_light.x * sunlit); // lamps and headlights of the clusters the segment of the cell goes through, each part of the segment with the lights of // its own cluster. the lights of the cluster of the point sampled would change with the point, frame to frame, where the // cell reaches into two clusters which don't hold the same lights (a full cluster leaves the least important out) diff --git a/shaders/vk14/light_common.glsl b/shaders/vk14/light_common.glsl index 5f1dc464..424be4a5 100644 --- a/shaders/vk14/light_common.glsl +++ b/shaders/vk14/light_common.glsl @@ -28,6 +28,7 @@ layout(std430, set = PASS_SET, binding = CAB_PROBE_SSBO) readonly buffer cab_pro #include "ssr.glsl" #include "conversion.glsl" #include "weather_common.glsl" +#include "sky_occlusion.glsl" float glossiness = 1.0; float metalic = 0.0; @@ -873,6 +874,10 @@ vec3 shade_surface(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float ref // attenuated by AMBIENT_SCALE - this only dims the indirect term. fragcolor *= AMBIENT_SCALE; } + // the scenery around holds back the sky (x) and the key light (y): tunnels, the ground under bridges and roofs, cuttings. not + // the light of the lamps and the headlights, nor of the cab lamp bounced around the cab, which come in below + vec2 skyoccl = sky_occlusion_world(mat3(inv_view) * f_pos.xyz, normalize(mat3(inv_view) * fragnormal)); + fragcolor *= skyoccl.x; vec3 emissioncolor = vec3(emission); vec3 view_dir = normalize(-f_pos.xyz); @@ -932,7 +937,8 @@ vec3 shade_surface(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float ref vec4 scenecolor = vec4(0.0); if (!matte) { - envcolor = reflection_surroundings(fragnormal, env_roughness); + // the sky the surface would reflect is held back as the sky which lights it + envcolor = reflection_surroundings(fragnormal, env_roughness) * skyoccl.x; // inside the cab the cab is what the surroundings are, where the probe holds it if (cabprobe && (cab_probe_params.y > 0.0)) envcolor = cab_probe_reflection(envcolor, fragnormal, env_roughness, cabambient); @@ -956,6 +962,7 @@ vec3 shade_surface(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float ref // the clouds hold back the sun (the moon at night) where their shadow falls float cloudsun = cloud_shadow(mat3(inv_view) * f_pos.xyz); // Sun HDR scale -> SUN_DIFFUSE_SCALE, the same for the diffuse and the specular light + cloudsun *= skyoccl.y; vec3 sunlight = lights[0].color * (SUN_DIFFUSE_SCALE * lights[0].intensity * cloudsun); float shadow = 0.0; diff --git a/shaders/vk14/particle_light.glsl b/shaders/vk14/particle_light.glsl index 88a9d815..e7605acd 100644 --- a/shaders/vk14/particle_light.glsl +++ b/shaders/vk14/particle_light.glsl @@ -8,6 +8,8 @@ #ifndef PARTICLE_LIGHT_GLSL #define PARTICLE_LIGHT_GLSL +#include "sky_occlusion.glsl" + layout(set = PASS_SET, binding = SHADOW_TEX) uniform sampler2DArrayShadow shadowmap; layout(set = PASS_SET, binding = HEADLIGHT_TEX) uniform sampler2D headlightmap; layout(set = PASS_SET, binding = HEADLIGHT_SHADOW_TEX) uniform sampler2DArrayShadow headlight_shadowmap; @@ -112,10 +114,12 @@ uint particle_cluster(vec3 position) vec3 particle_light(vec3 position, vec3 world, float forward) { vec3 tocamera = -normalize(position); - vec3 light = ambient * PARTICLE_AMBIENT_SCALE; + // the scenery around holds back the sky and the key light (tunnels, bridges, roofs, gfx::sky_occlusion) + vec2 skyoccl = sky_occlusion_world(world, vec3(0.0)); + vec3 light = ambient * (PARTICLE_AMBIENT_SCALE * skyoccl.x); if (lights_count > 0U) { - float sunlit = cloud_shadow(world) * (1.0 - particle_sun_shadow(position)); + float sunlit = cloud_shadow(world) * skyoccl.y * (1.0 - particle_sun_shadow(position)); if (sunlit > 0.0) light += lights[0].color * (0.5 * PARTICLE_SUN_SCALE * lights[0].intensity * sunlit * particle_phase(dot(normalize(lights[0].dir), tocamera), forward)); } diff --git a/shaders/vk14/particles_precipitation.comp b/shaders/vk14/particles_precipitation.comp index 6935ae46..917f63c5 100644 --- a/shaders/vk14/particles_precipitation.comp +++ b/shaders/vk14/particles_precipitation.comp @@ -71,6 +71,8 @@ void main() vec3 walls = abs(world) / (0.5 * size); opacity *= 1.0 - smoothstep(0.7, 1.0, max(max(walls.x, walls.y), walls.z)); opacity *= smoothstep(0.15, 0.6, length(world)); + // none under the roofs, the bridges, in the tunnels + opacity *= 1.0 - sky_occlusion_cover(world); if (opacity <= 0.002) return; vec3 tail = -move * timing.y; diff --git a/shaders/vk14/sky_occlusion.glsl b/shaders/vk14/sky_occlusion.glsl new file mode 100644 index 00000000..73252cef --- /dev/null +++ b/shaders/vk14/sky_occlusion.glsl @@ -0,0 +1,73 @@ +// the occlusion of the sky and of the sun by the scenery around the camera (gfx.skyvisibility, gfx::sky_occlusion, the track T): +// toroidal maps of the square around the camera, read texel by texel. SKY_OCCLUSION_HEIGHT_TEX holds the height of the top of the +// scenery over a texel (the highest surface, world space), SKY_OCCLUSION_TEX for the texel: x the share of the sky a surface facing +// up sees above the top, y the same under the ceiling (the vault of a tunnel, the deck of a bridge), z the height above the top over +// which the key light (the sun, the moon) comes, w the height above the top under which it comes in under the ceilings. +// the same as gfx::sky_occlusion::sample() of rendering/common/skyocclusion.cpp, which the cpu works out the light around the +// vehicles with: a change here goes there as well +#ifndef SKY_OCCLUSION_GLSL +#define SKY_OCCLUSION_GLSL + +layout(set = PASS_SET, binding = SKY_OCCLUSION_HEIGHT_TEX) uniform sampler2D sky_occlusion_height; +layout(set = PASS_SET, binding = SKY_OCCLUSION_TEX) uniform sampler2D sky_occlusion_map; + +// a point this far below the top of a texel is under it, fully this much further; half the height over which the key light comes +// in at the edge of its shadow +const float SKY_OCCLUSION_COVER_START = 0.75; +const float SKY_OCCLUSION_COVER_SPAN = 1.0; +const float SKY_OCCLUSION_SUN_SOFTNESS = 0.6; + +// the share of the sky (x) and of the key light (y) a point gets, 0..1. World: its offset from the camera along the world axes, +// Worldnormal: the normal of its surface in the world space (zero for a point in the air), the texels are read a little in front of +// the surface (a wall reads the ground in front of it, not the roof above it) +vec2 sky_occlusion_world(vec3 world, vec3 worldnormal) +{ + if (sky_occlusion_origin.w <= 0.0) + return vec2(1.0); + float fade = clamp((max(abs(world.x - sky_occlusion_center.x), abs(world.z - sky_occlusion_center.y)) - sky_occlusion_params.z) / max(sky_occlusion_params.w, 1e-3), 0.0, 1.0); + if (fade >= 1.0) + return vec2(1.0); + float texel = sky_occlusion_params.x / sky_occlusion_params.y; + vec3 point = world + worldnormal * (0.75 * texel); + vec2 coords = (point.xz + sky_occlusion_origin.xy) / texel - 0.5; + vec2 base = floor(coords); + vec2 f = coords - base; + vec2 uv = (base + 1.0) / sky_occlusion_params.y; + // the four texels around the point, each with its own top: under one of them a point gets what is under it, above it what is + // above, and the four are blended as bilinear filtering would + vec4 tops = textureGather(sky_occlusion_height, uv, 0); + vec4 skytop = textureGather(sky_occlusion_map, uv, 0); + vec4 skyunder = textureGather(sky_occlusion_map, uv, 1); + vec4 litabove = textureGather(sky_occlusion_map, uv, 2); + vec4 litbelow = textureGather(sky_occlusion_map, uv, 3); + float height = point.y + sky_occlusion_origin.z; + vec4 cover = clamp((tops - vec4(height) - SKY_OCCLUSION_COVER_START) / SKY_OCCLUSION_COVER_SPAN, 0.0, 1.0); + vec4 sky = mix(skytop, skyunder, cover); + vec4 suntop = smoothstep(vec4(-SKY_OCCLUSION_SUN_SOFTNESS), vec4(SKY_OCCLUSION_SUN_SOFTNESS), vec4(height) - (tops + litabove)); + vec4 sununder = smoothstep(vec4(-SKY_OCCLUSION_SUN_SOFTNESS), vec4(SKY_OCCLUSION_SUN_SOFTNESS), (tops + litbelow) - vec4(height)); + vec4 sun = mix(suntop, sununder, cover); + // the order of the texels of a gather: (0, 1), (1, 1), (1, 0), (0, 0) + vec4 weights = vec4((1.0 - f.x) * f.y, f.x * f.y, f.x * (1.0 - f.y), (1.0 - f.x) * (1.0 - f.y)); + return mix(vec2(dot(sky, weights), dot(sun, weights)), vec2(1.0), fade); +} + +// how much of a point in the air is under the scenery (0 in the open, 1 under a roof, a bridge, in a tunnel), for what falls from +// the sky. World: its offset from the camera along the world axes +float sky_occlusion_cover(vec3 world) +{ + if (sky_occlusion_origin.w <= 0.0) + return 0.0; + float fade = clamp((max(abs(world.x - sky_occlusion_center.x), abs(world.z - sky_occlusion_center.y)) - sky_occlusion_params.z) / max(sky_occlusion_params.w, 1e-3), 0.0, 1.0); + if (fade >= 1.0) + return 0.0; + float texel = sky_occlusion_params.x / sky_occlusion_params.y; + vec2 coords = (world.xz + sky_occlusion_origin.xy) / texel - 0.5; + vec2 base = floor(coords); + vec2 f = coords - base; + vec4 tops = textureGather(sky_occlusion_height, (base + 1.0) / sky_occlusion_params.y, 0); + vec4 cover = clamp((tops - vec4(world.y + sky_occlusion_origin.z) - SKY_OCCLUSION_COVER_START) / SKY_OCCLUSION_COVER_SPAN, 0.0, 1.0); + vec4 weights = vec4((1.0 - f.x) * f.y, f.x * f.y, f.x * (1.0 - f.y), (1.0 - f.x) * (1.0 - f.y)); + return dot(cover, weights) * (1.0 - fade); +} + +#endif diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 841b2ec3..331e7ccd 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1918,6 +1918,19 @@ global_settings::ConfigParse_gfx_sky( cParser &Parser, std::string_view const To gfx_cabprobe_interval = std::clamp( gfx_cabprobe_interval, 1, 120 ); } else if( Token == "gfx.cabprobe.bounce" ) { readvalue( gfx_cabprobe_bounce, 0.f, 4.f ); } else if( Token == "gfx.cabprobe.reflections" ) { readswitch( gfx_cabprobe_reflections ); } + else if( Token == "gfx.skyvisibility" ) { + // no/low/medium/high, or the number of the level + Parser.getTokens( 1 ); + std::string level; + Parser >> level; + gfx_skyvisibility = + ( level == "low" || level == "1" ) ? 1 : + ( level == "medium" || level == "yes" || level == "true" || level == "2" ) ? 2 : + ( level == "high" || level == "3" ) ? 3 : + 0; } + else if( Token == "gfx.skyvisibility.sun" ) { readswitch( gfx_skyvisibility_sun ); } + else if( Token == "gfx.skyvisibility.fog" ) { readswitch( gfx_skyvisibility_fog ); } + else if( Token == "gfx.tunnelflags" ) { readswitch( gfx_tunnelflags ); } 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 @@ -2394,6 +2407,10 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.cabprobe.interval", gfx_cabprobe_interval ); export_as_text( Output, "gfx.cabprobe.bounce", gfx_cabprobe_bounce ); export_as_text( Output, "gfx.cabprobe.reflections", gfx_cabprobe_reflections ); + export_as_text( Output, "gfx.skyvisibility", std::string{ gfx_skyvisibility == 3 ? "high" : gfx_skyvisibility == 2 ? "medium" : gfx_skyvisibility == 1 ? "low" : "no" } ); + export_as_text( Output, "gfx.skyvisibility.sun", gfx_skyvisibility_sun ); + export_as_text( Output, "gfx.skyvisibility.fog", gfx_skyvisibility_fog ); + export_as_text( Output, "gfx.tunnelflags", gfx_tunnelflags ); 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, "gfx.lights.shadows.lamps", gfx_lights_shadows_lamps ); diff --git a/utilities/Globals.h b/utilities/Globals.h index d9ddef40..5ff6ae9b 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -497,6 +497,18 @@ struct global_settings { int gfx_cabprobe_interval { 4 }; float gfx_cabprobe_bounce { 1.f }; bool gfx_cabprobe_reflections { true }; + // the occlusion of the sky and of the sun by the scenery around the camera (opengl 4.6 and vulkan, gfx::sky_occlusion, the track T): + // tunnels, the ground under bridges and roofs, cuttings and the foot of slopes are dark by their geometry, worked out in the + // background from the scenery seen from above. 0 off (nothing is occluded), 1 low (256 m around the camera, texels of 1 m), + // 2 medium (512 m, 1 m), 3 high (512 m, 0.5 m, searched farther); whether the sun (the moon) is held back too, and the light of + // the volumetric fog + int gfx_skyvisibility { 2 }; + bool gfx_skyvisibility_sun { true }; + bool gfx_skyvisibility_fog { true }; + // the environment flags of the tracks (tunnel, canyon) darken the tracks and the vehicles on them, as before the track T. off: the + // renderers leave them alone and the light comes from the geometry (gfx_skyvisibility); the flags stay in the scenery files for + // the sound and the driver + bool gfx_tunnelflags { false }; 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