From 719c729929d5f6249c63eee5935296b69f82aaca Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Mon, 26 Oct 2026 15:51:56 +0100 Subject: [PATCH] Improve rain shader --- CMakeLists.txt | 3 + gl/glsl_common.cpp | 10 + gl/ubo.h | 94 +- rendering/common/windscreenwater.cpp | 447 ++++++++ rendering/common/windscreenwater.h | 113 ++ rendering/opengl46/opengl46renderer.cpp | 18 +- rendering/opengl46/opengl46renderer.h | 2 + rendering/opengl46/opengl46water.cpp | 116 ++ rendering/opengl46/opengl46water.h | 55 + rendering/vulkan14/vulkan14bindings.h | 4 +- rendering/vulkan14/vulkan14context.h | 3 + rendering/vulkan14/vulkan14debug.cpp | 2 +- rendering/vulkan14/vulkan14frame.cpp | 12 + rendering/vulkan14/vulkan14scene.cpp | 38 + rendering/vulkan14/vulkan14shaders.cpp | 8 + rendering/vulkan14/vulkan14water.cpp | 244 ++++ rendering/vulkan14/vulkan14water.h | 96 ++ shaders/ogl46/common.glsl | 12 + shaders/ogl46/mat_rain_windscreen.frag | 1351 ++++++++++++++++++++--- shaders/ogl46/water.glsl | 113 ++ shaders/ogl46/water_bake.comp | 24 + shaders/ogl46/water_clear.comp | 18 + shaders/ogl46/water_gather.comp | 40 + shaders/ogl46/water_grid.comp | 52 + shaders/ogl46/water_merge.comp | 63 ++ shaders/ogl46/water_move.comp | 254 +++++ shaders/ogl46/water_settle.comp | 36 + shaders/ogl46/water_sim.glsl | 279 +++++ shaders/ogl46/water_spawn.comp | 65 ++ shaders/ogl46/water_trail.comp | 34 + shaders/vk14/common.glsl | 12 +- shaders/vk14/mat_rain_windscreen.frag | 1351 ++++++++++++++++++++--- shaders/vk14/water.glsl | 114 ++ shaders/vk14/water_bake.comp | 27 + shaders/vk14/water_clear.comp | 21 + shaders/vk14/water_gather.comp | 43 + shaders/vk14/water_grid.comp | 55 + shaders/vk14/water_merge.comp | 66 ++ shaders/vk14/water_move.comp | 257 +++++ shaders/vk14/water_settle.comp | 39 + shaders/vk14/water_sim.glsl | 279 +++++ shaders/vk14/water_spawn.comp | 68 ++ shaders/vk14/water_trail.comp | 37 + 43 files changed, 5620 insertions(+), 355 deletions(-) create mode 100644 rendering/common/windscreenwater.cpp create mode 100644 rendering/common/windscreenwater.h create mode 100644 rendering/opengl46/opengl46water.cpp create mode 100644 rendering/opengl46/opengl46water.h create mode 100644 rendering/vulkan14/vulkan14water.cpp create mode 100644 rendering/vulkan14/vulkan14water.h create mode 100644 shaders/ogl46/water.glsl create mode 100644 shaders/ogl46/water_bake.comp create mode 100644 shaders/ogl46/water_clear.comp create mode 100644 shaders/ogl46/water_gather.comp create mode 100644 shaders/ogl46/water_grid.comp create mode 100644 shaders/ogl46/water_merge.comp create mode 100644 shaders/ogl46/water_move.comp create mode 100644 shaders/ogl46/water_settle.comp create mode 100644 shaders/ogl46/water_sim.glsl create mode 100644 shaders/ogl46/water_spawn.comp create mode 100644 shaders/ogl46/water_trail.comp create mode 100644 shaders/vk14/water.glsl create mode 100644 shaders/vk14/water_bake.comp create mode 100644 shaders/vk14/water_clear.comp create mode 100644 shaders/vk14/water_gather.comp create mode 100644 shaders/vk14/water_grid.comp create mode 100644 shaders/vk14/water_merge.comp create mode 100644 shaders/vk14/water_move.comp create mode 100644 shaders/vk14/water_settle.comp create mode 100644 shaders/vk14/water_sim.glsl create mode 100644 shaders/vk14/water_spawn.comp create mode 100644 shaders/vk14/water_trail.comp diff --git a/CMakeLists.txt b/CMakeLists.txt index 21ceb453..920dfd49 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -318,6 +318,7 @@ set(SOURCES "rendering/common/terrainmaterials.cpp" "rendering/common/grassfield.cpp" "rendering/common/gpuparticles.cpp" +"rendering/common/windscreenwater.cpp" "rendering/common/locallights.cpp" "rendering/common/skylight.cpp" "rendering/common/weatherstate.cpp" @@ -416,6 +417,7 @@ if (WITH_OPENGL_MODERN) "rendering/opengl46/opengl46precipitation.cpp" "rendering/opengl46/opengl46particles.cpp" "rendering/opengl46/opengl46gpuparticles.cpp" + "rendering/opengl46/opengl46water.cpp" "rendering/opengl46/opengl46terrain.cpp" "rendering/opengl46/opengl46grass.cpp" "rendering/opengl46/opengl46backend.cpp" @@ -461,6 +463,7 @@ if (WITH_VULKAN) "rendering/vulkan14/vulkan14terrain.cpp" "rendering/vulkan14/vulkan14grass.cpp" "rendering/vulkan14/vulkan14particles.cpp" + "rendering/vulkan14/vulkan14water.cpp" "rendering/vulkan14/vulkan14debug.cpp" "rendering/vulkan14/vulkan14commands.cpp" "rendering/vulkan14/vulkan14raytracing.cpp" diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index e00810be..a938ce8c 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -40,6 +40,16 @@ void gl::glsl_common_setup() "#define PARTICLE_OWNER_SSBO_BINDING " + std::to_string(PARTICLE_OWNER_SSBO_BINDING) + "\n" + "#define PARTICLE_OUTPUT_SSBO_BINDING " + std::to_string(PARTICLE_OUTPUT_SSBO_BINDING) + "\n" + "#define PARTICLE_COMMAND_SSBO_BINDING " + std::to_string(PARTICLE_COMMAND_SSBO_BINDING) + "\n" + + "#define WATER_SLOTS " + std::to_string(WATER_SLOTS) + "\n" + + "#define WATER_DROPS " + std::to_string(WATER_DROPS) + "\n" + + "#define WATER_GRID " + std::to_string(WATER_GRID) + "\n" + + "#define WATER_CELL_DROPS " + std::to_string(WATER_CELL_DROPS) + "\n" + + "#define WATER_TRAIL " + std::to_string(WATER_TRAIL) + "\n" + + "#define WATER_MAP " + std::to_string(WATER_MAP) + "\n" + + "#define WATER_GROUP_SIZE " + std::to_string(WATER_GROUP_SIZE) + "\n" + + "#define WATER_SSBO_BINDING " + std::to_string(WATER_SSBO_BINDING) + "\n" + + "#define WATER_UBO_BINDING " + std::to_string(WATER_UBO_BINDING) + "\n" + + "#define WATER_MASK_TEX " + std::to_string(WATER_MASK_TEX) + "\n" + "#define INSTANCE_COUNTER_HEADER " + std::to_string(INSTANCE_COUNTER_HEADER) + "\n" + "#define INSTANCE_JOB_SLEEPERS " + std::to_string(INSTANCE_JOB_SLEEPERS) + "U\n" + "#define LIGHT_SSBO_BINDING " + std::to_string(LIGHT_SSBO_BINDING) + "\n" + diff --git a/gl/ubo.h b/gl/ubo.h index fa7563cb..929e615e 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -5,6 +5,8 @@ #include "buffer.h" #include #include +#include +#include #define UBS_PAD(x) uint8_t PAD[x] @@ -124,9 +126,12 @@ namespace gl // the reflection of the surroundings the screen space reflections and the ray tracing don't find, for every pass: x: albedo // of the ground below the horizon of the environment map (gfx.ambient.groundalbedo), yzw unused glm::vec4 env_params; + // the water on the windscreens (mat_rain_windscreen): xyz the velocity of the air against the vehicle of the camera (the + // share of the wind the particles of the rain drift with, less the velocity of the vehicle), world space, m/s; w unused + glm::vec4 rain_wind; }; - static_assert(sizeof(scene_ubs) == 688 + 80 * MAX_HEADLIGHT_SHADOWS, "bad size of ubs"); + static_assert(sizeof(scene_ubs) == 704 + 80 * MAX_HEADLIGHT_SHADOWS, "bad size of ubs"); const size_t MAX_PARAMS = 3; @@ -565,6 +570,93 @@ namespace gl }; static_assert(sizeof(particle_draw) == 48, "bad size of particle_draw"); + // the water on the windscreens (gfx::windscreen_water, water_*.comp, water.glsl, mat_rain_windscreen.frag): the glasses of the + // cab of the train simulated at once (each a material of mat_rain_windscreen), the drops a glass holds, the cells of the grid + // the drops are looked up by along each coordinate of the texture of the glass (uv) and the drops a cell lists, the texels of + // the map of the wetted paths and of the map of the glass (its axes, the blades of the wipers) along each coordinate, the work + // group of the passes + const GLuint WATER_SLOTS = 4; + const GLuint WATER_DROPS = 65536; + const GLuint WATER_GRID = 128; + const GLuint WATER_CELL_DROPS = 32; + const GLuint WATER_TRAIL = 1024; + const GLuint WATER_MAP = 256; + const GLuint WATER_GROUP_SIZE = 64; + // the binding points of the opengl renderer: the storage of the water (read by the glasses, written by the passes), the block + // of the step of the simulation (water_ubs) and the texture unit of the mask of the wipers the map is baked from. the storage + // point is used by nothing else, bound with the colour passes + const GLuint WATER_SSBO_BINDING = 16; + const GLuint WATER_UBO_BINDING = 6; + const size_t WATER_MASK_TEX = MAX_TEXTURES + HELPER_TEXTURES + 13; + // the parts of the storage of the water (std430, GLSL water_ssbo), offsets in bytes: the header of every glass (vec4), the + // counts of every glass (uvec4), the drops (two vec4 each), the axes of the map (two vec4 a texel), the blades of the map (vec4 + // a texel), the state of the drops (uint), the drops they flow into (uint), what they gather (four int), what they keep + // (vec4), then the cells of the grids (a count and the drops of each cell) and the wetted paths (uint) + const size_t WATER_SLOT_DROPS = static_cast(WATER_SLOTS) * WATER_DROPS; + const size_t WATER_SLOT_TEXELS = static_cast(WATER_SLOTS) * WATER_MAP * WATER_MAP; + const size_t WATER_HEADER_OFFSET = 0; + const size_t WATER_COUNTS_OFFSET = WATER_HEADER_OFFSET + 16 * WATER_SLOTS; + const size_t WATER_DROPS_OFFSET = WATER_COUNTS_OFFSET + 16 * WATER_SLOTS; + const size_t WATER_AXES_OFFSET = WATER_DROPS_OFFSET + 32 * WATER_SLOT_DROPS; + const size_t WATER_BLADES_OFFSET = WATER_AXES_OFFSET + 32 * WATER_SLOT_TEXELS; + const size_t WATER_STATE_OFFSET = WATER_BLADES_OFFSET + 16 * WATER_SLOT_TEXELS; + const size_t WATER_TARGET_OFFSET = WATER_STATE_OFFSET + 4 * WATER_SLOT_DROPS; + const size_t WATER_GATHER_OFFSET = WATER_TARGET_OFFSET + 4 * WATER_SLOT_DROPS; + const size_t WATER_EXTRA_OFFSET = WATER_GATHER_OFFSET + 16 * WATER_SLOT_DROPS; + const size_t WATER_CELLS_OFFSET = WATER_EXTRA_OFFSET + 16 * WATER_SLOT_DROPS; + const size_t WATER_CELLS_SIZE = 4 * static_cast(WATER_SLOTS) * WATER_GRID * WATER_GRID * (1 + WATER_CELL_DROPS); + const size_t WATER_TRAIL_OFFSET = WATER_CELLS_OFFSET + WATER_CELLS_SIZE; + const size_t WATER_STORAGE_SIZE = WATER_TRAIL_OFFSET + 4 * static_cast(WATER_SLOTS) * WATER_TRAIL * WATER_TRAIL; + // the parts of a glass in the storage: its drops and what the passes keep of them, its map (the axes the second part), its + // cells and its paths, its header and its counts, (offset, size) in bytes + inline std::array, 11> water_slot_parts(size_t const Slot) + { + auto const drops{static_cast(WATER_DROPS)}; + auto const texels{static_cast(WATER_MAP) * WATER_MAP}; + auto const cells{4 * static_cast(WATER_GRID) * WATER_GRID * (1 + WATER_CELL_DROPS)}; + auto const trail{4 * static_cast(WATER_TRAIL) * WATER_TRAIL}; + return {{{WATER_DROPS_OFFSET + 32 * drops * Slot, 32 * drops}, + {WATER_AXES_OFFSET + 32 * texels * Slot, 32 * texels}, + {WATER_BLADES_OFFSET + 16 * texels * Slot, 16 * texels}, + {WATER_STATE_OFFSET + 4 * drops * Slot, 4 * drops}, + {WATER_TARGET_OFFSET + 4 * drops * Slot, 4 * drops}, + {WATER_GATHER_OFFSET + 16 * drops * Slot, 16 * drops}, + {WATER_EXTRA_OFFSET + 16 * drops * Slot, 16 * drops}, + {WATER_CELLS_OFFSET + cells * Slot, cells}, + {WATER_TRAIL_OFFSET + trail * Slot, trail}, + {WATER_HEADER_OFFSET + 16 * Slot, 16}, + {WATER_COUNTS_OFFSET + 16 * Slot, 16}}}; + } + + // layout std140: the step of the simulation of the water on the windscreens (GLSL water_ubo), every vector in the space of the + // model of the cab + struct water_ubs + { + // x: seconds of the step, y: seconds the simulation runs (the wetted paths are marked with it), z: how heavy the rain or + // the snow is, 0 .. 1, w: 1 for snow + glm::vec4 step; + // xyz: up, w: the temperature of the air, degrees Celsius + glm::vec4 up; + // xyz: the air against the cab (the share of the wind the rain drifts with, less the velocity of the vehicle), m/s; w: how + // fast the rain or the snow falls, m/s + glm::vec4 air; + // the blades of the wipers (the channels of their mask) now and at the step before: 0 at rest .. 1 at the far end of + // their sweep, below 0 where there's none + glm::vec4 blades; + glm::vec4 blades_before; + // the drops of the rain: x the smallest diameter which lands as a drop of its own, mm (the smaller ones are the mist of the + // glass), y: 1 / the mean of the diameters above it, 1 / mm (they go as Marshall-Palmer), zw: unused + glm::vec4 rain; + // x: random seed of the step, y: the most landings tried on a glass in the step, z: the glass the passes for a single glass + // work on (the baking of the mask of the wipers) + glm::uvec4 counts; + // every glass: x the landings tried in the step, y 1 / the most metres squared of the glass a unit of its uv holds (the + // landings tried at the densest place are all taken), z 1 when it's simulated, w the size of its drops (the param + // rain_scale of its material) + glm::vec4 slots[WATER_SLOTS]; + }; + static_assert(sizeof(water_ubs) == 112 + 16 * WATER_SLOTS, "bad size of water_ubs"); + // layout std430: a bucket of a scene cell queued in the pass (GLSL: instance_cull_job) struct instance_cull_job { diff --git a/rendering/common/windscreenwater.cpp b/rendering/common/windscreenwater.cpp new file mode 100644 index 00000000..50c1147e --- /dev/null +++ b/rendering/common/windscreenwater.cpp @@ -0,0 +1,447 @@ +/* +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/windscreenwater.h" + +#include "model/Model3d.h" +#include "model/material.h" +#include "rendering/renderer.h" +#include "simulation/simulation.h" +#include "simulation/simulationenvironment.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "vehicle/DynObj.h" +#include "vehicle/Train.h" + +namespace gfx +{ + +namespace +{ + +// the param of mat_rain_windscreen the slot of a glass goes to (water_slot: param[2].z) and the one of the size of its drops +// (rain_scale: param[2].y) +std::size_t constexpr SLOT_PARAM{2}; +// how fast the rain and the snow fall, m/s (as the particles of the rain and the snow, gfx::gpu_particles) +float constexpr RAIN_FALL{7.f}; +float constexpr SNOW_FALL{1.1f}; +// the share of the wind the rain drifts with (as the particles of the rain and the snow) +double constexpr WIND_SHARE{0.5}; +// the smallest raindrop landing as a drop of its own, mm: the smaller ones are the mist of the wet glass +float constexpr LEAST_DROP{0.5f}; +// the water a flake of snow brings, mm3 on average (water_spawn.comp: 0.3 .. 2.5) +float constexpr FLAKE_WATER{1.4f}; +// the most landings tried on a glass in a step +std::uint32_t constexpr MOST_TRIES{1024}; +// the longest step, seconds: a longer frame slows the water down rather than let it jump +double constexpr LONGEST_STEP{0.05}; + +// whether the material is a glass: one of mat_rain_windscreen, with a mask of the wipers +bool is_glass(opengl_material const *Material) +{ + return (Material != nullptr) && (Material->shader != nullptr) && (Material->shader->texture_conf.count("wipermask") > 0) && (Material->shader->param_conf.count("water_slot") > 0); +} + +// calls Visit for every submodel of the branch, its children and its siblings +template void visit_submodels(TSubModel *Submodel, Visit &&Visitor) +{ + while (Submodel != nullptr) + { + Visitor(Submodel); + visit_submodels(Submodel->ChildGet(), Visitor); + Submodel = Submodel->NextGet(); + } +} + +// the vertices of a submodel, wherever they are kept +gfx::vertex_array const &submodel_vertices(TSubModel const *Submodel) +{ + return (Submodel->m_geometry.handle != null_handle ? GfxRenderer->Vertices(Submodel->m_geometry.handle) : Submodel->Vertices); +} + +// the transformation of a submodel to the space of the model +glm::mat4 submodel_transform(TSubModel const *Submodel) +{ + float4x4 matrix; + Submodel->ParentMatrix(&matrix); + return glm::make_mat4(matrix.readArray()); +} + +} // namespace + +bool windscreen_water::available() +{ + if (m_state == 0) + { + m_state = (gpu_init() ? 1 : -1); + if (m_state < 0) + { + WriteLog("windscreen water: the simulation can't be used, the windscreens make their water up"); + } + } + return m_state > 0; +} + +void windscreen_water::release() +{ + assign(false); + m_glasses = {}; + m_glasscount = 0; + m_vehicle = nullptr; + m_cab = nullptr; +} + +void windscreen_water::reset() +{ + release(); + m_state = 0; +} + +void windscreen_water::assign(bool const Assigned) +{ + if (false == bool(m_host.material)) + { + return; + } + for (std::size_t slot = 0; slot < m_glasscount; ++slot) + { + auto const &glass{m_glasses[slot]}; + if (auto *material{m_host.material(glass.material)}; is_glass(material)) + { + material->params[SLOT_PARAM].z = (Assigned && glass.baked ? static_cast(slot + 1) : 0.f); + } + } +} + +void windscreen_water::rasterize(TSubModel *Submodel, material_handle const Material, glm::vec3 const &Inside, std::vector &Axes) const +{ + if ((Submodel->m_material != Material) || (Submodel->eType != GL_TRIANGLES)) + { + return; + } + auto const &vertices{submodel_vertices(Submodel)}; + if (vertices.size() < 3) + { + return; + } + auto const transform{submodel_transform(Submodel)}; + gfx::index_array const *indices{nullptr}; + if ((Submodel->m_geometry.handle != null_handle) && (Submodel->m_geometry.index_count > 0)) + { + indices = &GfxRenderer->Indices(Submodel->m_geometry.handle); + } + else if (false == Submodel->Indices.empty()) + { + indices = &Submodel->Indices; + } + auto const count{indices != nullptr ? indices->size() : vertices.size()}; + auto const size{static_cast(gl::WATER_MAP)}; + for (std::size_t first = 0; first + 2 < count; first += 3) + { + std::array position; + std::array uv; + bool valid{true}; + for (std::size_t corner = 0; corner < 3; ++corner) + { + auto const index{indices != nullptr ? static_cast((*indices)[first + corner]) : first + corner}; + if (index >= vertices.size()) + { + valid = false; + break; + } + position[corner] = glm::vec3{transform * glm::vec4{vertices[index].position, 1.f}}; + uv[corner] = vertices[index].texture; + } + if (false == valid) + { + continue; + } + // the steps along the glass for the steps of u and v: the edges of the triangle in both spaces + glm::mat2 const uvedges{uv[1] - uv[0], uv[2] - uv[0]}; + auto const det{glm::determinant(uvedges)}; + if (std::abs(det) < 1e-12f) + { + continue; + } + auto const inverse{glm::inverse(uvedges)}; + auto const e1{position[1] - position[0]}; + auto const e2{position[2] - position[0]}; + // [tu tv] = [e1 e2] * inverse + auto const tu{e1 * inverse[0][0] + e2 * inverse[0][1]}; + auto const tv{e1 * inverse[1][0] + e2 * inverse[1][1]}; + auto const centre{(position[0] + position[1] + position[2]) / 3.f}; + auto const out{glm::dot(glm::cross(tu, tv), centre - Inside) >= 0.f ? 1.f : -1.f}; + // a triangle laid on a repeat of the texture is taken to the first one (the mask of the wipers repeats with it) + auto const shift{glm::floor((uv[0] + uv[1] + uv[2]) / 3.f)}; + for (auto &corner : uv) + { + corner -= shift; + } + auto const lower{glm::max(glm::ivec2{glm::floor(glm::min(glm::min(uv[0], uv[1]), uv[2]) * static_cast(size))}, glm::ivec2{0})}; + auto const upper{glm::min(glm::ivec2{glm::ceil(glm::max(glm::max(uv[0], uv[1]), uv[2]) * static_cast(size))}, glm::ivec2{size - 1})}; + for (int y = lower.y; y <= upper.y; ++y) + { + for (int x = lower.x; x <= upper.x; ++x) + { + // the middle of the texel inside the triangle (barycentric) + auto const point{(glm::vec2{static_cast(x), static_cast(y)} + 0.5f) / static_cast(size)}; + auto const weights{inverse * (point - uv[0])}; + if ((weights.x < -1e-4f) || (weights.y < -1e-4f) || (weights.x + weights.y > 1.f + 1e-4f)) + { + continue; + } + auto const texel{static_cast(y) * gl::WATER_MAP + static_cast(x)}; + Axes[texel * 2] = glm::vec4{tu, 1.f}; + Axes[texel * 2 + 1] = glm::vec4{tv, out}; + } + } + } +} + +void windscreen_water::build(TModel3d &Cab) +{ + auto *root{Cab.GetSMRoot()}; + if ((root == nullptr) || (false == bool(m_host.material))) + { + return; + } + // the materials of the glasses, in the order they come in the model; and a point inside the cab, the middle of the box + // around the whole model (the glasses are around it) + std::vector materials; + glm::vec3 lower{std::numeric_limits::max()}; + glm::vec3 upper{std::numeric_limits::lowest()}; + visit_submodels(root, [&](TSubModel *Submodel) { + auto const &vertices{submodel_vertices(Submodel)}; + if (false == vertices.empty()) + { + auto const transform{submodel_transform(Submodel)}; + for (auto const &vertex : vertices) + { + auto const position{glm::vec3{transform * glm::vec4{vertex.position, 1.f}}}; + lower = glm::min(lower, position); + upper = glm::max(upper, position); + } + } + auto const material{Submodel->m_material}; + if ((material != null_handle) && (material > 0) && (Submodel->eType == GL_TRIANGLES) && (std::find(materials.begin(), materials.end(), material) == materials.end()) && is_glass(m_host.material(material))) + { + materials.push_back(material); + } + }); + if (materials.empty()) + { + return; + } + if (materials.size() > gl::WATER_SLOTS) + { + WriteLog("windscreen water: the cab has " + std::to_string(materials.size()) + " glasses, the water of the first " + std::to_string(gl::WATER_SLOTS) + " is simulated"); + materials.resize(gl::WATER_SLOTS); + } + auto const inside{(lower + upper) * 0.5f}; + auto const texels{static_cast(gl::WATER_MAP) * gl::WATER_MAP}; + auto const size{static_cast(gl::WATER_MAP)}; + for (auto const material : materials) + { + std::vector axes(texels * 2, glm::vec4{0.f}); + visit_submodels(root, [&](TSubModel *Submodel) { rasterize(Submodel, material, inside, axes); }); + auto &glass{m_glasses[m_glasscount]}; + glass = windscreen_water::glass{}; + glass.material = material; + auto const *opengl{m_host.material(material)}; + glass.mask = opengl->textures[opengl->shader->texture_conf.at("wipermask").id]; + for (std::size_t texel = 0; texel < texels; ++texel) + { + if (axes[texel * 2].w > 0.f) + { + auto const density{glm::length(glm::cross(glm::vec3{axes[texel * 2]}, glm::vec3{axes[texel * 2 + 1]}))}; + glass.densest = std::max(glass.densest, density); + glass.area += density / static_cast(texels); + } + } + if (glass.densest <= 0.f) + { + continue; + } + // the edges spread out by two texels, so the filtering of the map near them takes the glass along + for (int pass = 0; pass < 2; ++pass) + { + auto const source{axes}; + for (int y = 0; y < size; ++y) + { + for (int x = 0; x < size; ++x) + { + auto const texel{static_cast(y * size + x)}; + if (source[texel * 2].w > 0.f) + { + continue; + } + for (int k = 0; k < 8; ++k) + { + static int const dx[]{1, -1, 0, 0, 1, 1, -1, -1}; + static int const dy[]{0, 0, 1, -1, 1, -1, 1, -1}; + auto const nx{x + dx[k]}; + auto const ny{y + dy[k]}; + if ((nx < 0) || (ny < 0) || (nx >= size) || (ny >= size)) + { + continue; + } + auto const neighbour{static_cast(ny * size + nx)}; + if (source[neighbour * 2].w > 0.f) + { + axes[texel * 2] = glm::vec4{glm::vec3{source[neighbour * 2]}, 0.5f}; + axes[texel * 2 + 1] = source[neighbour * 2 + 1]; + break; + } + } + } + } + } + gpu_setup(m_glasscount, axes); + WriteLog("windscreen water: glass " + std::to_string(m_glasscount + 1) + " of the cab, material " + opengl->GetName() + ", " + std::to_string(glass.area) + " m2"); + ++m_glasscount; + } +} + +void windscreen_water::update(double const Seconds) +{ + if (false == available()) + { + return; + } + auto const *vehicle{simulation::Train != nullptr ? simulation::Train->Dynamic() : nullptr}; + auto *cab{vehicle != nullptr ? vehicle->mdKabina : nullptr}; + if ((vehicle != m_vehicle) || (cab != m_cab)) + { + // another cab: its glasses dry + release(); + m_vehicle = vehicle; + m_cab = cab; + m_bladesbefore = glm::vec4{-1.f}; + if (cab != nullptr) + { + build(*cab); + } + } + if (m_glasscount == 0) + { + return; + } + + gl::water_ubs block{}; + // the blades of the wipers of the vehicle, as the glasses read them (the channels of the mask follow the wipers of the cab + // the vehicle is driven from) + glm::vec4 blades{-1.f}; + if (auto const cabactive{vehicle->MoverParameters->CabActive}; cabactive != 0) + { + auto const count{static_cast(vehicle->dWiperPos.size())}; + for (int i = 0; i < 4; ++i) + { + if (i < count) + { + auto const index{cabactive > 0 ? i : count - 1 - i}; + blades[i] = static_cast(std::clamp(vehicle->dWiperPos[index], 0.0, 1.0)); + } + } + } + // the space of the model of the cab, drawn with the matrix of the vehicle + auto const toworld{glm::dmat3{vehicle->mMatrix}}; + auto const tomodel{glm::inverse(toworld)}; + auto const up{glm::normalize(glm::vec3{tomodel * glm::dvec3{0.0, 1.0, 0.0}})}; + auto const air{glm::vec3{tomodel * (glm::dvec3{simulation::Environment.wind()} * WIND_SHARE - vehicle->VectorFront() * vehicle->MoverParameters->V)}}; + auto const heavy{std::clamp(Global.Overcast - 1.f, 0.f, 1.f)}; + auto const snow{Global.Weather == "snow:"}; + auto const fall{snow ? SNOW_FALL : RAIN_FALL}; + + // the masks of the wipers baked into the maps once the textures are there + for (std::size_t slot = 0; slot < m_glasscount; ++slot) + { + auto &glass{m_glasses[slot]}; + if (false == glass.baked) + { + block.counts = glm::uvec4{0u, 0u, static_cast(slot), 0u}; + glass.baked = gpu_bake(slot, glass.mask, block); + } + } + assign(true); + + auto const seconds{std::clamp(Seconds, 0.0, LONGEST_STEP)}; + if (seconds <= 0.0) + { + return; + } + m_time += seconds; + ++m_step; + + // the drops landing: as many raindrops a second come to a square metre of the ground as the rain gives water (from light rain at + // 1 mm/h to a downpour at 30 mm/h) in the drops of the Marshall-Palmer distribution above the least landing as a drop of its own + // (exponential, the mean diameter falling with the rain); a glass gets them in the share it faces the rain coming at it + // against the cab (water_spawn.comp) + float perground{0.f}; // the drops coming to a square metre of the ground in a second + auto const rate{1.f + 29.f * std::pow(heavy, 1.5f)}; // mm/h + auto const lambda{4.1f * std::pow(rate, -0.21f)}; // 1 / mm + if (heavy > 0.f) + { + auto const water{rate / 3600.f * 1e6f}; // mm3 a second on a square metre + if (snow) + { + perground = water * 0.25f / FLAKE_WATER; + } + else + { + // the share of the water in the drops above the least one, and their mean volume + auto const x{lambda * LEAST_DROP}; + auto const share{std::exp(-x) * (1.f + x + x * x / 2.f + x * x * x / 6.f)}; + auto const d0{LEAST_DROP}; + auto const mean{0.5236f * (d0 * d0 * d0 + 3.f * d0 * d0 / lambda + 6.f * d0 / (lambda * lambda) + 6.f / (lambda * lambda * lambda))}; + perground = water * share / mean; + } + } + // the landings tried on a glass: as if the whole of its uv held the glass at its densest, the ones at the less dense places and + // facing the rain less are left out. the share of a landing left over goes to the next step + auto const coming{glm::length(air - up * fall)}; + std::uint32_t most{0}; + m_tries = 0; + for (std::size_t slot = 0; slot < m_glasscount; ++slot) + { + auto &glass{m_glasses[slot]}; + auto const *material{m_host.material(glass.material)}; + auto const simulated{glass.baked && is_glass(material)}; + glass.landings += static_cast(perground / fall * coming * glass.densest) * seconds; + auto const whole{std::floor(glass.landings)}; + glass.landings -= whole; + auto const tries{simulated ? static_cast(std::min(whole, MOST_TRIES)) : 0u}; + most = std::max(most, tries); + m_tries += tries; + auto const scale{(material != nullptr) && std::isfinite(material->params[SLOT_PARAM].y) ? std::max(material->params[SLOT_PARAM].y, 0.05f) : 1.f}; + block.slots[slot] = glm::vec4{static_cast(tries), 1.f / glass.densest, simulated ? 1.f : 0.f, scale}; + } + + block.step = glm::vec4{static_cast(seconds), static_cast(m_time), heavy, snow ? 1.f : 0.f}; + block.up = glm::vec4{up, Global.AirTemperature}; + block.air = glm::vec4{air, fall}; + block.blades = blades; + block.blades_before = glm::vec4{ + m_bladesbefore.x < 0.f ? blades.x : m_bladesbefore.x, m_bladesbefore.y < 0.f ? blades.y : m_bladesbefore.y, m_bladesbefore.z < 0.f ? blades.z : m_bladesbefore.z, m_bladesbefore.w < 0.f ? blades.w : m_bladesbefore.w}; + block.rain = glm::vec4{LEAST_DROP, lambda, 1.f, 0.f}; + block.counts = glm::uvec4{m_step * 2654435761u, most, 0u, 0u}; + m_bladesbefore = blades; + gpu_step(block, most); +} + +std::string windscreen_water::info() const +{ + if ((m_state <= 0) || (m_glasscount == 0)) + { + return {}; + } + return "windscreen water: " + std::to_string(m_glasscount) + " glasses, " + std::to_string(m_tries) + " drops landing tried in the frame\n"; +} + +} // namespace gfx diff --git a/rendering/common/windscreenwater.h b/rendering/common/windscreenwater.h new file mode 100644 index 00000000..2c09951b --- /dev/null +++ b/rendering/common/windscreenwater.h @@ -0,0 +1,113 @@ +/* +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 "gl/ubo.h" +#include "interfaces/ITexture.h" +#include "utilities/Classes.h" + +struct opengl_material; +class TModel3d; +class TSubModel; +class TDynamicObject; + +namespace gfx +{ + +// the water on the windscreens of the cab of the train, simulated on the gpu, whichever api runs it. +// +// a glass is a material of mat_rain_windscreen (one with a mask of the wipers) in the model of the cab of the train, up to +// gl::WATER_SLOTS of them; the material is told its slot in its param water_slot (1 + the slot, the drawing of the glass reads +// the water of the slot where it's there, mat_rain_windscreen.frag). the water lies in the coordinates of the texture of the glass +// (uv), as the mask does. the map of a glass is made here from the triangles of the model with its material: for the texels of +// the uv on the glass, the step along the glass (metres, the space of the model of the cab) for a step of u and of v, and which +// way is out of the cab; the passes of the gpu bake the mask of the wipers into it. once a frame the step of the simulation is +// worked out here (the time, the rain or snow, gravity and the air against the cab in the space of its model, the blades) and +// the passes run it (water_*.comp: the landing drops, the drops moving and carried by the blades, the grids, the drops flowing +// together); what they keep and the glasses read is the storage of the water (water.glsl, gl::WATER_*_OFFSET) +class windscreen_water +{ + public: + // what the renderer gives: the materials of its manager, which get their slots + struct host + { + std::function material; + }; + + windscreen_water() = default; + virtual ~windscreen_water() = default; + windscreen_water(windscreen_water const &) = delete; + windscreen_water &operator=(windscreen_water const &) = delete; + + void set_host(host Host) + { + m_host = std::move(Host); + } + // whether the passes could be made + bool available(); + // once a frame, before the colour passes: the cab of the train followed, the step of the simulation run. Seconds: of the + // simulation since the frame before (0 when it's paused) + void update(double Seconds); + // the materials let go of their slots, the glasses forgotten (the storage stays as it is, unused) + void release(); + std::string info() const; + + protected: + // the glasses forgotten and the programs taken as not made yet (the renderer let go of them), made again when next needed + void reset(); + // the programs and the storage; false: the simulation can't be used, the glasses make their water up + virtual bool gpu_init() = 0; + // the slot set up for a glass: its drops, its grid and its paths cleared, its header saying it isn't simulated (until the + // first step), the axes of its map (two vec4 a texel, gl::WATER_MAP x WATER_MAP texels, see water.glsl) put in the storage + virtual void gpu_setup(std::size_t Slot, std::vector const &Axes) = 0; + // the mask of the wipers of a glass (a texture of the renderer) baked into its map; false when the texture isn't ready yet + // (tried again with the next frame) + virtual bool gpu_bake(std::size_t Slot, texture_handle Mask, gl::water_ubs const &Block) = 0; + // the step of the simulation of the glasses Block says are simulated; Tries: the most landings tried on a glass + virtual void gpu_step(gl::water_ubs const &Block, std::uint32_t Tries) = 0; + + private: + struct glass + { + material_handle material{null_handle}; + texture_handle mask{null_handle}; + float densest{0.f}; // the most metres squared a unit of the uv holds + float area{0.f}; // metres squared of the glass + double landings{0.0}; // the share of a landing left over from the steps before + bool baked{false}; + }; + + // the glasses of the model of a cab found and set up + void build(TModel3d &Cab); + // the triangles of a glass laid into its map; Inside: a point inside the cab + void rasterize(TSubModel *Submodel, material_handle Material, glm::vec3 const &Inside, std::vector &Axes) const; + // the slots given to the materials of the glasses (every frame: a material made anew takes its default params) + void assign(bool Assigned); + + host m_host; + int m_state{0}; // 0: not set up yet, 1: ready, -1: can't be used + TDynamicObject const *m_vehicle{nullptr}; + TModel3d const *m_cab{nullptr}; + std::array m_glasses{}; + std::size_t m_glasscount{0}; + glm::vec4 m_bladesbefore{-1.f}; + double m_time{0.0}; // seconds the simulation runs + std::uint32_t m_step{0}; + std::uint32_t m_tries{0}; // landings tried in the last step, all glasses +}; + +} // namespace gfx diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 31096763..14ff8ad3 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -86,6 +86,7 @@ bool opengl46_renderer::Init(GLFWwindow *Window) m_window = Window; gl::glsl_common_setup(); + m_windscreenwater.set_host({[this](material_handle const Material) { return &m_materials.material(Material); }}); if (true == Global.ScaleSpecularValues) { @@ -1086,7 +1087,8 @@ void opengl46_renderer::SwapBuffers() + ( Global.gfx_lights_clustered ? "clustered" : "first " + std::to_string( std::max( Global.DynamicLightCount, 0 ) ) ) + "\n" + m_locallights.info() + m_localshadows.info() - + m_gpuparticles.info(); + + m_gpuparticles.info() + + m_windscreenwater.info(); if( auto const cullstats = ( m_viewports.empty() || !( Global.gfx_lights_clustered || Global.gfx_lights_clusterheatmap ) ? std::nullopt : m_viewports.front()->clusters->stats() ) ) { statstext += "\ncluster lists: " + std::to_string( cullstats->index_count ) @@ -1599,6 +1601,16 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) float percipitation_intensity = glm::saturate(Global.Overcast - 1.); scene_ubs.rain_params.x = percipitation_intensity; // % amount of droplets scene_ubs.rain_params.y = glm::mix(15., 1., percipitation_intensity); // Regeneration time + scene_ubs.rain_params.z = (Global.Weather == "snow:" ? 1.f : 0.f); // flakes on the glass in place of the drops + scene_ubs.rain_params.w = Global.AirTemperature; // how fast the flakes melt + { + // the air the drops on the glass are blown with: the wind as the particles drift with it, less the velocity of the + // vehicle (V is along the front of the vehicle) + glm::dvec3 air{glm::dvec3{simulation::Environment.wind()} * 0.5}; + if (TDynamicObject const *owner = Global.pCamera.m_owner) + air -= owner->VectorFront() * owner->MoverParameters->V; + scene_ubs.rain_wind = glm::vec4{glm::vec3{air}, 0.f}; + } if (TDynamicObject const *owner = Global.pCamera.m_owner; owner && !!owner->MoverParameters->CabActive) { for (int i = 0; i < 4; ++i) @@ -1643,6 +1655,10 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) scene_ubs.inv_view = glm::inverse( glm::mat4{ glm::mat3{ m_colorpass.pass_camera.modelview() } } ); scene_ubo->update(scene_ubs); scene_ubo->bind_uniform(); + // the water on the windscreens of the cab: a step of its simulation once a frame, its storage for the glasses of every view + if (vp.main) + m_windscreenwater.update(Timer::GetDeltaTime()); + m_windscreenwater.bind(); if (Global.bWireFrame) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index ae19cf2f..47ddd242 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/opengl46/opengl46light.h" #include "rendering/opengl46/opengl46particles.h" #include "rendering/opengl46/opengl46gpuparticles.h" +#include "rendering/opengl46/opengl46water.h" #include "rendering/opengl46/opengl46skydome.h" #include "rendering/opengl46/opengl46precipitation.h" #include "simulation/simulationenvironment.h" @@ -533,6 +534,7 @@ class opengl46_renderer : public gfx_renderer, public gfx::scene_walk { opengl46_precipitation m_precipitationrenderer; opengl46_particles m_particlerenderer; // particle visualization subsystem opengl46_gpu_particles m_gpuparticles; // the smoke, rain and snow of the gpu (gfx.smoke.gpu, gfx.precipitation.gpu) + opengl46_windscreen_water m_windscreenwater; // the water on the windscreens of the cab of the train bool m_particlesoft{false}; // the depth for the soft particles is bound in the colour pass being drawn float m_framerate; diff --git a/rendering/opengl46/opengl46water.cpp b/rendering/opengl46/opengl46water.cpp new file mode 100644 index 00000000..805ae1bd --- /dev/null +++ b/rendering/opengl46/opengl46water.cpp @@ -0,0 +1,116 @@ +/* +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/opengl46water.h" + +#include "rendering/renderer.h" +#include "utilities/Logs.h" + +namespace +{ + +GLuint groups(std::size_t const Count) +{ + return static_cast((Count + gl::WATER_GROUP_SIZE - 1) / gl::WATER_GROUP_SIZE); +} + +} // namespace + +bool opengl46_windscreen_water::gpu_init() +{ + try + { + std::array const names{"water_trail.comp", "water_spawn.comp", "water_move.comp", "water_clear.comp", "water_grid.comp", + "water_merge.comp", "water_gather.comp", "water_settle.comp", "water_bake.comp"}; + for (std::size_t i = 0; i < names.size(); ++i) + { + gl::shader shader(names[i]); + m_programs[i] = std::make_unique(std::vector>{shader}); + } + } + catch (gl::shader_exception const &e) + { + ErrorLog("windscreen water: can't use the programs of the simulation: " + std::string(e.what())); + return false; + } + m_storage = std::make_unique(static_cast(gl::WATER_STORAGE_SIZE), gl::WATER_SSBO_BINDING); + ::glClearNamedBufferData(*m_storage, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, nullptr); + m_block = std::make_unique(sizeof(gl::water_ubs), gl::WATER_UBO_BINDING); + return true; +} + +void opengl46_windscreen_water::bind() const +{ + if (m_storage) + m_storage->bind_base(); +} + +void opengl46_windscreen_water::gpu_setup(std::size_t const Slot, std::vector const &Axes) +{ + if (!m_storage) + return; + // the frames before may still draw with the storage + ::glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT); + // the drops, the grid, the paths, the header and the counts of the slot cleared, the axes of the map put in + for (auto const &part : gl::water_slot_parts(Slot)) + ::glClearNamedBufferSubData(*m_storage, GL_R32UI, static_cast(part.first), static_cast(part.second), GL_RED_INTEGER, GL_UNSIGNED_INT, nullptr); + auto const axes{gl::water_slot_parts(Slot)[1]}; + m_storage->update(Axes.data(), static_cast(axes.first), static_cast(std::min(axes.second, Axes.size() * sizeof(glm::vec4)))); + ::glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT); +} + +bool opengl46_windscreen_water::gpu_bake(std::size_t const Slot, texture_handle const Mask, gl::water_ubs const &Block) +{ + if ((!m_storage) || (Mask == null_handle)) + return false; + auto &texture{GfxRenderer->Texture(Mask)}; + if ((false == texture.create()) || (false == texture.get_is_ready()) || (texture.get_id() == 0)) + return false; + m_storage->bind_base(); + m_block->update(Block); + m_block->bind_uniform(); + ::glBindTextureUnit(static_cast(gl::WATER_MASK_TEX), static_cast(texture.get_id())); + m_programs[pass::bake]->dispatch(groups(static_cast(gl::WATER_MAP) * gl::WATER_MAP)); + ::glBindTextureUnit(static_cast(gl::WATER_MASK_TEX), 0); + ::glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + gl::program::unbind(); + return true; +} + +void opengl46_windscreen_water::gpu_step(gl::water_ubs const &Block, std::uint32_t const Tries) +{ + if (!m_storage) + return; + m_storage->bind_base(); + m_block->update(Block); + m_block->bind_uniform(); + auto const slots{static_cast(gl::WATER_SLOTS)}; + auto const drops{groups(gl::WATER_DROPS)}; + auto const cells{groups(static_cast(gl::WATER_GRID) * gl::WATER_GRID)}; + auto const run = [&](pass const Pass, GLuint const Groups) { + if (Groups == 0) + return; + m_programs[Pass]->dispatch(Groups, slots); + ::glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + }; + // the wetted paths the blades went over in the step wiped + if (Block.blades != Block.blades_before) + run(pass::trail, groups(static_cast(gl::WATER_TRAIL) * gl::WATER_TRAIL)); + run(pass::spawn, groups(Tries)); + run(pass::move, drops); + run(pass::clear, cells); + run(pass::grid, drops); + run(pass::merge, drops); + run(pass::gather, drops); + run(pass::settle, drops); + run(pass::clear, cells); + run(pass::grid, drops); + gl::program::unbind(); +} diff --git a/rendering/opengl46/opengl46water.h b/rendering/opengl46/opengl46water.h new file mode 100644 index 00000000..851464e8 --- /dev/null +++ b/rendering/opengl46/opengl46water.h @@ -0,0 +1,55 @@ +/* +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 "rendering/common/windscreenwater.h" +#include "gl/shader.h" +#include "gl/ssbo.h" +#include "gl/ubo.h" + +// the water on the windscreens for the opengl 4.6 renderer (gfx::windscreen_water): the storage of the water, the block of the +// step and the compute programs +class opengl46_windscreen_water : public gfx::windscreen_water +{ + public: + opengl46_windscreen_water() = default; + ~opengl46_windscreen_water() override = default; + + // the storage bound to its point for the glasses drawn next (the bindings are kept by every context of its own) + void bind() const; + + protected: + bool gpu_init() override; + void gpu_setup(std::size_t Slot, std::vector const &Axes) override; + bool gpu_bake(std::size_t Slot, texture_handle Mask, gl::water_ubs const &Block) override; + void gpu_step(gl::water_ubs const &Block, std::uint32_t Tries) override; + + private: + // the passes of the step, in their order + enum pass + { + trail, + spawn, + move, + clear, + grid, + merge, + gather, + settle, + bake, + count + }; + std::array, pass::count> m_programs; + std::unique_ptr m_storage; + std::unique_ptr m_block; +}; diff --git a/rendering/vulkan14/vulkan14bindings.h b/rendering/vulkan14/vulkan14bindings.h index f58bccf7..b32c178e 100644 --- a/rendering/vulkan14/vulkan14bindings.h +++ b/rendering/vulkan14/vulkan14bindings.h @@ -71,7 +71,9 @@ constexpr std::uint32_t HEADLIGHT_SHADOW_TEX = 23; constexpr std::uint32_t SCENE_DEPTH_TEX = 24; // the image of the last frame of the main view with its mipmaps, which the screen space reflections take their colour from constexpr std::uint32_t SSR_HISTORY_TEX = 25; -constexpr std::uint32_t PASS_BINDING_COUNT = 26; +// the water on the windscreens (gfx::windscreen_water): the drops, their grids and the wetted paths the glasses read +constexpr std::uint32_t WATER_SSBO = 26; +constexpr std::uint32_t PASS_BINDING_COUNT = 27; // 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 559287e5..79603dda 100644 --- a/rendering/vulkan14/vulkan14context.h +++ b/rendering/vulkan14/vulkan14context.h @@ -31,6 +31,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/vulkan14/vulkan14terrain.h" #include "rendering/vulkan14/vulkan14grass.h" #include "rendering/vulkan14/vulkan14particles.h" +#include "rendering/vulkan14/vulkan14water.h" #include "rendering/vulkan14/vulkan14commands.h" #include "rendering/vulkan14/vulkan14raytracing.h" #include "rendering/vulkan14/vulkan14dlss.h" @@ -397,6 +398,8 @@ struct scene_state bool grass_tried{false}; // the smoke and the rain or snow of the gpu gpu_particles particles; + // the water on the windscreens of the cab of the train; its storage is held by the pass sets (WATER_SSBO) + windscreen_water water; // the ray traced reflections (gfx.raytracing), on a device with ray queries; its set is set RT_SET of the pipelines of the scene std::unique_ptr raytracing; // the heatmap of the light clusters (gfx.lights.clusterheatmap), made when it's first drawn diff --git a/rendering/vulkan14/vulkan14debug.cpp b/rendering/vulkan14/vulkan14debug.cpp index 96958560..4936fc77 100644 --- a/rendering/vulkan14/vulkan14debug.cpp +++ b/rendering/vulkan14/vulkan14debug.cpp @@ -168,7 +168,7 @@ std::string vulkan14_renderer::scene_info() 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), " + (Global.gfx_lights_clustered ? "clustered" : "first " + std::to_string(std::max(Global.DynamicLightCount, 0))) + "\n" + m_locallights.info() + scene.lamps.info() + - scene.particles.info(); + scene.particles.info() + scene.water.info(); auto const &stats{scene.clusters.stats}; if (stats.valid && (Global.gfx_lights_clustered || Global.gfx_lights_clusterheatmap)) { diff --git a/rendering/vulkan14/vulkan14frame.cpp b/rendering/vulkan14/vulkan14frame.cpp index 88d34d3b..96bf1418 100644 --- a/rendering/vulkan14/vulkan14frame.cpp +++ b/rendering/vulkan14/vulkan14frame.cpp @@ -990,6 +990,8 @@ void vulkan14_renderer::render_scene() { auto &ctx{*m_context}; auto &scene{ctx.scene}; + // the water on the windscreens of the cab: a step of its simulation once a frame, before the passes which draw the glasses + scene.water.update(Timer::GetDeltaTime()); if (scene.targets.size() < m_viewports.size()) { scene.targets.resize(m_viewports.size()); @@ -1406,6 +1408,16 @@ void vulkan14_renderer::render_viewport(std::size_t const Index) float percipitation_intensity = glm::saturate(Global.Overcast - 1.); scene_ubs.rain_params.x = percipitation_intensity; scene_ubs.rain_params.y = glm::mix(15., 1., percipitation_intensity); + scene_ubs.rain_params.z = (Global.Weather == "snow:" ? 1.f : 0.f); // flakes on the glass in place of the drops + scene_ubs.rain_params.w = Global.AirTemperature; // how fast the flakes melt + { + // the air the drops on the glass are blown with: the wind as the particles drift with it, less the velocity of the + // vehicle (V is along the front of the vehicle) + glm::dvec3 air{glm::dvec3{simulation::Environment.wind()} * 0.5}; + if (TDynamicObject const *owner = Global.pCamera.m_owner) + air -= owner->VectorFront() * owner->MoverParameters->V; + scene_ubs.rain_wind = glm::vec4{glm::vec3{air}, 0.f}; + } if (TDynamicObject const *owner = Global.pCamera.m_owner; owner && !!owner->MoverParameters->CabActive) { for (int i = 0; i < 4; ++i) diff --git a/rendering/vulkan14/vulkan14scene.cpp b/rendering/vulkan14/vulkan14scene.cpp index ba9f0cba..8193d5ae 100644 --- a/rendering/vulkan14/vulkan14scene.cpp +++ b/rendering/vulkan14/vulkan14scene.cpp @@ -150,6 +150,7 @@ VkDescriptorType pass_binding_type(std::uint32_t const Binding) case CLUSTER_RANGE_SSBO: case CLUSTER_INDEX_SSBO: case SKY_SSBO: + case WATER_SSBO: return VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; case ATMOSPHERE_UBO: case WEATHER_HORIZON_UBO: @@ -708,6 +709,40 @@ bool vulkan14_renderer::init_scene(std::string &Reason) scene.particles.init(device, ctx.pipelines, ctx.shaders, scene.pass_layout, ctx.memory, ctx.uploads, ctx.deletions, ctx.ring, ctx.device.caps().limits.minUniformBufferOffsetAlignment, frames, std::move(host)); } + { + vk14::windscreen_water::host host; + host.commands = [this]() { + suspend_rendering(); + return m_context->commands; + }; + host.texture = [this](texture_handle const Texture, VkImageView &View, VkSampler &Sampler) { + auto &ctx{*m_context}; + if ((Texture == null_handle) || (false == bool(ctx.textures))) + { + return false; + } + auto &source{ctx.textures->mark_as_used(Texture)}; + if (false == source.create()) + { + return false; + } + auto const *entry{ctx.images.find(source.get_id())}; + if (entry == nullptr) + { + return false; + } + View = entry->target->view(); + Sampler = entry->sampler; + return true; + }; + if (false == scene.water.init(device, ctx.pipelines, ctx.shaders, scene.pass_layout, ctx.memory, ctx.uploads, ctx.deletions, ctx.ring, ctx.device.caps().limits.minUniformBufferOffsetAlignment, + std::move(host))) + { + Reason = "no storage of the water on the windscreens"; + return false; + } + scene.water.set_host({[this](material_handle const Material) { return &m_materials.material(Material); }}); + } scene.terrain->init(ctx.resources, [this](vk14::command_stream *const Commands, VkImageView const Target, VkFormat const Format, VkExtent2D const Extent, VkImageView const Source, VkSampler const Sampler) { auto &scene{m_context->scene}; auto const pipeline{post_pipeline("copy.frag", static_cast(Format))}; @@ -779,6 +814,7 @@ void vulkan14_renderer::destroy_scene() scene.terrain.reset(); scene.grass.destroy(); scene.particles.destroy(); + scene.water.destroy(); scene.grass_program.reset(); scene.grass_tried = false; scene.cluster_heatmap.reset(); @@ -931,6 +967,7 @@ void vulkan14_renderer::write_pass_sets(vk14::instance_cull *Cull) VkDescriptorBufferInfo const atmosphereblock{scene.sky.atmosphere.handle(), 0, sizeof(gl::atmosphere_ubs)}; VkDescriptorBufferInfo const weatherblock{ctx.ring.handle(), 0, offsetof(gl::weather_ubs, fog_horizon)}; VkDescriptorBufferInfo const horizonblock{scene.weather_horizon.handle(), 0, sizeof(glm::vec4) * gl::FOG_HORIZON_SAMPLES}; + VkDescriptorBufferInfo const water{scene.water.storage(), 0, VK_WHOLE_SIZE}; VkDescriptorImageInfo const headlightsinfo{scene.headlights_sampler, scene.headlights_view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; std::vector writes; std::vector> images; @@ -964,6 +1001,7 @@ void vulkan14_renderer::write_pass_sets(vk14::instance_cull *Cull) write(handle, ATMOSPHERE_UBO, &atmosphereblock, nullptr); write(handle, WEATHER_UBO, &weatherblock, nullptr); write(handle, WEATHER_HORIZON_UBO, &horizonblock, nullptr); + write(handle, WATER_SSBO, &water, nullptr); write(handle, HEADLIGHT_TEX, nullptr, &headlightsinfo); write(handle, SHADOW_TEX, nullptr, image(ctx.sampling.shadow, views[SHADOW_TEX])); write(handle, ENV_TEX, nullptr, image(ctx.sampling.clamp_linear, views[ENV_TEX])); diff --git a/rendering/vulkan14/vulkan14shaders.cpp b/rendering/vulkan14/vulkan14shaders.cpp index c39884b9..eb24cadf 100644 --- a/rendering/vulkan14/vulkan14shaders.cpp +++ b/rendering/vulkan14/vulkan14shaders.cpp @@ -802,6 +802,7 @@ std::string binding_preamble(bool const Raytracing) define("HEADLIGHT_SHADOW_TEX", HEADLIGHT_SHADOW_TEX); define("SCENE_DEPTH_TEX", SCENE_DEPTH_TEX); define("SSR_HISTORY_TEX", SSR_HISTORY_TEX); + define("WATER_SSBO", WATER_SSBO); define("PASS_BINDING_COUNT", PASS_BINDING_COUNT); define("MATERIAL_TEXTURE_FIRST", MATERIAL_TEXTURE_FIRST); define("CL_CLUSTER_UBO", CL_CLUSTER_UBO); @@ -865,6 +866,13 @@ std::string binding_preamble(bool const Raytracing) define("SHADOW_FIT_GROUP_SIZE", gl::SHADOW_FIT_GROUP_SIZE); preamble += "#define LOCAL_SHADOW_NEAR " + std::to_string(gfx::local_shadows::shadow_near) + "\n"; define("PARTICLE_GROUP_SIZE", gl::PARTICLE_GROUP_SIZE); + define("WATER_SLOTS", gl::WATER_SLOTS); + define("WATER_DROPS", gl::WATER_DROPS); + define("WATER_GRID", gl::WATER_GRID); + define("WATER_CELL_DROPS", gl::WATER_CELL_DROPS); + define("WATER_TRAIL", gl::WATER_TRAIL); + define("WATER_MAP", gl::WATER_MAP); + define("WATER_GROUP_SIZE", gl::WATER_GROUP_SIZE); return preamble; } diff --git a/rendering/vulkan14/vulkan14water.cpp b/rendering/vulkan14/vulkan14water.cpp new file mode 100644 index 00000000..4d323b33 --- /dev/null +++ b/rendering/vulkan14/vulkan14water.cpp @@ -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/. +*/ + +#include "stdafx.h" +#include "rendering/vulkan14/vulkan14water.h" +#include "rendering/vulkan14/vulkan14commands.h" +#include "utilities/Logs.h" + +#include + +namespace vk14 +{ + +namespace +{ + +std::uint32_t groups(std::size_t const Count) +{ + return static_cast((Count + gl::WATER_GROUP_SIZE - 1) / gl::WATER_GROUP_SIZE); +} + +VkBufferUsageFlags constexpr storage_usage{VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT}; + +} // namespace + +windscreen_water::~windscreen_water() +{ + // the materials may be gone already + set_host({}); + destroy(); +} + +bool windscreen_water::init(VkDevice const Device, pipeline_library &Pipelines, shader_library &Shaders, VkDescriptorSetLayout const Passlayout, allocator &Memory, uploader &Uploads, + deletion_queue &Deletions, frame_ring &Ring, VkDeviceSize const Alignment, host Host) +{ + m_device = Device; + m_pipelines = &Pipelines; + m_shaders = &Shaders; + m_passlayout = Passlayout; + m_memory = &Memory; + m_uploads = &Uploads; + m_deletions = &Deletions; + m_ring = &Ring; + m_alignment = std::max(Alignment, 16); + m_host = std::move(Host); + m_storage = std::make_shared(); + m_full = m_storage->create(Memory, static_cast(gl::WATER_STORAGE_SIZE), storage_usage, memory_kind::device, "windscreen water"); + if (false == m_full) + { + // the pass sets need a buffer to hold: a small one, which no glass reads (none gets a slot) + ErrorLog("windscreen water: no storage of " + std::to_string(gl::WATER_STORAGE_SIZE / (1024 * 1024)) + " MiB, the windscreens make their water up"); + m_storage = std::make_shared(); + if (false == m_storage->create(Memory, 65536, storage_usage, memory_kind::device, "windscreen water")) + { + m_storage.reset(); + return false; + } + } + return true; +} + +void windscreen_water::destroy() +{ + if (m_device == VK_NULL_HANDLE) + { + return; + } + for (auto &kernel : m_kernels) + { + kernel.destroy(m_device); + } + m_storage.reset(); + m_device = VK_NULL_HANDLE; + m_cleared = false; + reset(); +} + +bool windscreen_water::gpu_init() +{ + if ((m_device == VK_NULL_HANDLE) || (false == m_full) || (false == bool(m_host.commands)) || (false == bool(m_host.texture))) + { + return false; + } + try + { + std::array const names{"water_trail.comp", "water_spawn.comp", "water_move.comp", "water_clear.comp", "water_grid.comp", + "water_merge.comp", "water_gather.comp", "water_settle.comp", "water_bake.comp"}; + for (std::size_t i = 0; i < names.size(); ++i) + { + if (false == m_kernels[i].create(m_device, *m_pipelines, m_shaders->get(names[i]), m_passlayout, {}, names[i])) + { + throw gfx::shader_exception(std::string("no compute pipeline of ") + names[i]); + } + } + } + catch (gfx::shader_exception const &e) + { + ErrorLog("windscreen water: can't use the programs of the simulation: " + std::string(e.what())); + return false; + } + return true; +} + +void windscreen_water::begin(command_stream *const Commands) +{ + // the glasses of the frames before read the storage, the fills and the copies write it + memory_barrier(Commands, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT, + VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_CLEAR_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT); + if (false == m_cleared) + { + // nothing in the storage yet + m_cleared = true; + Commands->fill_buffer(m_storage->handle(), 0, VK_WHOLE_SIZE, 0); + compute_barrier(Commands); + } +} + +void windscreen_water::gpu_setup(std::size_t const Slot, std::vector const &Axes) +{ + auto const commands{m_host.commands()}; + if ((commands == nullptr) || (false == m_full)) + { + return; + } + begin(commands); + // the drops, the grid, the paths, the header and the counts of the slot cleared with the commands of the frame + auto const parts{gl::water_slot_parts(Slot)}; + for (std::size_t part = 0; part < parts.size(); ++part) + { + if (part != 1) + { + commands->fill_buffer(m_storage->handle(), static_cast(parts[part].first), static_cast(parts[part].second), 0); + } + } + // the axes of the map copied after that, from a staging buffer kept until the gpu is done with the frame + auto const size{std::min(static_cast(parts[1].second), static_cast(Axes.size() * sizeof(glm::vec4)))}; + auto staging{std::make_shared()}; + if ((false == staging->create(*m_memory, size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, memory_kind::upload, "windscreen water map")) || (staging->mapped() == nullptr)) + { + ErrorLog("windscreen water: no staging buffer for the map of a glass"); + compute_barrier(commands); + return; + } + std::memcpy(staging->mapped(), Axes.data(), static_cast(size)); + staging->flush(0, size); + VkBufferCopy2 region{}; + region.sType = VK_STRUCTURE_TYPE_BUFFER_COPY_2; + region.dstOffset = static_cast(parts[1].first); + region.size = size; + VkCopyBufferInfo2 copy{}; + copy.sType = VK_STRUCTURE_TYPE_COPY_BUFFER_INFO_2; + copy.srcBuffer = staging->handle(); + copy.dstBuffer = m_storage->handle(); + copy.regionCount = 1; + copy.pRegions = ®ion; + commands->copy_buffer2(©); + compute_barrier(commands); + m_deletions->push(m_uploads->current_value(), [staging]() {}); +} + +ring_slice windscreen_water::block(gl::water_ubs const &Block) +{ + auto const slice{m_ring->allocate(sizeof(gl::water_ubs), m_alignment)}; + if (slice) + { + std::memcpy(slice.data, &Block, sizeof(gl::water_ubs)); + } + return slice; +} + +bool windscreen_water::gpu_bake(std::size_t const Slot, texture_handle const Mask, gl::water_ubs const &Block) +{ + auto const commands{m_host.commands()}; + if ((commands == nullptr) || (false == m_full) || (Mask == null_handle)) + { + return false; + } + VkImageView view{VK_NULL_HANDLE}; + VkSampler sampler{VK_NULL_HANDLE}; + if ((false == m_host.texture(Mask, view, sampler)) || (view == VK_NULL_HANDLE)) + { + return false; + } + auto const slice{block(Block)}; + if (false == bool(slice)) + { + return false; + } + begin(commands); + m_kernels[pass::bake].run(commands, + {buffer_descriptor(m_storage->handle(), 0, VK_WHOLE_SIZE), buffer_descriptor(slice.buffer, slice.offset, sizeof(gl::water_ubs)), + image_descriptor(sampler, view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)}, + groups(static_cast(gl::WATER_MAP) * gl::WATER_MAP)); + compute_barrier(commands); + return true; +} + +void windscreen_water::gpu_step(gl::water_ubs const &Block, std::uint32_t const Tries) +{ + auto const commands{m_host.commands()}; + if ((commands == nullptr) || (false == m_full)) + { + return; + } + auto const slice{block(Block)}; + if (false == bool(slice)) + { + return; + } + begin(commands); + std::vector const descriptors{buffer_descriptor(m_storage->handle(), 0, VK_WHOLE_SIZE), buffer_descriptor(slice.buffer, slice.offset, sizeof(gl::water_ubs))}; + auto const run = [&](pass const Pass, std::uint32_t const Groups) { + if (Groups == 0) + { + return; + } + m_kernels[Pass].run(commands, descriptors, Groups, gl::WATER_SLOTS); + compute_barrier(commands); + }; + auto const drops{groups(gl::WATER_DROPS)}; + auto const cells{groups(static_cast(gl::WATER_GRID) * gl::WATER_GRID)}; + // the wetted paths the blades went over in the step wiped + if (Block.blades != Block.blades_before) + { + run(pass::trail, groups(static_cast(gl::WATER_TRAIL) * gl::WATER_TRAIL)); + } + run(pass::spawn, groups(Tries)); + run(pass::move, drops); + run(pass::clear, cells); + run(pass::grid, drops); + run(pass::merge, drops); + run(pass::gather, drops); + run(pass::settle, drops); + run(pass::clear, cells); + run(pass::grid, drops); +} + +} // namespace vk14 diff --git a/rendering/vulkan14/vulkan14water.h b/rendering/vulkan14/vulkan14water.h new file mode 100644 index 00000000..2b24bbf9 --- /dev/null +++ b/rendering/vulkan14/vulkan14water.h @@ -0,0 +1,96 @@ +/* +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 water on the windscreens in the vulkan renderer (gfx::windscreen_water, opengl46_windscreen_water): the storage of the water, +// which the pass sets hold for the glasses (WATER_SSBO), and the compute kernels of the simulation, run with the commands of the +// frame before its passes (outside a rendering). the kernels push everything they read (set COMPUTE_SET: the storage, the block of +// the step in the ring of the frame, the mask of the wipers being baked) + +#pragma once + +#include "rendering/common/windscreenwater.h" +#include "rendering/vulkan14/vulkan14compute.h" +#include "rendering/vulkan14/vulkan14texture.h" + +#include +#include +#include + +namespace vk14 +{ + +class windscreen_water : public gfx::windscreen_water +{ + public: + // what the renderer does for the water + struct host + { + // the commands of the frame, outside a rendering + std::function commands; + // the view and the sampler of a texture of the renderer, made when needed; false when it isn't ready + std::function texture; + }; + + windscreen_water() = default; + ~windscreen_water() override; + + // called once, before the pass sets are made: the storage made (the pass sets hold it from then on). false: no storage + bool init(VkDevice Device, pipeline_library &Pipelines, shader_library &Shaders, VkDescriptorSetLayout Passlayout, allocator &Memory, uploader &Uploads, deletion_queue &Deletions, frame_ring &Ring, + VkDeviceSize Alignment, host Host); + // everything goes (the device is idle) + void destroy(); + // the storage the pass sets hold + VkBuffer storage() const + { + return m_storage ? m_storage->handle() : VK_NULL_HANDLE; + } + + protected: + bool gpu_init() override; + void gpu_setup(std::size_t Slot, std::vector const &Axes) override; + bool gpu_bake(std::size_t Slot, texture_handle Mask, gl::water_ubs const &Block) override; + void gpu_step(gl::water_ubs const &Block, std::uint32_t Tries) override; + + private: + // the passes of the step, in their order + enum pass + { + trail, + spawn, + move, + clear, + grid, + merge, + gather, + settle, + bake, + count + }; + // the block of the step in the ring of the frame; empty when the ring is full + ring_slice block(gl::water_ubs const &Block); + // the frames before are done with the storage before the commands of this one write it + void begin(command_stream *Commands); + + VkDevice m_device{VK_NULL_HANDLE}; + pipeline_library *m_pipelines{nullptr}; + shader_library *m_shaders{nullptr}; + VkDescriptorSetLayout m_passlayout{VK_NULL_HANDLE}; + allocator *m_memory{nullptr}; + uploader *m_uploads{nullptr}; + deletion_queue *m_deletions{nullptr}; + frame_ring *m_ring{nullptr}; + VkDeviceSize m_alignment{256}; + host m_host; + std::array m_kernels; + std::shared_ptr m_storage; + bool m_full{false}; // the storage is as large as the simulation needs (else a stand-in, the glasses make their water up) + bool m_cleared{false}; +}; + +} // namespace vk14 diff --git a/shaders/ogl46/common.glsl b/shaders/ogl46/common.glsl index 3dad7f79..b30a0873 100644 --- a/shaders/ogl46/common.glsl +++ b/shaders/ogl46/common.glsl @@ -49,6 +49,16 @@ #define PARTICLE_OWNER_SSBO_BINDING 11 #define PARTICLE_OUTPUT_SSBO_BINDING 12 #define PARTICLE_COMMAND_SSBO_BINDING 13 +#define WATER_SLOTS 4 +#define WATER_DROPS 65536 +#define WATER_GRID 128 +#define WATER_CELL_DROPS 32 +#define WATER_TRAIL 1024 +#define WATER_MAP 256 +#define WATER_GROUP_SIZE 64 +#define WATER_SSBO_BINDING 16 +#define WATER_UBO_BINDING 6 +#define WATER_MASK_TEX 25 #define INSTANCE_COUNTER_HEADER 8 #define INSTANCE_JOB_SLEEPERS 1U #define LIGHT_SSBO_BINDING 0 @@ -297,6 +307,8 @@ layout(std140, binding = SCENE_UBO_BINDING) uniform scene_ubo vec4 ssr_params; vec4 ssr_params2; vec4 env_params; + // the air against the vehicle of the camera, see gl::scene_ubs + vec4 rain_wind; }; #ifdef STAGE_VERTEX diff --git a/shaders/ogl46/mat_rain_windscreen.frag b/shaders/ogl46/mat_rain_windscreen.frag index 5babaab4..91ed0ad9 100644 --- a/shaders/ogl46/mat_rain_windscreen.frag +++ b/shaders/ogl46/mat_rain_windscreen.frag @@ -1,7 +1,29 @@ #version 460 core #define STAGE_FRAGMENT 1 -// Windshield rain effects -// by @lcddisplay; wiper movement @MichauSto +// Windshield rain and snow +// by @lcddisplay; wiper movement @MichauSto; water on the glass rewritten as a model of drops lying on it +// +// the material: the textures diffuse (the glass) and wipermask (a blade per channel, r g b a: 0 where it doesn't reach, elsewhere +// the place of the blade over its sweep, 0 at rest .. 1 at the far end, at which it goes over the place). raindropsatlas and +// raindrop_grid_size aren't used any more, they stay so the materials which name them still load. rain_scale (1 when not given) +// scales the drops and the flakes. water_slot is given by the renderer (1 + the glass of the simulation, 0 none), not by the +// material. the block of the scene: rain_params (x: how heavy the rain or snow is, z: 1 for snow, w: temperature of the air), +// wiper_pos, wiper_timer_out, wiper_timer_return (the blades of the vehicle of the camera) and rain_wind (the air against it). +// +// the glasses of the cab of the train are simulated (gfx::windscreen_water, water_*.comp, water.glsl): the raindrops land as often +// as the rain hits the glass, splashing; the drops lie held by surface tension until gravity and the drag of the air pull them +// harder, then slide in stops and goes, wetting their paths (the next ones follow them into rivulets) and leaving droplets behind; +// the drops which touch flow together; the blades take the water along, gather it into a bead and leave it at the ends of their +// sweep, where it runs down; the flakes stick and melt. here the drops of the simulation are drawn where the fragment is, and the +// wetted paths as a thin film. elsewhere (the windows of other vehicles, a glass the simulation has no room for) the water is made +// up from the time alone: drops landing, growing and sliding along tracks, the blades pushing a ridge and leaving the water at the +// edge of their sweep. +// +// the water is a field of soft kernels (metaballs): drops coming close flow together, and a drop is a few kernels, so the drops +// are of many shapes. a drop is a lens holding a sharp picture of the place around it turned over (the image of the last frame +// where the screen holds it, the sky and the ground elsewhere), its edge shows the light of the sky reflected inside it; the lamps +// and the headlights light the water up; the film the blades leave breaks up behind them and the lights seen through it are drawn +// into lines towards their pivot; the wet glass is a little milky layout(location = 0) in vec3 f_normal; layout(location = 2) in vec2 f_coord; @@ -21,191 +43,1170 @@ layout(location = 1) out vec4 out_motion; #pragma param(reflection, 1, 2, 1, zero) #pragma param(glossiness, 1, 3, 1, glossiness) #pragma param(raindrop_grid_size, 2, 0, 1, one) +#pragma param(rain_scale, 2, 1, 1, one) #include "light_common.glsl" #include "apply_fog.glsl" #include "tonemapping.glsl" -#include "random.glsl" +#include "water.glsl" layout(binding = 0) uniform sampler2D diffuse; layout(binding = 1) uniform sampler2D raindropsatlas; layout(binding = 2) uniform sampler2D wipermask; -const float specular_intensity = 1.0; -const float wobble_strength = 0.002; -const float wobble_speed = 30.0; +const float RW_PI = 3.14159265; +// refractive index of water +const float RW_IOR = 1.333; +// the falling particles (particles_precipitation.comp, rendering/common/gpuparticles.cpp): the diameters of the flakes, metres, +// and how fast the rain and the snow fall, m/s +const vec2 RW_SNOW_DIAMETER = vec2(0.004, 0.012); +const float RW_RAIN_FALL = 7.0; +const float RW_SNOW_FALL = 1.1; +// a drop is a lens: it holds a sharp picture of the place around it, turned over through its middle, RW_LENS times as far out +// as the fragment is from the middle (and as much smaller), with this share of the bending of the view by the surface on top +const float RW_LENS = 2.0; +const float RW_LENS_BEND = 0.15; +// the drag of the air going along the glass: the speed at which it equals the weight of a drop, m/s; and the share of the air +// meeting the glass head on which turns up along it. so the water goes up a glass the sooner the more it leans back: on a +// vertical glass at about 250 km/h, leaning 20 degrees at about 130 km/h, 45 degrees at about 80 km/h +const float RW_DRAG_SPEED = 24.0; +const float RW_HEAD_ON = 0.35; +// the water is a field summed from soft kernels (metaballs) and its surface is where the field is RW_SURFACE: drops which come +// close flow together the way surface tension pulls them. a kernel alone reaches the surface at RW_EDGE of its radius +const float RW_SURFACE = 0.25; +const float RW_EDGE = 0.61; -vec4 getDropTex(float choice, vec2 uv) { - vec2 offset; - if (choice < 0.25) offset = vec2(0.0, 0.0); - else if (choice < 0.5) offset = vec2(0.5, 0.0); - else if (choice < 0.75) offset = vec2(0.0, 0.5); - else offset = vec2(0.5, 0.5); - return texture(raindropsatlas, offset + uv * 0.5); +// the glass at the fragment: the view space steps along it for a step of the coordinates of the glass (metres along the axes +// of the model, see main()), and the size of a pixel on it, metres +vec3 rw_tu; +vec3 rw_tv; +float rw_pixel; + +// how the water on the glass behaves here, set by main() +float rw_scale; // size of the drops (param rain_scale) +float rw_since; // seconds since a blade last cleaned the place +float rw_period; // seconds between the small drops landing on a place of one +float rw_grow; // seconds the drops need after a wipe to grow big enough to slide +float rw_rate; // how fast the glass gathers the rain against a standing vertical glass in the heaviest rain +float rw_push; // the force on a drop along the glass, in units of its weight +vec2 rw_down; // the way gravity pulls along the glass (coordinates of the glass, unit) +vec2 rw_flow; // the way the air goes along the glass (unit, 0 in still air) + +// the field of the water: the sum of the kernels, its gradient along the glass (view space) and the sums for the mean radius of +// the drops it is made of +float rw_field = 0.0; +vec3 rw_fgrad = vec3(0.0); +float rw_fsize = 0.0; +float rw_fweight = 0.0; + +// the water at the fragment: the thickness of the highest water there (metres), its gradient (view space), the radius of the +// curvature of its surface (metres: how much it bends the view across a pixel) and how much of the pixel it covers +float rw_height = 0.0; +vec3 rw_slope = vec3(0.0); +float rw_curve = 1.0; +float rw_cover = 0.0; +// from the middle of the drop which gives the water to the fragment (view space, metres), 0 for spread water +vec3 rw_offset = vec3(0.0); +// the last time the water a blade pushes went over the fragment, taking the drops lying there with it +float rw_cleared = -1.0e9; + +uvec4 rw_pcg4d(uvec4 v) +{ + v = v * 1664525u + 1013904223u; + v.x += v.y * v.w; + v.y += v.z * v.x; + v.z += v.x * v.y; + v.w += v.y * v.z; + v ^= v >> 16u; + v.x += v.y * v.w; + v.y += v.z * v.x; + v.z += v.x * v.y; + v.w += v.y * v.z; + return v; } -float GetMixFactor(in vec2 co, out float side); - -void main() { - vec4 tex_color = texture(diffuse, f_coord); - if (tex_color.a < 0.01) discard; - - vec2 rainCoord = f_coord; - float gridSize = ceil(param[2].x); - - const float numDrops = 20000.0; - const float cycleDuration = 4.0; - - float squareMin = 0.5 / gridSize; - float squareMax = 1.2 / gridSize; - - vec2 cell = floor(rainCoord * gridSize); - - vec3 dropLayer = vec3(0.0); - float dropMaskSum = 0.0; - - // Grid of 9 droplets in immediate neighbourhood - for (int oy = -1; oy <= 1; oy++) { - for (int ox = -1; ox <= 1; ox++) { - - vec2 neighborCell = cell + vec2(ox, oy); - vec2 neighborCenter = (neighborCell + .5) / gridSize; - - float side; - float mixFactor = GetMixFactor(neighborCenter, side); - - uint seed = Hash(uvec3(neighborCell, side)); - - if(mixFactor < RandF(seed)) { - continue; - } - - // Show a percentage of droplets given by rain intensity param - float activationSeed = RandF(seed); - if (activationSeed > rain_params.x) - continue; // kropla nieaktywna - - // Randomly modulate droplet center & size - vec2 dropCenter = (neighborCell + vec2(RandF(seed), RandF(seed))) / gridSize; - float squareSize = mix(squareMin, squareMax, RandF(seed)); - - float lifeTime = time + RandF(seed) * cycleDuration; - float phase = fract(lifeTime / cycleDuration); - float actiwe = clamp(1.0 - phase, 0.0, 1.0); - - // Gravity influence (TODO add vehicle speed & wind here!) - float gravityStart = 0.5; - float gravityPhase = smoothstep(gravityStart, 1.0, phase); - float dropMass = mix(0.3, 1.2, RandF(seed)); - float gravitySpeed = 0.15 * dropMass; - vec2 gravityOffset = vec2(0.0, gravityPhase * gravitySpeed * phase); - - // Random wobble - bool hasWobble = (RandF(seed) < 0.10); - vec2 wobbleOffset = vec2(0.0); - if (hasWobble && gravityPhase > 0.0) { - float intensity = sin(time * wobble_speed + RandF(seed) * 100.) * wobble_strength * gravityPhase; - wobbleOffset = vec2(intensity, 0.0); - } - - vec2 slideOffset = gravityOffset + wobbleOffset; - - // Flatten droplets influenced by gravity - float flattenAmount = smoothstep(0.1, 0.5, gravityPhase); - float flattenX = mix(1.0, 0.4, flattenAmount); - float stretchY = mix(1.0, 1.6, flattenAmount); - - // Droplet local position & mask - vec2 diff = (rainCoord + slideOffset) - dropCenter; - diff.x *= 1.0 / flattenX; - diff.y *= 1.0 / stretchY; - float mask = smoothstep(squareSize * 0.5, squareSize * 0.45, max(abs(diff.x), abs(diff.y))); - - if (mask > 0.001) { - vec2 localUV = (diff + squareSize * 0.5) / squareSize; - float choice = RandF(seed); - vec4 dropTex = getDropTex(choice, localUV); - float sharpAlpha = smoothstep(0.3, 0.9, dropTex.a); - - float ambLum = clamp(dot(ambient, vec3(0.2126, 0.7152, 0.0722)), 0.0, 1.0); - float sunFactor = pow(clamp(ambLum * 6.0, 0.0, 1.0), 0.5); - float dynBright = mix(0.0, 1.0, sunFactor); - - float colorLuma = length(dropTex.rgb); - float alphaRange = smoothstep(0.1, 0.3, colorLuma); - float blackAlpha = mix(0.25, 0.85, alphaRange); - - float sparkle = 0.0; - if (hasWobble) { - sparkle = pow(abs(sin(f_coord.x * 8.0 + time * 2.0 + RandF(seed) * 6.2831)), 40.0) - * mix(0.2, 1.0, sunFactor); - } - - vec3 specularColor = vec3(1.0) * specular_intensity * sparkle; - vec3 dropLit = dropTex.rgb * dynBright + specularColor * sharpAlpha; - - dropLayer += dropLit * sharpAlpha * actiwe * blackAlpha * mask; - dropMaskSum += sharpAlpha * actiwe * blackAlpha * mask; - } - } - } - vec3 finalMix = dropLayer; - float alphaOut = clamp(dropMaskSum, 0.0, 1.0); - out_color = vec4(finalMix, alphaOut); - - { // Overlay windshield texture with alpha - vec3 fragcolor = ambient; - vec3 fragnormal = normalize(f_normal); - float reflectivity = param[1].z; - float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; - legacy_gloss(); - - fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); - vec4 color = vec4(fragcolor, tex_color.a * alpha_mult); - - out_color.xyz = apply_fog(mix(out_color.xyz, color.xyz, color.a)); - out_color.a = mix(out_color.a, 1., color.a); - } - - if (MOTIONBLUR_ENABLED) - { - out_motion = vec4(0.0); - } +// four numbers 0 .. 1 of a cell of a layer +vec4 rw_random(ivec2 cell, uint layer, uint index) +{ + return vec4(rw_pcg4d(uvec4(uvec2(cell), layer, index))) * (1.0 / 4294967296.0); } -float GetMixFactor(in vec2 co, out float side) { - vec4 movePhase = wiper_pos; - bvec4 is_out = lessThanEqual(movePhase, vec4(1.)); - movePhase = mix(vec4(2.) - movePhase, movePhase, is_out); - - vec4 mask = texture(wipermask, co); - - vec4 areaMask = step(.001, mask); - - vec4 maskVal = mix(1. - mask, mask, is_out); - vec4 wipeWidth = smoothstep(1., .9, movePhase) * .25; - vec4 cleaned = smoothstep(movePhase - wipeWidth, movePhase, maskVal) * areaMask; - vec4 side_v = step(maskVal, movePhase); - cleaned *= side_v; - side_v = mix(side_v, vec4(1.) - side_v, is_out); - - // "regeneration", raindrops gradually returning after wiper pass: - vec4 regenPhase = clamp((vec4(time) - mix(wiper_timer_out, wiper_timer_return, side_v) - vec4(.2)) / vec4(rain_params.y), vec4(0.), vec4(1.)); - - side_v = mix(vec4(0.), vec4(1.) - side_v, areaMask); - - vec4 factor_v = mix(vec4(1.), regenPhase * (vec4(1.) - cleaned), areaMask); - - side = 0.; - float out_factor = 1.; - - // Find out the wiper blade that influences given grid cell the most - for(int i = 0; i < 4; ++i) - { - bool is_candidate = factor_v[i] < out_factor; - out_factor = mix(out_factor, factor_v[i], is_candidate); - side = mix(side, side_v[i], is_candidate); - } - - return out_factor; +float rw_noise(float x, uint layer) +{ + float i = floor(x); + float f = x - i; + float a = rw_random(ivec2(int(i), 0), layer, 7u).x; + float b = rw_random(ivec2(int(i) + 1, 0), layer, 7u).x; + return mix(a, b, f * f * (3.0 - 2.0 * f)); +} + +// value noise 0 .. 1 (x) with its gradient (yz) +vec3 rw_noise2(vec2 p, uint layer) +{ + vec2 i = floor(p); + vec2 f = p - i; + vec2 u = f * f * (3.0 - 2.0 * f); + vec2 du = 6.0 * f * (1.0 - f); + ivec2 c = ivec2(i); + float a = rw_random(c, layer, 8u).x; + float b = rw_random(c + ivec2(1, 0), layer, 8u).x; + float d = rw_random(c + ivec2(0, 1), layer, 8u).x; + float e = rw_random(c + ivec2(1, 1), layer, 8u).x; + float k1 = b - a; + float k2 = d - a; + float k4 = a - b - d + e; + return vec3(a + k1 * u.x + k2 * u.y + k4 * u.x * u.y, du * vec2(k1 + k4 * u.y, k2 + k4 * u.x)); +} + +// P turned by A (radians). every set of drops is laid on its own grid, turned and shifted against the others, so the drops line +// up neither with each other nor along the glass +vec2 rw_turn(vec2 p, float a) +{ + float c = cos(a); + float s = sin(a); + return vec2(c * p.x - s * p.y, s * p.x + c * p.y); +} + +// a gradient given along the coordinates of the glass, view space +vec3 rw_along(vec2 gradient) +{ + return rw_tu * (gradient.x / dot(rw_tu, rw_tu)) + rw_tv * (gradient.y / dot(rw_tv, rw_tv)); +} + +// the gradient along the glass (view space) of a value of the fragments, from how it changes from pixel to pixel; Dpdx, Dpdy: +// the steps of the position, Normal: of the glass +vec3 rw_gradient(float value, vec3 dpdx, vec3 dpdy, vec3 normal) +{ + vec3 r1 = cross(dpdy, normal); + vec3 r2 = cross(normal, dpdx); + float det = dot(dpdx, r1); + float dx = dFdx(value); + float dy = dFdy(value); + return (abs(det) > 1e-14) ? (dx * r1 + dy * r2) / det : vec3(0.0); +} + +// a kernel of the water, Offset (view space) from its centre to the fragment: alone it makes water of the radius Visible, +// Weight 1 .. 0 as it goes. water smaller than a pixel would only flicker, it is left to the haze of the wet glass +void rw_kernel(vec3 offset, float visible, float weight) +{ + if ((weight <= 0.0) || (visible < 0.5 * rw_pixel)) + return; + // a drop a pixel or two across is grain rather than a lens: it fades into the haze of the wet glass + weight *= smoothstep(0.5 * rw_pixel, 2.0 * rw_pixel, visible); + float support = visible / RW_EDGE; + float q = dot(offset, offset) / (support * support); + if (q >= 1.0) + return; + float k = 1.0 - q; + float k3 = weight * k * k * k; + rw_field += k3; + rw_fgrad += offset * (-6.0 * weight * k * k / (support * support)); + rw_fsize += k3 * visible; + rw_fweight += k3; +} + +// a kernel at the Offset in the coordinates of the glass +void rw_blob(vec2 offset, float visible, float weight) +{ + rw_kernel(offset.x * rw_tu + offset.y * rw_tv, visible, weight); +} + +// a drop lying on the glass: a few kernels, sagging down the glass and lopsided as Shape (random 0 .. 1) says +void rw_drop(vec2 offset, float visible, float weight, vec4 shape) +{ + rw_blob(offset, visible, weight); + if (visible < 1.5 * rw_pixel) + return; + vec2 side = vec2(-rw_down.y, rw_down.x); + vec2 sag = (rw_down * (0.2 + 0.35 * shape.x) + side * ((shape.y - 0.5) * 0.7)) * visible; + rw_blob(offset - sag, visible * mix(0.6, 0.85, shape.z), weight); + if (shape.w > 0.4) + { + float angle = shape.w * 40.0; + rw_blob(offset - vec2(cos(angle), sin(angle)) * (0.6 * visible), visible * mix(0.4, 0.6, shape.x), weight); + } +} + +// water spread over the glass (a film, a ridge): its thickness, the gradient of it (view space), the radius of the curvature of +// its surface and how much of the pixel it covers +void rw_sheet(float thickness, vec3 slope, float curve, float cover) +{ + if (cover <= 0.0) + return; + rw_cover = max(rw_cover, cover); + if (thickness > rw_height) + { + rw_height = thickness; + rw_slope = slope; + rw_curve = curve; + rw_offset = vec3(0.0); + } +} + +// the surface of the water of the field: a drop is a cap of a sphere, flat lens of the contact angle Contact (radians) the water +// makes with the glass. how far across a drop the fragment is follows from the field (inverting the kernel), the height and +// the slope of the cap from that: the slope rises slowly from the middle and steeply only close to the edge, where it reaches +// the contact angle; where drops flowed together the field is above that of a drop alone and the water is flat. the edge is +// antialiased by how fast the field changes across a pixel +void rw_settle(float contact) +{ + if (rw_field <= 0.5 * RW_SURFACE) + return; + float gradient = length(rw_fgrad); + float cover = clamp((rw_field - RW_SURFACE) / max(gradient * rw_pixel, 1e-3) + 0.5, 0.0, 1.0); + if (cover <= 0.0) + return; + float mean = rw_fsize / max(rw_fweight, 1e-12); + // 0 in the middle of a drop, 1 at its edge + float across = min(sqrt(max(1.0 - pow(min(rw_field, 1.0), 1.0 / 3.0), 0.0)) / RW_EDGE, 1.0); + float s = sin(contact); + float rs = across * s; + float root = sqrt(max(1.0 - rs * rs, 1e-4)); + float sphere = mean / s; + vec3 slope = (gradient > 1e-9) ? rw_fgrad * ((rs / root) / gradient) : vec3(0.0); + float height = max(sphere * (root - cos(contact)), 0.0); + bool top = height > rw_height; + rw_sheet(height, slope, sphere, cover); + // the field rises towards the middle of the drop + if (top && (gradient > 1e-9)) + rw_offset = rw_fgrad * (-across * mean / gradient); +} + +// the blades. a channel of the mask of the wipers is a blade: 0 where it doesn't reach, elsewhere the place of the blade over its +// sweep (0 at rest .. 1 at the far end) at which it goes over the place. wiper_pos: where the blade is (0 .. 1 on its way out, +// 1 + where it is on its way back), wiper_timer_out / _return: the last time it lay at its start / was at its far end +struct rw_wipe_s +{ + float since; // seconds since a blade last went over the place (large where none reaches) + float ahead; // 0 .. 1, how close in front of a moving blade the place is (the water the blade pushes) + float front; // how far in front of the nearest moving blade the place is, units of the mask (1 where none) + float swept; // 0 .. 1, how much of its way the blade in front did, the water it gathered on the way + float working; // 0 .. 1, the blades went lately +}; + +rw_wipe_s rw_wipe(vec4 mask) +{ + rw_wipe_s wipe = rw_wipe_s(1.0e4, 0.0, 1.0, 0.0, 0.0); + for (int i = 0; i < 4; ++i) + { + float m = mask[i]; + if (m < 0.001) + continue; + float pos = wiper_pos[i]; + bool back = pos > 1.0; + float blade = back ? pos - 1.0 : pos; + float t0 = wiper_timer_out[i]; + float t1 = wiper_timer_return[i]; + // when the blade went over the place last: on this stroke, the blade moving at an even pace from its end, or on the + // stroke before, which took as long as the time between the ends (if the blade lay at its end longer, two seconds) + float pass; + float front; // how far in front of the blade the place is (units of the mask), below 0 behind it + if (back) + { + pass = (m >= blade) ? t1 + (time - t1) * ((1.0 - m) / max(1.0 - blade, 1e-3)) : t0 + m * clamp(t1 - t0, 0.0, 2.0); + front = blade - m; + } + else + { + pass = (m <= blade) ? t0 + (time - t0) * (m / max(blade, 1e-3)) : t1 + (1.0 - m) * clamp(t0 - t1, 0.0, 2.0); + front = m - blade; + } + wipe.since = min(wipe.since, max(time - pass, 0.0)); + float moving = smoothstep(0.0, 0.03, blade) * smoothstep(1.0, 0.97, blade); + float ahead = (front > 0.0) ? (1.0 - smoothstep(0.0, 0.035, front)) * moving : 0.0; + wipe.ahead = max(wipe.ahead, ahead); + if ((front > 0.0) && (moving > 0.5)) + wipe.front = min(wipe.front, front); + wipe.swept = max(wipe.swept, ahead * (back ? 1.0 - blade : blade)); + float working = 1.0 - smoothstep(4.0, 30.0, time - t1); + wipe.working = max(wipe.working, working); + } + return wipe; +} + +// a place of the glass a blade reaches +bool rw_reached(vec2 coords) +{ + return any(greaterThanEqual(textureLod(wipermask, clamp(coords, vec2(0.0), vec2(1.0)), 0.0), vec4(0.001))); +} + +// the edge of the sweep of the blades, where they push the water out to: 0 where a blade reaches, else the share of the places +// around at Reach (metres along the glass) which a blade reaches. found from where the blades reach and not from the values of +// the mask, which may stay at the end of the sweep over a wide part of the glass. Maskstep: the coordinates of the mask for a +// step along the glass +float rw_rim(vec2 coords, float reach, mat2 maskstep) +{ + if (rw_reached(coords)) + return 0.0; + float inside = 0.0; + for (int k = 0; k < 6; ++k) + { + float angle = float(k) * (RW_PI / 3.0); + inside += rw_reached(coords + maskstep * (vec2(cos(angle), sin(angle)) * reach)) ? 1.0 : 0.0; + } + return inside / 6.0; +} + +// whether the edge of the sweep of the blades is within Reach of the place +bool rw_border(vec2 coords, float reach, mat2 maskstep) +{ + bool here = rw_reached(coords); + for (int k = 0; k < 6; ++k) + { + float angle = float(k) * (RW_PI / 3.0); + if (rw_reached(coords + maskstep * (vec2(cos(angle), sin(angle)) * reach)) != here) + return true; + } + return false; +} + +// how far along its way a sliding drop is at the share T of the time it takes: it stops and goes, the more smoothly the harder +// it is pushed +float rw_stickslip(float t, float seed) +{ + t = clamp(t, 0.0, 1.0); + float s = t * 3.0; + float i = floor(s); + float stuck = (i + smoothstep(0.25 + 0.4 * seed, 1.0, s - i)) / 3.0; + return mix(stuck, t, smoothstep(0.8, 2.0, rw_push)); +} + +// the track of the water down a column of the glass (Column: its index, Width: of a column, Y: metres up the way): where it +// is across, metres. the water keeps to the way wetted before, so every drop of a column goes the same way, wavy, with kinks +// where the glass held it +float rw_path(int column, float y, float width, uint layer) +{ + float slow = rw_noise(y / (0.05 * rw_scale) + float(column) * 7.31, layer) - 0.5; + float k = y / (0.008 * rw_scale); + float i = floor(k); + float a = rw_random(ivec2(column, int(i)), layer + 500u, 3u).x; + float b = rw_random(ivec2(column, int(i) + 1), layer + 500u, 3u).x; + float kinks = mix(a, b, smoothstep(0.15, 0.85, k - i)) - 0.5; + return (float(column) + 0.5 + 0.4 * slow + 0.2 * kinks) * width; +} + +// the drops which grew big enough to slide, and the streams they leave: Q the place across and up the way they go (Side, Down: +// those directions in the coordinates of the glass), Share of them going this way. a column of the glass carries the water of +// a track; a drop starts somewhere in a cell of the column now and then, slides down with stops, thinning, and leaves a stream +// which drains into beads and dries +void rw_sliders(vec2 q, vec2 side, vec2 down, uint layer, float share) +{ + float width = 0.04 * rw_scale; + float height = 0.15 * rw_scale; + int column = int(floor(q.x / width)); + int row = int(floor(q.y / height)); + vec4 a = rw_random(ivec2(column, 7), layer, 0u); + if (a.x > share * mix(0.35, 0.9, clamp(rw_rate / 3.0, 0.0, 1.0))) + return; + float speed = clamp(0.035 + 0.06 * (rw_push - 0.4) + 0.3 * max(rw_push - 1.5, 0.0), 0.01, 0.8); + float period = clamp(25.0 / rw_rate, 3.0, 60.0); + float trail = 0.0; + for (int r = 0; r < 3; ++r) + { + ivec2 cell = ivec2(column, row + r); + vec4 b = rw_random(cell, layer, 1u); + float span = period * mix(0.7, 1.3, b.x); + float now = floor(time / span + b.y); + for (int k = 0; k < 2; ++k) + { + float cycle = now - float(k); + float start = (cycle - b.y) * span; + float t = time - start; + // the drop gathered from the drops landed since the last wipe + if ((t < 0.0) || (rw_since - t < rw_grow)) + continue; + vec4 c = rw_random(cell, layer + 1000u, uint(int(cycle) + 1000000)); + float top = (float(cell.y) + 0.8 - 0.6 * c.x) * height; + float travel = mix(0.06, 0.28, c.y) * rw_scale; + float v = speed * mix(0.7, 1.3, c.z); + float gather = 0.6; + float moved = travel * rw_stickslip((t - gather) * v / travel, c.w); + float y = top - moved; + float radius = mix(0.003, 0.0042, c.w) * rw_scale * (1.0 - 0.35 * moved / travel) * mix(0.6, 1.0, smoothstep(0.0, gather, t)); + // at the end of its way what is left of it stays and flows out into the stream + float body = 1.0 - smoothstep(0.0, 1.5, t - gather - travel / v); + if ((body > 0.0) && (abs(q.y - y) < 3.5 * radius)) + { + vec2 o = vec2(q.x - rw_path(column, y, width, layer), q.y - y); + rw_blob(side * o.x - down * o.y, radius, body); + // its rear up the stream: a corner, drawn out into a cusp the faster it goes (the front stays round) + float drawn = clamp(v * 5.0, 0.0, 1.5); + for (int k = 1; k <= 2; ++k) + { + float yt = y + radius * (0.45 + 0.5 * drawn) * float(k); + vec2 ot = vec2(q.x - rw_path(column, yt, width, layer), q.y - yt); + rw_blob(side * ot.x - down * ot.y, radius * ((k == 1) ? 0.7 : 0.42), body); + } + } + if ((moved > 0.0) && (q.y > y) && (q.y < top)) + { + float age = t - gather - (top - q.y) / v; + if ((age >= 0.0) && (age < rw_since)) + { + // fresh, the stream is whole; draining, it breaks into beads, which dry + float beads = rw_noise(q.y / (0.004 * rw_scale), layer + 9u); + float wet = mix(1.15, 0.2 + 1.0 * beads, smoothstep(0.0, 15.0, age)) * (1.0 - smoothstep(30.0, 60.0, age)); + trail = max(trail, wet); + } + } + } + } + if (trail > 0.0) + { + float across = q.x - rw_path(column, q.y, width, layer); + float half_width = mix(0.0005, 0.0011, a.y) * rw_scale * (0.6 + 0.8 * rw_noise(q.y / (0.006 * rw_scale), layer + 11u)); + rw_blob(side * across, half_width, trail); + } +} + +// a drop lying on the glass, as rw_drop() with the directions along the glass given in the view space (Down: the way gravity +// pulls along the glass, Side across it) +void rw_drop_view(vec3 offset, float visible, float weight, vec4 shape, vec3 down, vec3 side) +{ + rw_kernel(offset, visible, weight); + if (visible < 1.5 * rw_pixel) + return; + vec3 sag = (down * (0.2 + 0.35 * shape.x) + side * ((shape.y - 0.5) * 0.7)) * visible; + rw_kernel(offset - sag, visible * mix(0.6, 0.85, shape.z), weight); + if (shape.w > 0.4) + { + float angle = shape.w * 40.0; + rw_kernel(offset - (cos(angle) * side + sin(angle) * down) * (0.6 * visible), visible * mix(0.4, 0.6, shape.x), weight); + } +} + +// the water the simulation keeps on the glass (gfx::windscreen_water, water.glsl), where the material is a glass of the cab of the +// train: the drops listed by the cell of the grid the fragment is in. Slot: of the glass, Uv: the coordinates of the fragment, +// Puv: the view space step along the glass for a step of the uv; Down, Side: along the glass. a sliding drop is drawn out behind +// itself into a cusp, the faster the longer; one lying is lopsided and sags down the glass; a flake of snow is lace, grey and +// clear as it melts (Snowcover, Wet: as the flakes of rw_snow()) +void rw_simulated(uint slot, vec2 uv, mat2x3 puv, vec3 down, vec3 side, inout float snowcover, inout float wet) +{ + uint base = water_grid_base(slot); + uint cell = water_cell(uv); + uint count = min(water_cells[base + cell], uint(WATER_CELL_DROPS)); + for (uint k = 0u; k < count; ++k) + { + uint place = water_cells[base + uint(WATER_GRID * WATER_GRID) + cell * uint(WATER_CELL_DROPS) + k]; + water_drop_s drop = water_drops[slot * uint(WATER_DROPS) + place]; + float volume = drop.place.z; + if (volume <= 0.0) + continue; + vec3 offset = puv * (uv - drop.place.xy); + vec4 shape = vec4(rw_pcg4d(uvec4(place, slot, 41u, 0u))) * (1.0 / 4294967296.0); + if (drop.motion.w < 1.0) + { + // a flake: lobed lace, shrinking, grey and clear as it melts + float m = drop.motion.w; + vec2 flat2 = vec2(dot(offset, side), dot(offset, down)); + float radius = 0.5 * water_flake(volume); + float dist = length(flat2); + if (dist < radius + rw_pixel) + { + float angle = atan(flat2.y, flat2.x); + float lobes = 0.55 + 0.45 * (0.6 * rw_noise(angle * 1.9 + shape.z * 40.0, 13u) + 0.4 * abs(cos(3.0 * angle + shape.w * 6.2831853))); + float rim = radius * lobes * (1.0 - 0.45 * m); + float body = smoothstep(rim + rw_pixel, 0.55 * rim - rw_pixel, dist); + float lace = rw_noise2(flat2 / 0.0007 + shape.zw * 100.0, 14u).x; + float flake = body * mix(0.55, 1.0, lace) * (1.0 - 0.7 * m * m); + snowcover = 1.0 - (1.0 - snowcover) * (1.0 - flake); + wet = max(wet, m * body); + } + continue; + } + float radius = water_radius(volume); + vec3 velocity = puv * drop.motion.xy; + float speed = length(velocity); + if (speed > 0.002) + { + // its rear up the way it came: a corner, drawn out into a cusp the faster it goes (the front stays round) + vec3 way = velocity / speed; + rw_kernel(offset, radius, 1.0); + float drawn = clamp(speed * 5.0, 0.0, 1.5); + for (int t = 1; t <= 2; ++t) + rw_kernel(offset + way * (radius * (0.45 + 0.5 * drawn) * float(t)), radius * ((t == 1) ? 0.7 : 0.42), 1.0); + } + else + { + rw_drop_view(offset, radius, 1.0, shape, down, side); + } + } +} + +// how fresh the wetted paths of the simulation are at a place of the glass (x) and how that changes along u and v (yz): the +// texels of their map filtered by a cubic b-spline over 4 x 4 of them, so the edges of a path are smooth curves and its slopes +// change smoothly (a bilinear filter would leave steps at every texel in the bending of the view) +vec3 rw_trail(uint slot, vec2 uv, float now) +{ + // a path stays wet (and seen) longer than the drops keep to it + float lasting = 25.0; + vec2 p = uv * float(WATER_TRAIL) - 0.5; + vec2 i = floor(p); + vec2 f = p - i; + // the weights of the b-spline and their derivatives, for the texels i - 1 .. i + 2 + vec2 g = 1.0 - f; + vec4 wx = vec4(g.x * g.x * g.x, 3.0 * f.x * f.x * f.x - 6.0 * f.x * f.x + 4.0, -3.0 * f.x * f.x * f.x + 3.0 * f.x * f.x + 3.0 * f.x + 1.0, f.x * f.x * f.x) / 6.0; + vec4 wy = vec4(g.y * g.y * g.y, 3.0 * f.y * f.y * f.y - 6.0 * f.y * f.y + 4.0, -3.0 * f.y * f.y * f.y + 3.0 * f.y * f.y + 3.0 * f.y + 1.0, f.y * f.y * f.y) / 6.0; + vec4 dx = vec4(-g.x * g.x, 3.0 * f.x * f.x - 4.0 * f.x, -3.0 * f.x * f.x + 2.0 * f.x + 1.0, f.x * f.x) * 0.5; + vec4 dy = vec4(-g.y * g.y, 3.0 * f.y * f.y - 4.0 * f.y, -3.0 * f.y * f.y + 2.0 * f.y + 1.0, f.y * f.y) * 0.5; + vec3 result = vec3(0.0); + ivec2 base = ivec2(i) - 1; + for (int y = 0; y < 4; ++y) + { + for (int x = 0; x < 4; ++x) + { + float value = water_wetted(slot, base + ivec2(x, y), now, lasting); + result += value * vec3(wx[x] * wy[y], dx[x] * wy[y], wx[x] * dy[y]); + } + } + result.yz *= float(WATER_TRAIL); + return result; +} + +// the surroundings in a view space direction where the screen doesn't hold them: the environment map above the horizon (the sky +// and the clouds), the ground below it, blurred by Lod +vec3 rw_surroundings(vec3 direction, float lod) +{ + vec3 world = mat3(inv_view) * direction; + vec3 sky = envmap_sample_rough(normalize(vec3(world.x, max(world.y, 0.0), world.z)), clamp(lod / 8.0, 0.0, 1.0)); + // the horizon as blurred as the view is + float below = smoothstep(-0.03 - 0.05 * lod, 0.25 + 0.05 * lod, -world.y); + if ((below <= 0.0) || (false == ENVMAP_ENABLED)) + return sky; + return mix(sky, reflection_ground(), below); +} + +// what is seen through the glass in a view space direction: the image of the last frame (the screen space reflections keep it) +// where it holds the place, Distance away along the direction (0: far), blurred by Lod; Share: how much of it the image gave. +// Fade: towards the edges of the image the surroundings take over (else the edges are stretched) +vec3 rw_behind_edges(vec3 direction, float lod, float distance, out float share, bool fade) +{ + share = 0.0; + vec3 around = rw_surroundings(direction, lod); + if (ssr_params.x < 1.0) + return around; + vec4 previous = ssr_reprojection * ((distance > 0.0) ? vec4(direction * distance, 1.0) : vec4(direction, 0.0)); + if (previous.w <= 1e-5) + return around; + vec2 coords = (previous.xy / previous.w) * 0.5 + 0.5; + vec2 edges = min(coords, 1.0 - coords); + share = fade ? smoothstep(0.0, 0.04, min(edges.x, edges.y)) : step(-0.02, min(edges.x, edges.y)); + coords = clamp(coords, vec2(0.0), vec2(1.0)); + if (share <= 0.0) + return around; + // the view out through the water goes on outside, where the image shows the inside of the cab around the windows the + // surroundings stand for it (else the drops show the dark walls and roof of the cab) + ivec2 size = textureSize(scene_depth, 0); + float depth = texelFetch(scene_depth, clamp(ivec2(coords * vec2(size)), ivec2(0), size - 1), 0).r; + float near = projection[3][2]; + if ((depth > 0.0) && (near / depth < -f_pos.z + 2.5)) + { + share = 0.0; + return around; + } + vec3 color = textureLod(ssr_history, coords, clamp(lod, 0.0, ssr_params2.x)).rgb; + if (any(isnan(color)) || any(isinf(color))) + { + share = 0.0; + return around; + } + return mix(around, max(color, vec3(0.0)), share); +} + +vec3 rw_behind(vec3 direction, float lod, float distance, out float share) +{ + return rw_behind_edges(direction, lod, distance, share, true); +} + +// a layer Over put over Under, the colours not premultiplied +vec4 rw_over(vec4 over, vec4 under) +{ + float alpha = over.a + under.a * (1.0 - over.a); + if (alpha <= 1e-5) + return vec4(0.0); + return vec4((over.rgb * over.a + under.rgb * under.a * (1.0 - over.a)) / alpha, alpha); +} + +void main() +{ + vec4 tex_color = texture(diffuse, f_coord); + vec4 mask = texture(wipermask, f_coord); + + // the coordinates of the glass: metres along the axes of the model, so the water stays where it lies on the glass whatever the + // vehicle and the camera do. the axis of the model nearest to the vertical is up, the glass is projected along the + // horizontal one it faces most (along the vertical one if it lies flat) + mat3 linear = mat3(modelview); + mat3 rot = mat3(normalize(linear[0]), normalize(linear[1]), normalize(linear[2])); + mat3 unrot = transpose(rot); + vec3 local = unrot * (f_pos.xyz - modelview[3].xyz); + vec3 viewnormal = normalize(f_normal); + // the water is on the side away from the camera, the outside of the cab + vec3 outward = (dot(viewnormal, f_pos.xyz) < 0.0) ? -viewnormal : viewnormal; + vec3 normal = unrot * outward; + vec3 up = unrot * (vec3(0.0, 1.0, 0.0) * mat3(inv_view)); + vec3 aup = abs(up); + int iu = (aup.x > aup.y) ? ((aup.x > aup.z) ? 0 : 2) : ((aup.y > aup.z) ? 1 : 2); + int ia = (iu + 1) % 3; + int ib = (iu + 2) % 3; + vec3 axis_up = vec3(0.0); + axis_up[iu] = (up[iu] < 0.0) ? -1.0 : 1.0; + vec3 axis_a = vec3(0.0); + axis_a[ia] = 1.0; + vec3 axis_b = vec3(0.0); + axis_b[ib] = 1.0; + vec3 an = abs(normal); + vec3 axis_u, axis_v, axis_n; + if (an[iu] > max(an[ia], an[ib])) + { + axis_n = axis_up; + axis_u = axis_a; + axis_v = axis_b; + } + else if (an[ia] >= an[ib]) + { + axis_n = axis_a; + axis_u = axis_b; + axis_v = axis_up; + } + else + { + axis_n = axis_b; + axis_u = axis_a; + axis_v = axis_up; + } + vec2 glass = vec2(dot(local, axis_u), dot(local, axis_v)); + float facing = dot(normal, axis_n); + facing = ((facing < 0.0) ? -1.0 : 1.0) * max(abs(facing), 0.35); + // a step of the coordinates is a step along the glass + rw_tu = rot * (axis_u - axis_n * (dot(normal, axis_u) / facing)); + rw_tv = rot * (axis_v - axis_n * (dot(normal, axis_v) / facing)); + + // the derivatives, before anything is discarded: the size of a pixel on the glass and in the view, and how the coordinates of + // the mask follow the ones of the glass + vec3 view = normalize(f_pos.xyz); + vec3 dpdx = dFdx(f_pos.xyz); + vec3 dpdy = dFdy(f_pos.xyz); + rw_pixel = max(max(length(dpdx), length(dpdy)), 1e-6); + float pixelangle = max(max(length(dFdx(view)), length(dFdy(view))), 1e-6); + vec2 gdx = dFdx(glass); + vec2 gdy = dFdy(glass); + mat2 steps = mat2(gdx, gdy); + float det = determinant(steps); + mat2 maskstep = (abs(det) > 1e-14) ? mat2(dFdx(f_coord), dFdy(f_coord)) * inverse(steps) : mat2(0.0); + + // the water the simulation keeps, where the material is a glass of the cab of the train (the param water_slot, given by the + // renderer: 1 + the glass): the view space step along the glass for a step of the uv, and how fresh the wetted path the water + // went along is, with how that changes along the glass (before anything is discarded) + int slot = int(param[2].z + 0.5) - 1; + mat2 uvsteps = mat2(dFdx(f_coord), dFdy(f_coord)); + float uvdet = determinant(uvsteps); + bool simulated = (slot >= 0) && (slot < WATER_SLOTS) && (abs(uvdet) > 1e-24); + simulated = simulated && (water_header[clamp(slot, 0, WATER_SLOTS - 1)].x > 0.5); + mat2x3 puv = (abs(uvdet) > 1e-24) ? mat2x3(dpdx, dpdy) * inverse(uvsteps) : mat2x3(0.0); + vec3 trailfield = simulated ? rw_trail(uint(slot), f_coord, water_header[clamp(slot, 0, WATER_SLOTS - 1)].y) : vec3(0.0); + float trail = trailfield.x; + // its gradient along the glass, view space: the uv gradient taken through the steps of the uv along the glass + vec3 trailslope = vec3(0.0); + { + mat2 metric = transpose(puv) * puv; + if (abs(determinant(metric)) > 1e-30) + trailslope = puv * (inverse(metric) * trailfield.yz); + } + + if (tex_color.a < 0.01) + discard; + + vec4 layer = vec4(0.0); + float heavy = clamp(rain_params.x, 0.0, 1.0); + bool snow = rain_params.z > 0.5; + if ((heavy > 0.0) || simulated) + { + rw_scale = max(param[2].y, 0.05); + + // what meets the glass. the air against the vehicle (rain_wind) drags the drops along the glass with what of it goes + // along the glass and with the share of what meets it head on which turns up it: the more the glass leans back the more + // of the air goes along it, and the less of their weight pulls the drops down it + vec3 air = unrot * (rain_wind.xyz * mat3(inv_view)); + float fall = snow ? RW_SNOW_FALL : RW_RAIN_FALL; + float hits = max(-dot(air - up * fall, normal), 0.0); + rw_rate = heavy * clamp(0.7 + hits / fall, 0.7, 6.0); + rw_period = clamp((snow ? 10.0 : 5.0) / rw_rate, 0.4, 300.0); + rw_grow = 2.0 * rw_period; + vec3 gravity = -up - normal * dot(-up, normal); + vec3 rise = up - normal * dot(up, normal); + rise = (length(rise) > 0.1) ? normalize(rise) : vec3(0.0); + vec3 flow = air - normal * dot(air, normal) + rise * (max(-dot(air, normal), 0.0) * RW_HEAD_ON); + vec3 push = gravity + flow * (length(flow) / (RW_DRAG_SPEED * RW_DRAG_SPEED)); + vec2 force = vec2(dot(push, axis_u), dot(push, axis_v)); + rw_push = length(force); + vec2 fallway = vec2(dot(gravity, axis_u), dot(gravity, axis_v)); + rw_down = (length(fallway) > 0.1) ? normalize(fallway) : vec2(0.0, -1.0); + vec2 flowway = vec2(dot(flow, axis_u), dot(flow, axis_v)); + rw_flow = (length(flowway) > 1.0) ? normalize(flowway) : vec2(0.0); + + rw_wipe_s wipe = rw_wipe(mask); + rw_since = wipe.since; + // the blades went lately (any of them, also where none of them reaches) + float working = 0.0; + for (int i = 0; i < 4; ++i) + working = max(working, 1.0 - smoothstep(4.0, 30.0, time - wiper_timer_return[i])); + // how much of the water the place gathers it has + float gathered = 1.0 - exp(-rw_since / (6.0 * rw_period)); + // the film a blade leaves is drawn into fine streaks along its arcs, each a thin cylindrical lens which draws a light seen + // through it into a line across the streaks, towards the pivot of the blade: the way across the streaks on the screen + vec2 streakway = vec2(0.0); + if ((working > 0.0) && (false == snow) && (abs(det) > 1e-14)) + { + int blade = 0; + for (int i = 1; i < 4; ++i) + blade = (mask[i] > mask[blade]) ? i : blade; + if (mask[blade] >= 0.001) + { + // the mask grows along the arcs; a step of a centimetre, so the steps of its values don't matter + float mu = textureLod(wipermask, f_coord + maskstep * vec2(0.01, 0.0), 0.0)[blade]; + float mv = textureLod(wipermask, f_coord + maskstep * vec2(0.0, 0.01), 0.0)[blade]; + vec2 arc = vec2(mu, mv) - mask[blade]; + vec2 onscreen = inverse(steps) * vec2(-arc.y, arc.x); + if ((length(arc) > 1e-4) && (length(onscreen) > 1e-9)) + streakway = normalize(onscreen); + } + } + vec3 ripple = rw_noise2(glass / (0.006 * rw_scale), 5u); + + // the water a blade pushes: a bead in front of it while it moves, as wide as the water it gathered on its way, with a steep + // lumpy front (its edge shows dark, the view can't get out there). the drops the bead reaches flow into it + vec3 lumps = rw_noise2(glass / (0.009 * rw_scale), 7u); + float gatheredwater = snow ? 0.0 : min(gathered * 1.5, 1.0) * (0.35 + 0.65 * min(wipe.swept * 2.5, 1.0)); + float reach = 0.004 + 0.018 * gatheredwater + 0.008 * (ripple.x - 0.5); + float ridge = gatheredwater * smoothstep(0.0, 0.003, wipe.front) * (1.0 - smoothstep(reach - 0.004, reach, wipe.front)); + // the smooth shape from how it changes from pixel to pixel, the lumps on it as they are + float pooledbase = 0.004 * ridge; + float pooled = pooledbase * (0.75 + 0.5 * lumps.x); + vec3 pooledslope = rw_gradient(pooledbase, dpdx, dpdy, outward) * (0.75 + 0.5 * lumps.x) + rw_along(lumps.yz * (0.5 * pooledbase / (0.009 * rw_scale))); + if ((pooled > 0.0) && (false == simulated)) + { + rw_sheet(pooled, pooledslope, 0.03, smoothstep(0.0, 0.1, ridge)); + if (ridge > 0.05) + rw_cleared = time; + } + + float snowcover = 0.0; + float wet = 0.0; + if (simulated) + { + // the directions along the glass, view space + vec3 upview = vec3(0.0, 1.0, 0.0) * mat3(inv_view); + vec3 downview = -(upview - outward * dot(upview, outward)); + downview = (length(downview) > 0.05) ? normalize(downview) : normalize(cross(outward, normalize(rw_tu))); + vec3 sideview = cross(outward, downview); + rw_simulated(uint(slot), f_coord, puv, downview, sideview, snowcover, wet); + // the paths the water went along lately: a rivulet, a band of water rounded across (a cylinder lens: it holds a picture of + // the place around it turned over across the band, its edges dark as the edges of drops), thicker the fresher. the + // smoothed freshness falls off from the middle of a path to its edges + if (trail > 0.08) + { + float p = clamp((trail - 0.08) / 0.5, 0.0, 1.0); + float thickness = 0.0005 * (1.0 - (1.0 - p) * (1.0 - p)); + vec3 rise = trailslope * (0.0005 * 2.0 * (1.0 - p) / 0.5); + float steep = length(rise); + if (steep > 0.7) + rise *= 0.7 / steep; + bool top = thickness > rw_height; + rw_sheet(thickness, rise, 0.004, smoothstep(0.0, 0.15, p) * 0.95); + // from the middle of the band to the fragment + float across = length(trailslope) / 0.5; + if (top && (across > 1.0)) + rw_offset = -trailslope * ((1.0 - p) / (across * length(trailslope))); + } + } + else if (false == snow) + { + // the drops which grew big enough slide off, with gravity and the air: a set of tracks for each of the directions of the + // push nearest to the one now, each laid along its own way, so none of them moves when the push turns + float sliding = smoothstep(0.25, 0.6, rw_push); + if (sliding > 0.0) + { + float bin = atan(force.y, force.x) / (RW_PI / 6.0); + float lower = floor(bin); + for (int k = 0; k < 2; ++k) + { + float share = ((k == 0) ? 1.0 - (bin - lower) : bin - lower) * sliding; + if (share < 0.02) + continue; + float angle = (lower + float(k)) * (RW_PI / 6.0); + vec2 down = vec2(cos(angle), sin(angle)); + vec2 side = vec2(-down.y, down.x); + rw_sliders(vec2(dot(glass, side), -dot(glass, down)), side, down, uint(int(lower) + k + 64) * 2u + 16u, share); + } + } + + // the drops as they land, each time somewhere else in its cell, as often as the rain hits the glass: the drops of the + // rain spread on the glass, thrown with a fine splash which runs back into them + // each drop anywhere in its cell, the cell as large as a drop with its splash reaches + float cell = 0.007 * rw_scale; + vec2 laid = rw_turn(glass, 0.61) + vec2(0.31, 0.17); + ivec2 home = ivec2(floor(laid / cell)); + float density = mix(0.08, 0.3, heavy); + for (int y = -1; y <= 1; ++y) + { + for (int x = -1; x <= 1; ++x) + { + ivec2 c = home + ivec2(x, y); + vec4 a = rw_random(c, 1u, 0u); + if (a.x > density) + continue; + vec4 b = rw_random(c, 1u, 1u); + float span = rw_period * mix(0.7, 1.3, a.w); + float cycle = floor(time / span + b.x); + float age = time - (cycle - b.x) * span; + float life = span * mix(0.5, 0.9, b.y); + if ((age > life) || (age > rw_since) || (time - age < rw_cleared)) + continue; + vec4 p = rw_random(c, 2u, uint(cycle)); + vec2 offset = rw_turn(laid - (vec2(c) + p.xy) * cell, -0.61); + float visible = mix(0.0011, 0.0019, b.z) * rw_scale; + // a raindrop hits the glass far above the speed at which it splashes (Weber number in the hundreds and thousands): + // it spreads, throws a spray of droplets which stay on the glass around it, and draws back + float splash = exp(-age / 0.06); + float fade = 1.0 - smoothstep(life - min(0.4, 0.3 * life), life, age); + if (visible * 0.3 > 0.5 * rw_pixel) + { + int spray = 3 + int(p.z * 4.0); + for (int s = 0; s < spray; ++s) + { + vec4 e = rw_random(c, 3u, uint(cycle) * 8u + uint(s)); + vec2 way = normalize(vec2(cos(e.x * 6.2831853), sin(e.x * 6.2831853)) + rw_flow * 0.9); + rw_blob(offset - way * (visible * (1.4 + 1.6 * e.y)), visible * mix(0.18, 0.35, e.z), fade * (0.55 + 0.45 * splash)); + } + } + rw_drop(offset, visible * (1.0 + 0.3 * splash), fade, p.wzyx); + } + } + // the drops the landing ones gathered into, growing until they are big enough to slide (the air blows them off sooner) + cell = 0.011 * rw_scale; + laid = rw_turn(glass, 1.07) + vec2(0.53, 0.29); + home = ivec2(floor(laid / cell)); + density = mix(0.55, 1.0, heavy); + for (int y = -1; y <= 1; ++y) + { + for (int x = -1; x <= 1; ++x) + { + ivec2 c = home + ivec2(x, y); + vec4 a = rw_random(c, 4u, 0u); + if (a.x > density) + continue; + vec4 b = rw_random(c, 4u, 1u); + float span = rw_period * mix(2.5, 4.0, a.w); + float cycle = floor(time / span + b.x); + float age = time - (cycle - b.x) * span; + float life = span * mix(0.75, 0.98, b.y) / (1.0 + 2.0 * max(rw_push - 1.0, 0.0)); + if ((age > life) || (age > rw_since) || (time - age < rw_cleared)) + continue; + vec4 p = rw_random(c, 5u, uint(cycle)); + // the big drops are much alike: they grow as they gather the small ones (the radius in step with the time) up to the + // size at which they slide off (on a vertical glass a footprint of about 9 mm, larger on a glass leaning back, smaller + // in a strong air flow), and go + float slides = min(0.0045 / sqrt(max(rw_push, 0.3)), 0.005); + float visible = slides * mix(0.6, 1.0, b.z) * rw_scale * mix(0.35, 1.0, clamp(age / (0.6 * life), 0.0, 1.0)); + rw_drop(rw_turn(laid - (vec2(c) + p.xy) * cell, -1.07), visible, 1.0 - smoothstep(life - min(0.5, 0.3 * life), life, age), p.zwxy); + } + } + + // the water the blades push out of their sweep: a broken row of drops flowed together along its edge, just outside it + float leave = working * min(heavy * 1.5, 1.0); + if ((leave > 0.0) && rw_border(f_coord, 0.009 * rw_scale, maskstep)) + { + cell = 0.0065 * rw_scale; + laid = rw_turn(glass, 0.29) + vec2(0.11, 0.43); + home = ivec2(floor(laid / cell)); + for (int y = -1; y <= 1; ++y) + { + for (int x = -1; x <= 1; ++x) + { + ivec2 c = home + ivec2(x, y); + vec4 a = rw_random(c, 20u, 0u); + if (a.x > 0.85) + continue; + // from the fragment to the middle of the drop, along the glass + vec2 centre = rw_turn((vec2(c) + a.yz) * cell - laid, -0.29); + float rim = rw_rim(f_coord + maskstep * centre, 0.004 * rw_scale, maskstep) * leave; + if (rim <= 0.05) + continue; + rw_drop(-centre, mix(0.0016, 0.003, a.w) * rw_scale * mix(0.6, 1.0, rim), smoothstep(0.05, 0.35, rim), rw_random(c, 21u, 0u)); + } + } + } + + // the streams running down from the water the blades leave at the ends of their sweep: a stream in some of the columns + // along the way down, where up the glass there is such water, as far down as the water had time to run since the blade + // went over the place + if ((working > 0.0) && (length(fallway) > 0.3)) + { + vec2 side = vec2(-rw_down.y, rw_down.x); + float width = 0.035 * rw_scale; + float across = dot(glass, side); + float along = -dot(glass, rw_down); + int column = int(floor(across / width)); + vec4 a = rw_random(ivec2(column, 0), 30u, 0u); + if (a.x < 0.6 * heavy * working) + { + float lateral = across - rw_path(column, along, width, 31u); + float half_width = mix(0.0006, 0.0012, a.z) * rw_scale * (0.6 + 0.8 * rw_noise(along / (0.006 * rw_scale), 32u)); + if (abs(lateral) < 2.0 * half_width + rw_pixel) + { + // up the glass, the first place where the reach of the blades begins or ends + const float reach[6] = float[](0.01, 0.025, 0.05, 0.1, 0.18, 0.3); + bool here = rw_reached(f_coord); + float source = -1.0; + for (int s = 0; s < 6; ++s) + { + if (rw_reached(f_coord + maskstep * (-rw_down * reach[s])) != here) + { + source = reach[s]; + break; + } + } + if ((source > 0.0) && (source < max(rw_since - 0.1, 0.0) * 0.2)) + { + float beads = rw_noise(along / (0.004 * rw_scale), 33u); + float flowing = working * (1.0 - 0.5 * smoothstep(0.15, 0.3, source)); + rw_blob(side * lateral, half_width, flowing * mix(0.6, 1.2, beads)); + } + } + } + } + } + else + { + // the flakes stick where they land and melt into drops: the glass is warmed by the cab, faster in the air above zero + float warm = rain_params.w; + float cell = 0.016 * rw_scale; + vec2 laid = rw_turn(glass, 0.83) + vec2(0.07, 0.61); + ivec2 home = ivec2(floor(laid / cell)); + float density = mix(0.5, 0.9, heavy); + for (int y = -1; y <= 1; ++y) + { + for (int x = -1; x <= 1; ++x) + { + ivec2 c = home + ivec2(x, y); + vec4 a = rw_random(c, 11u, 0u); + if (a.x > density) + continue; + vec4 b = rw_random(c, 11u, 1u); + float span = rw_period * mix(0.7, 1.3, a.w); + float cycle = floor(time / span + b.x); + float age = time - (cycle - b.x) * span; + float life = span * mix(0.6, 0.95, b.y); + if ((age > life) || (age > rw_since)) + continue; + vec4 p = rw_random(c, 12u, uint(cycle)); + vec2 offset = rw_turn(laid - (vec2(c) + p.xy) * cell, -0.83); + float diameter = mix(RW_SNOW_DIAMETER.x, RW_SNOW_DIAMETER.y, b.z) * rw_scale; + float melt = ((warm > 0.5) ? mix(3.0, 10.0, b.w) / (1.0 + 0.25 * warm) : mix(15.0, 45.0, b.w)); + if (age < melt) + { + float m = age / melt; + float dist = length(offset.x * rw_tu + offset.y * rw_tv); + float radius = 0.5 * diameter; + if (dist < radius + rw_pixel) + { + float angle = atan(offset.y, offset.x); + float lobes = 0.55 + 0.45 * (0.6 * rw_noise(angle * 1.9 + p.z * 40.0, 13u) + 0.4 * abs(cos(3.0 * angle + p.w * 6.2831853))); + float rim = radius * lobes * (1.0 - 0.45 * m); + float body = smoothstep(rim + rw_pixel, 0.55 * rim - rw_pixel, dist); + float lace = rw_noise2(glass / (0.0007 * rw_scale) + p.zw * 100.0, 14u).x; + float flake = body * mix(0.55, 1.0, lace) * (1.0 - 0.7 * m * m); + snowcover = 1.0 - (1.0 - snowcover) * (1.0 - flake); + wet = max(wet, m * body); + } + } + else + { + // the water of the flake + rw_drop(offset, 0.0018 * pow(diameter / 0.008, 0.667) * rw_scale, 1.0 - smoothstep(life - min(0.5, 0.3 * life), life, age), p.wzyx); + } + } + } + // the snow the blades push in front of them and leave at the ends of their sweep + float pile = max(wipe.ahead * min(gathered * 2.0, 1.0) * (0.4 + 0.6 * min(wipe.swept * 2.5, 1.0)), rw_rim(f_coord, 0.006 * rw_scale, maskstep) * working * heavy); + float packed = rw_noise2(glass / (0.0015 * rw_scale), 15u).x; + snowcover = max(snowcover, smoothstep(0.15, 0.5, pile * 1.2 + 0.4 * (ripple.x - 0.5)) * mix(0.7, 1.0, packed)); + } + // untreated glass, weathered and a little dirty: flat drops (a rain repellent coating would give beads above 90 degrees) + rw_settle(radians(mix(30.0, 48.0, rw_noise2(glass / (0.005 * rw_scale), 16u).x))); + + // the lamps (of the cab, of the scenery) and the headlights light the water up, from both sides of the glass (worked out + // here, where the fragments of a quad go together) + vec3 lamps = vec3(0.0); + vec3 lampgloss = vec3(0.0); + add_local_lights(outward, lamps, lampgloss); + add_local_lights(-outward, lamps, lampgloss); + + // the film just behind a blade, breaking up + float film = snow ? 0.0 : heavy * exp(-rw_since / 0.8) * smoothstep(0.02, 0.08, rw_since); + if (film > 0.01) + { + vec3 fine = rw_noise2(glass / (0.0021 * rw_scale), 6u); + vec2 tilt = fine.yz * (0.004 * film); + rw_sheet(0.00005 * film, rw_along(tilt), 0.3, 0.8 * film); + } + + // what the water shows: the view goes through the glass (a plate: only the way from the air into the water counts) and out + // through the surface of the water, bent as much as it is curved. it is the world turned upside down in a drop, and the + // edges of a drop, where the view can't get out (total internal reflection), show the inside of the cab + float background = 0.0; + bool image = ssr_params.x >= 1.0; + if (image) + { + ivec2 size = textureSize(scene_depth, 0); + float depth = texelFetch(scene_depth, clamp(ivec2(gl_FragCoord.xy), ivec2(0), size - 1), 0).r; + if (depth > 0.0) + background = (projection[3][2] / depth) / max(-view.z, 0.05); + } + vec3 inside = ambient * (AMBIENT_SCALE * 0.5); + vec4 water = vec4(0.0); + if (rw_cover > 0.0) + { + vec3 surface = normalize(outward - rw_slope); + vec3 inwater = refract(view, -outward, 1.0 / RW_IOR); + vec3 through = refract(inwater, -surface, RW_IOR); + // what the surface reflects back inside the water: the light which came in at the other side of the drop, from the sky, + // and the inside of the cab. where the view can't get out (the edge of a drop) that is all it shows: a bright edge + vec3 bounced = reflect(inwater, surface); + if (dot(bounced, outward) < 0.0) + bounced = reflect(bounced, outward); + vec3 inner = mix(inside, rw_surroundings(normalize(bounced), 5.0), 0.3); + vec3 color = inner; + if (dot(through, through) > 0.5) + { + float fresnel = 0.02 + 0.98 * pow(1.0 - abs(dot(through, surface)), 5.0); + // the view through the water spreads over this much of the world across a pixel (radians) + float spread = (RW_IOR - 1.0) * rw_pixel / max(rw_curve, 1e-4); + // the picture the drop holds as a lens, and a share of the bending by the surface + vec3 lens = normalize(f_pos.xyz - (1.0 + RW_LENS) * rw_offset) + (through - view) * RW_LENS_BEND; + float lod = mix(log2(1.0 + RW_LENS) - 0.5, log2(max(spread / pixelangle, 1.0)), RW_LENS_BEND); + // a drop a few pixels across would hold a picture of single pixels, a grain: blurred more the smaller it looks + lod += clamp(2.5 - log2(max(rw_curve / rw_pixel, 1.0)), 0.0, 1.5); + float share; + vec3 seen = rw_behind(normalize(lens), lod, background, share); + // the sun through the drop, where the image doesn't hold it: as bright as the drop gathers its light. in heavier rain + // the sun is behind the clouds, its disc is not there to be focused + float sunshare = (1.0 - heavy) * (1.0 - heavy); + if ((lights_count > 0U) && (share < 1.0) && (sunshare > 0.01)) + { + vec3 sun = normalize(-lights[0].dir); + float width = max(spread, 0.03); + float off = acos(clamp(dot(through, sun), -1.0, 1.0)); + if (off < 4.0 * width) + { + float lit = 1.0 - clamp(calc_shadow(), 0.0, 1.0); + vec3 sunlight = lights[0].color * (SUN_DIFFUSE_SCALE * lights[0].intensity * cloud_shadow(mat3(inv_view) * f_pos.xyz)); + // the light of the sun over the spread of the view: a pixel shows pi * irradiance * the share of a steradian + seen += sunlight * (sunshare * lit * (1.0 - share) * exp(-0.5 * off * off / (width * width)) / (2.0 * width * width)); + } + } + color = mix(seen, inner, fresnel); + } + color += lamps * 0.6; + // without the image of the last frame the surroundings stand for what the water bends the view to; nearly flat water + // would show them in place of the scene behind it, so it is left mostly clear + float opaque = image ? 1.0 : mix(0.3, 1.0, smoothstep(0.02, 0.15, 1.0 - dot(through, view))); + water = vec4(color, rw_cover * opaque); + } + // the wet glass where the rain gathers is a little hazy: the tiniest drops spread the light + if (image && (false == snow)) + { + float mist = heavy * gathered * (simulated ? 0.18 : 0.3); + if (mist > 0.01) + { + float share; + vec3 hazy = rw_behind_edges(view, 3.5, background, share, false); + // the tiniest drops spread the light of the sky coming through the glass + hazy = mix(hazy, rw_surroundings(outward, 7.0), 0.1); + layer = vec4(hazy, mist * max(share, 0.5)); + } + } + layer = rw_over(water, layer); + // the lights seen through the streaks of the film the blades leave, drawn into lines towards their pivot + if (image && (length(streakway) > 0.5)) + { + vec4 previous = ssr_reprojection * ((background > 0.0) ? vec4(view * background, 1.0) : vec4(view, 0.0)); + if (previous.w > 1e-5) + { + vec2 coords = (previous.xy / previous.w) * 0.5 + 0.5; + vec2 texel = 1.0 / vec2(textureSize(ssr_history, 0)); + ivec2 size = textureSize(scene_depth, 0); + float near = projection[3][2]; + // a light: much brighter than the place around it (the bright sky is not) + vec3 around = textureLod(ssr_history, clamp(coords, vec2(0.0), vec2(1.0)), 6.0).rgb; + float threshold = max(1.0, 3.0 * max(around.r, max(around.g, around.b))); + vec3 glare = vec3(0.0); + float total = 0.0; + for (int k = -8; k <= 8; ++k) + { + if (k == 0) + continue; + float w = 1.0 - abs(float(k)) / 9.0; + total += w; + vec2 tap = coords + streakway * texel * (float(k) * 5.0); + if (any(lessThan(tap, vec2(0.0))) || any(greaterThan(tap, vec2(1.0)))) + continue; + // only what is outside: the lamps of the cab are not behind the glass + float depth = texelFetch(scene_depth, clamp(ivec2(tap * vec2(size)), ivec2(0), size - 1), 0).r; + if ((depth > 0.0) && (near / depth < -f_pos.z + 2.5)) + continue; + vec3 c = textureLod(ssr_history, tap, 1.5).rgb; + if (any(isnan(c)) || any(isinf(c))) + continue; + glare += max(c - vec3(threshold), vec3(0.0)) * w; + } + glare *= 0.6 * heavy * working / total; + if (any(greaterThan(glare, vec3(1e-4)))) + { + // the scene itself under the water carries the light added (it changes nothing seen) + vec3 scene = textureLod(ssr_history, clamp(coords, vec2(0.0), vec2(1.0)), 0.0).rgb; + layer = rw_over(layer, vec4(max(scene, vec3(0.0)), 0.2)); + layer.rgb += glare / layer.a; + } + } + } + if (snowcover > 0.0) + { + // the flakes are lit through from the outside, by the sky and the sun, and from the inside of the cab; melting they turn + // grey and clear + vec3 outside = rw_surroundings(outward, 6.0); + if (lights_count > 0U) + { + vec3 sun = normalize(-lights[0].dir); + outside += lights[0].color * (SUN_DIFFUSE_SCALE * lights[0].intensity * max(dot(outward, sun), 0.0) * (1.0 - clamp(calc_shadow(), 0.0, 1.0))); + } + vec3 flake = mix(vec3(0.92), vec3(0.62, 0.66, 0.7), wet); + vec3 color = flake * (outside * (0.35 * (1.0 - 0.5 * wet)) + ambient * (AMBIENT_SCALE * 0.7)); + layer = rw_over(vec4(color, snowcover * mix(0.97, 0.6, wet)), layer); + } + } + out_color = layer; + + { // Overlay windshield texture with alpha + vec3 fragcolor = ambient; + vec3 fragnormal = normalize(f_normal); + float reflectivity = param[1].z; + float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; + legacy_gloss(); + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + vec4 color = vec4(fragcolor, tex_color.a * alpha_mult); + + out_color.xyz = apply_fog(mix(out_color.xyz, color.xyz, color.a)); + out_color.a = mix(out_color.a, 1., color.a); + } + if (false == POSTFX_ENABLED) + { + out_color = tonemap(out_color); + } + + if (MOTIONBLUR_ENABLED) + { + out_motion = vec4(0.0); + } } diff --git a/shaders/ogl46/water.glsl b/shaders/ogl46/water.glsl new file mode 100644 index 00000000..6c3dcbfb --- /dev/null +++ b/shaders/ogl46/water.glsl @@ -0,0 +1,113 @@ +// the water on the windscreens (gfx::windscreen_water, gl::WATER_* in gl/ubo.h): what the passes of the simulation (water_*.comp) +// keep and the glasses (mat_rain_windscreen.frag) read. a glass is a material of mat_rain_windscreen in the cab of the train, a +// slot of the storage; its drops lie in the coordinates of its texture (uv), as its mask of the wipers does, so they stay where +// they lie on the glass whatever the vehicle and the camera do. the map of the glass tells what a step of the uv is along the +// glass (metres, in the space of the model of the cab) and where the blades go over it; the drops are looked up through a grid +// of cells over the uv, each listing the drops which reach into it; the wetted paths are marked on a map of their own with the +// time the water went there + +#ifndef WATER_GLSL +#define WATER_GLSL + +struct water_drop_s +{ + // xy: the place, uv; z: the water, mm3 (0: no drop there); w: seconds it lies on the glass + vec4 place; + // xy: the velocity, uv per second; z: random 0 .. 1, its shape; w: 0 .. 1 how far it melted from a flake of snow (1: water) + vec4 motion; +}; + +#if defined(STAGE_COMPUTE) +layout(std430, binding = WATER_SSBO_BINDING) buffer water_ssbo +#else +layout(std430, binding = WATER_SSBO_BINDING) readonly buffer water_ssbo +#endif +{ + // every glass: x 1 when it's simulated, y seconds its simulation runs (the time of the wetted paths), zw unused + vec4 water_header[WATER_SLOTS]; + // every glass: x the drops on it, yzw unused + uvec4 water_counts_of[WATER_SLOTS]; + water_drop_s water_drops[WATER_SLOTS * WATER_DROPS]; + // the map, two for a texel: xyz the step along the glass for a step of u (metres), w 1 on the glass, 0.5 next to it (the edges + // spread out, for the filtering), 0 elsewhere; xyz the same for v, w 1 when the cross product of the two points out of the cab, + // -1 when it points in + vec4 water_axes[WATER_SLOTS * WATER_MAP * WATER_MAP * 2]; + // the mask of the wipers baked into the map: the place of a blade over its sweep at which it goes over the texel, 0 where it + // doesn't reach + vec4 water_blades[WATER_SLOTS * WATER_MAP * WATER_MAP]; + // every drop: 0 the place is free, else bit 0 set, bits 1 .. 3 the blade carrying it (1 + its number, 0 none), bit 4 set when + // the blade carries it towards the far end of its sweep + uint water_state[WATER_SLOTS * WATER_DROPS]; + // the drop it flows into (itself when none) + uint water_target[WATER_SLOTS * WATER_DROPS]; + // what flowed into it: the water (1e-4 mm3) and the moment of the places of the drops about its own (1e-6 uv mm3) + int water_gather[WATER_SLOTS * WATER_DROPS * 4]; + // x: the way it went since it left a droplet behind, metres; y: the way it goes on before it leaves the next one, metres + vec4 water_extra[WATER_SLOTS * WATER_DROPS]; + // the grids of the glasses, WATER_GRID x WATER_GRID cells each: the counts of the drops of the cells, then the drops of each + // cell (WATER_CELL_DROPS for a cell, the ones over the count aren't there). then the wetted paths of the glasses, + // WATER_TRAIL x WATER_TRAIL each: in the upper 24 bits 1 + the time the water went over the texel last, 1/2 s (0: never), + // in the lower 8 how near the middle of the path the texel is, 0 .. 255 (filtered between the texels the edges of the paths + // come out smooth wherever they go across the texels; the time is coarse, so a drop going along leaves the nearness of its + // whole passage, not of the step which happened to come last) + uint water_cells[]; +}; + +// the radius of the footprint of a drop holding Volume mm3 of water, metres: a cap of a sphere meeting the glass at 40 degrees +float water_radius(float volume) +{ + return 0.001 * pow(1.675 * max(volume, 0.0), 1.0 / 3.0); +} + +// the diameter of a flake of snow holding Volume mm3 of water, metres +float water_flake(float volume) +{ + return 0.006 * pow(max(volume, 0.0), 1.0 / 3.0); +} + +// where the grid of a glass begins in water_cells: the count of a cell at the base + the cell, its drops from the base + +// WATER_GRID * WATER_GRID + the cell * WATER_CELL_DROPS +uint water_grid_base(uint slot) +{ + return slot * uint(WATER_GRID * WATER_GRID * (1 + WATER_CELL_DROPS)); +} + +uint water_trail_base(uint slot) +{ + return uint(WATER_SLOTS * WATER_GRID * WATER_GRID * (1 + WATER_CELL_DROPS)) + slot * uint(WATER_TRAIL * WATER_TRAIL); +} + +// the cell of the grid holding a place of the glass +uint water_cell(vec2 uv) +{ + ivec2 cell = clamp(ivec2(floor(uv * float(WATER_GRID))), ivec2(0), ivec2(WATER_GRID - 1)); + return uint(cell.y * WATER_GRID + cell.x); +} + +// how fresh the wetted path is at a texel of the map of the paths, 1 just wetted in its middle .. 0 dry long ago or never; Now: +// seconds the simulation of the glass runs, Lasting: seconds in which the freshness falls to 1/e +float water_wetted(uint slot, ivec2 texel, float now, float lasting) +{ + texel = clamp(texel, ivec2(0), ivec2(WATER_TRAIL - 1)); + uint value = water_cells[water_trail_base(slot) + uint(texel.y * WATER_TRAIL + texel.x)]; + uint stamp = value >> 8u; + if (stamp == 0u) + return 0.0; + float age = max(now - float(stamp - 1u) / 2.0, 0.0); + return exp(-age / lasting) * (float(value & 255u) / 255.0); +} + +// the same filtered between the texels at a place of the glass +float water_wetted_at(uint slot, vec2 uv, float now, float lasting) +{ + vec2 p = uv * float(WATER_TRAIL) - 0.5; + ivec2 i = ivec2(floor(p)); + vec2 f = p - vec2(i); + float a = water_wetted(slot, i, now, lasting); + float b = water_wetted(slot, i + ivec2(1, 0), now, lasting); + float c = water_wetted(slot, i + ivec2(0, 1), now, lasting); + float d = water_wetted(slot, i + ivec2(1, 1), now, lasting); + return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); +} + +#endif diff --git a/shaders/ogl46/water_bake.comp b/shaders/ogl46/water_bake.comp new file mode 100644 index 00000000..4fec4729 --- /dev/null +++ b/shaders/ogl46/water_bake.comp @@ -0,0 +1,24 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): the mask of the wipers of a glass baked into its map, once, when the glass is +// set up. a thread per texel of the map; the glass is water_counts.z + +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +// the mask of the wipers of the material of the glass (wipermask) +layout(binding = WATER_MASK_TEX) uniform sampler2D water_mask; + +void main() +{ + uint slot = water_counts.z; + uint texel = gl_GlobalInvocationID.x; + if ((slot >= uint(WATER_SLOTS)) || (texel >= uint(WATER_MAP * WATER_MAP))) + return; + vec2 uv = (vec2(float(texel % uint(WATER_MAP)), float(texel / uint(WATER_MAP))) + 0.5) / float(WATER_MAP); + // as much of the mask as a texel of the map covers + float lod = log2(max(float(textureSize(water_mask, 0).x) / float(WATER_MAP), 1.0)); + water_blades[slot * uint(WATER_MAP * WATER_MAP) + texel] = textureLod(water_mask, uv, lod); +} diff --git a/shaders/ogl46/water_clear.comp b/shaders/ogl46/water_clear.comp new file mode 100644 index 00000000..4a867ead --- /dev/null +++ b/shaders/ogl46/water_clear.comp @@ -0,0 +1,18 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): the counts of the cells of the grids emptied before the drops are put in them +// (water_grid.comp). a thread per cell, a row of groups per glass + +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +void main() +{ + uint slot = gl_WorkGroupID.y; + uint cell = gl_GlobalInvocationID.x; + if ((water_slots[slot].z < 0.5) || (cell >= uint(WATER_GRID * WATER_GRID))) + return; + water_cells[water_grid_base(slot) + cell] = 0u; +} diff --git a/shaders/ogl46/water_gather.comp b/shaders/ogl46/water_gather.comp new file mode 100644 index 00000000..241a9050 --- /dev/null +++ b/shaders/ogl46/water_gather.comp @@ -0,0 +1,40 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): the drops flow into the ones water_merge.comp found for them, where those don't +// flow on themselves (they do in a later step): the water and the moment of the places of the drops gathered by the one they +// flow into (water_settle.comp moves it to the middle of the water), the drops gone. a thread per drop, a row of groups per glass + +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +void main() +{ + uint slot = gl_WorkGroupID.y; + uint place = gl_GlobalInvocationID.x; + if ((water_slots[slot].z < 0.5) || (place >= uint(WATER_DROPS))) + return; + uint base = slot * uint(WATER_DROPS); + uint index = base + place; + uint target = water_target[index]; + if ((target == place) || (target >= uint(WATER_DROPS)) || (water_target[base + target] != target)) + return; + water_drop_s drop = water_drops[index]; + water_drop_s into = water_drops[base + target]; + if ((drop.place.z <= 0.0) || (into.place.z <= 0.0)) + return; + // with what flowed into the drop itself in the step (the landings) + float landed = float(water_gather[index * 4u]) * 1e-4; + vec2 moment = vec2(float(water_gather[index * 4u + 1u]), float(water_gather[index * 4u + 2u])) * 1e-6; + float volume = drop.place.z + landed; + vec2 offset = drop.place.xy - into.place.xy; + atomicAdd(water_gather[(base + target) * 4u], int(volume * 1e4)); + atomicAdd(water_gather[(base + target) * 4u + 1u], int((offset.x * volume + moment.x) * 1e6)); + atomicAdd(water_gather[(base + target) * 4u + 2u], int((offset.y * volume + moment.y) * 1e6)); + // the blade carrying the drop carries what it flows into + uint carried = water_state[index] & 0x1eu; + if ((carried != 0u) && ((water_state[base + target] & 0x0eu) == 0u)) + atomicOr(water_state[base + target], carried); + water_remove(index); +} diff --git a/shaders/ogl46/water_grid.comp b/shaders/ogl46/water_grid.comp new file mode 100644 index 00000000..60c5db63 --- /dev/null +++ b/shaders/ogl46/water_grid.comp @@ -0,0 +1,52 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): every drop put in the cells of the grid it reaches into, as far as the glasses +// draw it (a sliding one is drawn out behind itself) and as far as the drops it may flow together with are. a thread per drop, +// a row of groups per glass + +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +void main() +{ + uint slot = gl_WorkGroupID.y; + uint place = gl_GlobalInvocationID.x; + if ((water_slots[slot].z < 0.5) || (place >= uint(WATER_DROPS))) + return; + water_drop_s drop = water_drops[slot * uint(WATER_DROPS) + place]; + if (drop.place.z <= 0.0) + return; + water_frame_s frame = water_frame(slot, drop.place.xy); + float lu = length(frame.tu); + float lv = length(frame.tv); + if ((lu < 1e-6) || (lv < 1e-6)) + return; + float reach; + if (drop.motion.w < 1.0) + { + reach = 0.6 * water_flake(drop.place.z); + } + else + { + bool sliding = length(frame.tu * drop.motion.x + frame.tv * drop.motion.y) > 0.002; + reach = water_radius(drop.place.z) * (sliding ? 3.6 : 2.2); + } + vec2 extent = vec2(reach / lu, reach / lv); + ivec2 lower = clamp(ivec2(floor((drop.place.xy - extent) * float(WATER_GRID))), ivec2(0), ivec2(WATER_GRID - 1)); + ivec2 upper = clamp(ivec2(floor((drop.place.xy + extent) * float(WATER_GRID))), ivec2(0), ivec2(WATER_GRID - 1)); + // a drop can't be larger than this many cells, it would only flood the lists + upper = min(upper, lower + ivec2(6)); + uint base = water_grid_base(slot); + for (int y = lower.y; y <= upper.y; ++y) + { + for (int x = lower.x; x <= upper.x; ++x) + { + uint cell = uint(y * WATER_GRID + x); + uint count = atomicAdd(water_cells[base + cell], 1u); + if (count < uint(WATER_CELL_DROPS)) + water_cells[base + uint(WATER_GRID * WATER_GRID) + cell * uint(WATER_CELL_DROPS) + count] = place; + } + } +} diff --git a/shaders/ogl46/water_merge.comp b/shaders/ogl46/water_merge.comp new file mode 100644 index 00000000..9f47561a --- /dev/null +++ b/shaders/ogl46/water_merge.comp @@ -0,0 +1,63 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): the drops which touch flow together, the way surface tension pulls them. every +// drop finds the largest of the drops it touches which are larger than itself (by the cell of the grid its middle is in, which +// lists every drop reaching into it); the water goes over to that one in water_gather.comp. flakes of snow don't flow, but the +// ones a blade pushes pile up. a thread per drop, a row of groups per glass + +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +void main() +{ + uint slot = gl_WorkGroupID.y; + uint place = gl_GlobalInvocationID.x; + if ((water_slots[slot].z < 0.5) || (place >= uint(WATER_DROPS))) + return; + uint index = slot * uint(WATER_DROPS) + place; + water_drop_s drop = water_drops[index]; + water_target[index] = place; + if (drop.place.z <= 0.0) + return; + bool flake = drop.motion.w < 1.0; + bool carried = ((water_state[index] >> 1u) & 7u) != 0u; + if (flake && (false == carried)) + return; + water_frame_s frame = water_frame(slot, drop.place.xy); + float size = flake ? 0.4 * water_flake(drop.place.z) : water_radius(drop.place.z); + uint cell = water_cell(drop.place.xy); + uint base = water_grid_base(slot); + uint count = min(water_cells[base + cell], uint(WATER_CELL_DROPS)); + uint best = place; + float most = drop.place.z; + for (uint k = 0u; k < count; ++k) + { + uint other = water_cells[base + uint(WATER_GRID * WATER_GRID) + cell * uint(WATER_CELL_DROPS) + k]; + if (other == place) + continue; + uint otherindex = slot * uint(WATER_DROPS) + other; + water_drop_s neighbour = water_drops[otherindex]; + float volume = neighbour.place.z; + if ((volume < most) || ((volume == most) && (other > best)) || (volume <= 0.0)) + continue; + if ((volume == drop.place.z) && (other > place)) + continue; + bool otherflake = neighbour.motion.w < 1.0; + if (otherflake != flake) + continue; + // the snow a blade pushes piles up into lumps a few centimetres across, not more + if (otherflake && ((((water_state[otherindex] >> 1u) & 7u) == 0u) || (volume + drop.place.z > 20.0))) + continue; + vec2 step = neighbour.place.xy - drop.place.xy; + float apart = length(frame.tu * step.x + frame.tv * step.y); + float reach = size + (otherflake ? 0.4 * water_flake(volume) : water_radius(volume)); + if (apart < 0.92 * reach) + { + best = other; + most = volume; + } + } + water_target[index] = best; +} diff --git a/shaders/ogl46/water_move.comp b/shaders/ogl46/water_move.comp new file mode 100644 index 00000000..feca9136 --- /dev/null +++ b/shaders/ogl46/water_move.comp @@ -0,0 +1,254 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): every drop through the step. a thread per drop, a row of groups per glass. +// a blade going over the drop takes it along in front of itself until it turns back at the end of its sweep or stops, or until +// the drop is pushed out of the sweep; a drop of its own slides when gravity and the air pull it harder than its line of +// contact holds it, wetting its path and leaving droplets behind; a flake melts into a drop + +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +// how fresh the wetted path is at a place of the glass +float water_fresh(uint slot, vec2 uv) +{ + return water_wetted_at(slot, uv, water_step.y, 8.0); +} + +// the path from one place to another marked wetted, Halfwidth (metres) to the sides: the texels around it get the time and how +// near they are to the middle of the path +void water_wet_path(uint slot, water_frame_s frame, vec2 from, vec2 to, float halfwidth) +{ + uint stamp = (uint(water_step.y * 2.0) + 1u) << 8u; + uint base = water_trail_base(slot); + float texels = float(WATER_TRAIL); + // the half width in texels, a texel and a half more for the filtering + float reach = halfwidth / max(min(length(frame.tu), length(frame.tv)), 1e-6) * texels + 1.5; + int spread = clamp(int(ceil(reach)), 1, 3); + vec2 a = from * texels - 0.5; + vec2 b = to * texels - 0.5; + int steps = clamp(int(ceil(length(b - a) * 2.0)), 1, 24); + for (int s = 1; s <= steps; ++s) + { + vec2 point = mix(a, b, float(s) / float(steps)); + ivec2 centre = ivec2(floor(point + 0.5)); + for (int y = -spread; y <= spread; ++y) + { + for (int x = -spread; x <= spread; ++x) + { + ivec2 texel = centre + ivec2(x, y); + if (any(lessThan(texel, ivec2(0))) || any(greaterThanEqual(texel, ivec2(WATER_TRAIL)))) + continue; + float near = 1.0 - length(vec2(texel) - point) / reach; + if (near <= 0.0) + continue; + atomicMax(water_cells[base + uint(texel.y * WATER_TRAIL + texel.x)], stamp | uint(near * 255.0)); + } + } + } +} + +void main() +{ + uint slot = gl_WorkGroupID.y; + uint place = gl_GlobalInvocationID.x; + if ((water_slots[slot].z < 0.5) || (place >= uint(WATER_DROPS))) + return; + uint index = slot * uint(WATER_DROPS) + place; + water_drop_s drop = water_drops[index]; + if (drop.place.z <= 0.0) + return; + float seconds = water_step.x; + uint state = water_state[index]; + vec4 extra = water_extra[index]; + vec2 uv = drop.place.xy; + water_frame_s frame = water_frame(slot, uv); + if (frame.valid < 0.25) + { + water_remove(index); + return; + } + uint seed = water_pcg4d(uvec4(water_counts.x, slot, place, 9u)).x; + + // a flake melts into a drop: the glass is warmed by the cab, the faster the warmer the air + bool flake = drop.motion.w < 1.0; + if (flake) + { + float warm = water_up.w; + float melt = (warm > 0.5) ? mix(3.0, 10.0, drop.motion.z) / (1.0 + 0.25 * warm) : mix(15.0, 45.0, drop.motion.z); + drop.motion.w = min(drop.motion.w + seconds / melt, 1.0); + flake = drop.motion.w < 1.0; + } + + // the blades: one going over the drop in the step takes it along + uint carried = (state >> 1u) & 7u; + float way = (((state >> 4u) & 1u) != 0u) ? 1.0 : -1.0; + vec4 mask = water_blades_at(slot, uv); + vec4 reached = water_blades_texel(slot, uv); + for (int k = 0; k < 4; ++k) + { + float before = water_blades_before[k]; + float now = water_blades_now[k]; + if ((before < 0.0) || (now < 0.0)) + { + if (carried == uint(k + 1)) + carried = 0u; + continue; + } + float moved = now - before; + if (carried == uint(k + 1)) + { + // the blade turned back at the end of its sweep or stopped: the water stays where it brought it + if ((moved * way <= 0.0) || (now >= 0.995) || (now <= 0.005)) + carried = 0u; + } + // a blade goes over a drop which was ahead of it (by a little more than nothing: the water it left where it turned back at + // the end of its sweep isn't taken back) and is behind it now + else if ((carried == 0u) && (moved != 0.0) && (reached[k] >= 0.001) && ((mask[k] - before) * sign(moved) > 0.003) && ((mask[k] - now) * sign(moved) <= 0.0)) + { + carried = uint(k + 1); + way = (moved > 0.0) ? 1.0 : -1.0; + // the blade doesn't take all of it: now and then a droplet stays behind + vec4 r = water_random(seed, 5u, place, slot); + if ((false == flake) && (drop.place.z > 1.0) && (r.x < 0.35)) + { + float left = mix(0.02, 0.08, r.y); + if (water_add(slot, seed ^ 0x9e3779b9u, uv, left, 1.0, r.z, false)) + drop.place.z -= left; + } + } + } + + vec3 velocity = frame.tu * drop.motion.x + frame.tv * drop.motion.y; + if (carried != 0u) + { + // in front of the blade by its own size and the lip of the bead: the place where the mask is as far ahead of the blade, + // found along the gradient of the mask (along the arc of the blade) + int k = int(carried) - 1; + float radius = flake ? 0.5 * water_flake(drop.place.z) : water_radius(drop.place.z); + float a = dot(frame.tu, frame.tu); + float b = dot(frame.tu, frame.tv); + float c = dot(frame.tv, frame.tv); + float det = max(a * c - b * b, 1e-20); + float h = 1.0 / float(WATER_MAP); + bool edge = false; + for (int step = 0; step < 4; ++step) + { + // at the edge of the sweep (the end of the blade) the water squeezes out and stays + if ((water_blades_texel(slot, uv + vec2(h, 0.0))[k] < 0.001) || (water_blades_texel(slot, uv - vec2(h, 0.0))[k] < 0.001) || + (water_blades_texel(slot, uv + vec2(0.0, h))[k] < 0.001) || (water_blades_texel(slot, uv - vec2(0.0, h))[k] < 0.001)) + { + edge = true; + break; + } + float m = water_blades_at(slot, uv)[k]; + vec2 gradient = vec2(water_blades_at(slot, uv + vec2(h, 0.0))[k] - water_blades_at(slot, uv - vec2(h, 0.0))[k], + water_blades_at(slot, uv + vec2(0.0, h))[k] - water_blades_at(slot, uv - vec2(0.0, h))[k]) / (2.0 * h); + float g2 = dot(gradient, gradient); + if (g2 < 1e-6) + break; + // how fast the mask changes along the glass, per metre + float permetre = sqrt(max((c * gradient.x * gradient.x - 2.0 * b * gradient.x * gradient.y + a * gradient.y * gradient.y) / det, 0.0)); + float target = water_blades_now[k] + way * (0.0015 + radius) * permetre; + vec2 delta = gradient * ((target - m) / g2); + float length_delta = length(delta); + if (length_delta > 0.02) + delta *= 0.02 / length_delta; + uv += delta; + if (length_delta < 1e-5) + break; + } + // pushed out of the sweep it stays at its edge + if (edge || (water_blades_texel(slot, uv)[k] < 0.001)) + carried = 0u; + velocity = vec3(0.0); + } + else if (false == flake) + { + float volume = drop.place.z; + float radius = water_radius(volume); + vec3 n = frame.normal; + vec3 up = water_up.xyz; + // gravity along the glass + float weight = 1000.0 * WATER_GRAVITY * volume * 1e-9; + vec3 force = -(up - n * dot(up, n)) * weight; + // the drag of the air going along the glass, with a share of the air meeting it head on turned up along it + vec3 air = water_air.xyz; + vec3 rise = up - n * dot(up, n); + rise = (length(rise) > 0.1) ? normalize(rise) : vec3(0.0); + vec3 flow = (air - n * dot(air, n) + rise * (max(-dot(air, n), 0.0) * WATER_HEAD_ON)) * WATER_BOUNDARY; + float area = 2.0 * radius * (0.364 * radius) * 0.8; + force += flow * (0.6 * area * length(flow)); + // the hold of the line of contact, uneven over the glass, much less on a path wetted lately + vec2 spread = uv * vec2(length(frame.tu), length(frame.tv)); + float uneven = mix(0.55, 1.45, water_noise(spread / 0.003, 3u + slot)); + float hold = WATER_TENSION * 2.0 * radius * WATER_HOLD * uneven * mix(1.0, 0.3, water_fresh(slot, uv)) * mix(0.85, 1.15, drop.motion.z); + float pull = length(force); + vec3 target = vec3(0.0); + if (pull > hold) + { + vec3 dir = force / pull; + // the way bent towards the side which holds the drop less: the wetter one (the water keeps to the wetted paths) and the + // one where the glass is cleaner, so it meanders + vec3 side = cross(n, dir); + float offset = max(radius, 0.0015); + vec2 leftuv = uv + water_to_uv(frame, side * offset); + vec2 rightuv = uv - water_to_uv(frame, side * offset); + float wetter = water_fresh(slot, leftuv) - water_fresh(slot, rightuv); + float cleaner = water_noise(rightuv * vec2(length(frame.tu), length(frame.tv)) / 0.003, 3u + slot) - water_noise(leftuv * vec2(length(frame.tu), length(frame.tv)) / 0.003, 3u + slot); + float wander = water_noise(spread / 0.02 + vec2(17.0 * drop.motion.z), 11u + slot) - 0.5; + dir = normalize(dir + side * (wetter * 0.6 + cleaner * 0.5 + wander * 0.35)); + target = dir * min((pull - hold) / (WATER_VISCOUS * radius), 1.5); + } + velocity += (target - velocity) * (1.0 - exp(-seconds / ((pull > hold) ? 0.05 : 0.015))); + velocity -= n * dot(velocity, n); + vec3 moved = velocity * seconds; + float travel = length(moved); + if (travel > 1e-7) + { + vec2 next = uv + water_to_uv(frame, moved); + water_wet_path(slot, frame, uv, next, 0.4 * radius); + // a sliding drop leaves droplets behind as it goes, and thins; a large one breaks up the faster (a lot of water runs as + // a stream of beads rather than as one big drop) + extra.x += travel; + if ((extra.x >= extra.y) && (volume > 1.5)) + { + vec4 r = water_random(seed, 6u, place, slot); + float left = clamp(volume * mix(0.01, 0.03, r.x) * (1.0 + volume / 40.0), 0.01, 0.25 * volume); + vec3 behind = -(velocity / max(length(velocity), 1e-6)) * ((radius + 1.2 * water_radius(left)) * 1.05); + if (water_add(slot, seed ^ 0x85ebca6bu, uv + water_to_uv(frame, behind), left, 1.0, r.y, false)) + drop.place.z -= left; + extra.x = 0.0; + extra.y = mix(0.002, 0.005, r.z) / (1.0 + volume / 100.0); + } + uv = next; + } + // without the rain the water dries, slowly + if (water_step.z <= 0.0) + drop.place.z -= seconds * 0.004 * radius * 1000.0; + } + else + { + velocity = vec3(0.0); + } + + if ((drop.place.z < WATER_LEAST) || any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0)))) + { + water_remove(index); + return; + } + water_frame_s there = water_frame(slot, uv); + if (there.valid < 0.25) + { + // off the glass, it falls off + water_remove(index); + return; + } + drop.place.xy = uv; + drop.place.w += seconds; + drop.motion.xy = water_to_uv(there, velocity); + water_drops[index] = drop; + water_extra[index] = extra; + water_state[index] = 1u | (carried << 1u) | ((way > 0.0) ? 16u : 0u); +} diff --git a/shaders/ogl46/water_settle.comp b/shaders/ogl46/water_settle.comp new file mode 100644 index 00000000..78638cf8 --- /dev/null +++ b/shaders/ogl46/water_settle.comp @@ -0,0 +1,36 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): the drops which gathered others take their water, move to the middle of it and +// slow down as much as the still water slows them; the header of the glass tells the glasses it's simulated. a thread per drop, +// a row of groups per glass + +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +void main() +{ + uint slot = gl_WorkGroupID.y; + uint place = gl_GlobalInvocationID.x; + if ((water_slots[slot].z < 0.5) || (place >= uint(WATER_DROPS))) + return; + if (place == 0u) + water_header[slot] = vec4(1.0, water_step.y, 0.0, 0.0); + uint index = slot * uint(WATER_DROPS) + place; + int gathered = water_gather[index * 4u]; + if (gathered == 0) + return; + vec2 moment = vec2(float(water_gather[index * 4u + 1u]), float(water_gather[index * 4u + 2u])) * 1e-6; + water_gather[index * 4u] = 0; + water_gather[index * 4u + 1u] = 0; + water_gather[index * 4u + 2u] = 0; + water_drop_s drop = water_drops[index]; + if (drop.place.z <= 0.0) + return; + float volume = drop.place.z + float(gathered) * 1e-4; + drop.place.xy += moment / volume; + drop.motion.xy *= drop.place.z / volume; + drop.place.z = volume; + water_drops[index] = drop; +} diff --git a/shaders/ogl46/water_sim.glsl b/shaders/ogl46/water_sim.glsl new file mode 100644 index 00000000..75919c9e --- /dev/null +++ b/shaders/ogl46/water_sim.glsl @@ -0,0 +1,279 @@ +// what the passes of the simulation of the water on the windscreens share (water_*.comp): the block of the step, the random +// numbers, the map of the glass, the forces on a drop +// +// the physics (the drops are caps of spheres flat on the glass, 40 degrees at their edge, untreated weathered glass): +// - a drop lies held by its line of contact: the hold is the surface tension times the width of the drop times the difference +// of the cosines of its angles at the front and the rear (WATER_HOLD), so it grows with the radius, while the weight grows +// with the cube of it. on a vertical glass a drop slides once its footprint is about 7 mm across (25 mm3), on a glass leaning +// back later. the hold is uneven over the glass (dirt, scratches: the drops stop and go) and much less where the water went +// lately (a wetted path: the next drops follow it and it becomes a rivulet) +// - the air going along the glass drags the drops (with the area they stand up into it, within the layer of slow air at the +// glass), and of the air meeting the glass head on a share turns up along it: at speed the water goes up the glass, the sooner +// the more it leans back +// - a sliding drop is held back by the viscosity in proportion to its speed and radius; it leaves droplets behind as it goes +// (it thins and stops in the end) and wets its path +// - drops which touch flow together into one (surface tension), the water and the momentum of the places kept +// - a raindrop hits the glass far above the speed at which it splashes: it spreads and throws droplets around, which stay +// - a blade going over a drop takes it along in front of itself; the water it gathers flows together into a bead along it, left +// at the ends of its sweep (where it runs down as streams) and pushed off where the glass ends + +#ifndef WATER_SIM_GLSL +#define WATER_SIM_GLSL + +#include "water.glsl" + +// gl::water_ubs, every vector in the space of the model of the cab +layout(std140, binding = WATER_UBO_BINDING) uniform water_ubo +{ + // x: seconds of the step, y: seconds the simulation runs, z: how heavy the rain or the snow is 0 .. 1, w: 1 for snow + vec4 water_step; + // xyz: up, w: the temperature of the air, degrees Celsius + vec4 water_up; + // xyz: the air against the cab, m/s; w: how fast the rain or the snow falls, m/s + vec4 water_air; + // the blades now and at the step before: 0 .. 1 across their sweep, below 0 where there's none + vec4 water_blades_now; + vec4 water_blades_before; + // x: the smallest diameter of a raindrop landing as a drop of its own, mm, y: 1 / the mean of the diameters above it, 1 / mm + vec4 water_rain; + // x: random seed of the step, y: the most landings tried on a glass, z: the glass a single glass pass works on + uvec4 water_counts; + // every glass: x the landings tried in the step, y 1 / the most metres squared a unit of its uv holds, z 1 when it's simulated, + // w the size of its drops (rain_scale of its material) + vec4 water_slots[WATER_SLOTS]; +}; + +const float WATER_GRAVITY = 9.81; +// the surface tension of water, N/m, and the share of it a drop is held by for the width of its footprint +const float WATER_TENSION = 0.072; +const float WATER_HOLD = 0.5; +// the viscous drag of a sliding drop for its radius and speed, N s / m2 +const float WATER_VISCOUS = 1.3; +// the share of the speed of the air the drops feel in the slow layer at the glass, and of the air meeting the glass head on +// which turns up along it +const float WATER_BOUNDARY = 0.3; +const float WATER_HEAD_ON = 0.35; +// the drops below this much water (mm3) dry at once; a raindrop or a droplet of a splash smaller than WATER_SEEN (0.5 mm across +// its footprint, hardly a pixel) landing where there's no drop is left to the mist of the glass +const float WATER_LEAST = 0.004; +const float WATER_SEEN = 0.075; + +uvec4 water_pcg4d(uvec4 v) +{ + v = v * 1664525u + 1013904223u; + v.x += v.y * v.w; + v.y += v.z * v.x; + v.z += v.x * v.y; + v.w += v.y * v.z; + v ^= v >> 16u; + v.x += v.y * v.w; + v.y += v.z * v.x; + v.z += v.x * v.y; + v.w += v.y * v.z; + return v; +} + +// four random numbers 0 .. 1 +vec4 water_random(uint a, uint b, uint c, uint d) +{ + return vec4(water_pcg4d(uvec4(a, b, c, d))) * (1.0 / 4294967296.0); +} + +// value noise 0 .. 1 +float water_noise(vec2 p, uint layer) +{ + vec2 i = floor(p); + vec2 f = p - i; + vec2 u = f * f * (3.0 - 2.0 * f); + uvec2 c = uvec2(ivec2(i)); + float a = water_random(c.x, c.y, layer, 0u).x; + float b = water_random(c.x + 1u, c.y, layer, 0u).x; + float d = water_random(c.x, c.y + 1u, layer, 0u).x; + float e = water_random(c.x + 1u, c.y + 1u, layer, 0u).x; + return mix(mix(a, b, u.x), mix(d, e, u.x), u.y); +} + +// the glass at a place of the uv: the steps along it for the steps of u and v (metres), the way out of the cab and whether the +// place is on the glass (1), next to it (0.5) or off it (0) +struct water_frame_s +{ + vec3 tu; + vec3 tv; + vec3 normal; + float valid; +}; + +water_frame_s water_frame(uint slot, vec2 uv) +{ + water_frame_s frame = water_frame_s(vec3(0.0), vec3(0.0), vec3(0.0, 0.0, 1.0), 0.0); + if (any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0)))) + return frame; + uint base = slot * uint(WATER_MAP * WATER_MAP); + vec2 p = uv * float(WATER_MAP) - 0.5; + ivec2 i = ivec2(floor(p)); + vec2 f = p - vec2(i); + float total = 0.0; + float side = 0.0; + for (int k = 0; k < 4; ++k) + { + ivec2 texel = clamp(i + ivec2(k & 1, k >> 1), ivec2(0), ivec2(WATER_MAP - 1)); + uint index = base + uint(texel.y * WATER_MAP + texel.x); + vec4 a = water_axes[index * 2u]; + vec4 b = water_axes[index * 2u + 1u]; + if (a.w <= 0.0) + continue; + float w = (((k & 1) != 0) ? f.x : 1.0 - f.x) * (((k >> 1) != 0) ? f.y : 1.0 - f.y) + 1e-4; + frame.tu += a.xyz * w; + frame.tv += b.xyz * w; + side += b.w * w; + total += w; + } + if (total <= 0.0) + return frame; + frame.tu /= total; + frame.tv /= total; + vec3 n = cross(frame.tu, frame.tv); + float length_n = length(n); + if (length_n < 1e-12) + return frame; + frame.normal = n * (((side < 0.0) ? -1.0 : 1.0) / length_n); + // on the glass by the texel the place is in + ivec2 nearest = clamp(ivec2(floor(uv * float(WATER_MAP))), ivec2(0), ivec2(WATER_MAP - 1)); + frame.valid = water_axes[(base + uint(nearest.y * WATER_MAP + nearest.x)) * 2u].w; + return frame; +} + +// a step along the glass (metres, the space of the cab) as a step of the uv +vec2 water_to_uv(water_frame_s frame, vec3 step) +{ + float a = dot(frame.tu, frame.tu); + float b = dot(frame.tu, frame.tv); + float c = dot(frame.tv, frame.tv); + float det = a * c - b * b; + if (abs(det) < 1e-20) + return vec2(0.0); + vec2 r = vec2(dot(frame.tu, step), dot(frame.tv, step)); + return vec2(c * r.x - b * r.y, a * r.y - b * r.x) / det; +} + +// the blades at a place of the uv, filtered between the texels of the map +vec4 water_blades_at(uint slot, vec2 uv) +{ + uint base = slot * uint(WATER_MAP * WATER_MAP); + vec2 p = clamp(uv, vec2(0.0), vec2(1.0)) * float(WATER_MAP) - 0.5; + ivec2 i = ivec2(floor(p)); + vec2 f = p - vec2(i); + vec4 result = vec4(0.0); + for (int k = 0; k < 4; ++k) + { + ivec2 texel = clamp(i + ivec2(k & 1, k >> 1), ivec2(0), ivec2(WATER_MAP - 1)); + float w = (((k & 1) != 0) ? f.x : 1.0 - f.x) * (((k >> 1) != 0) ? f.y : 1.0 - f.y); + result += water_blades[base + uint(texel.y * WATER_MAP + texel.x)] * w; + } + return result; +} + +// the blades at the texel of the map holding a place of the uv, unfiltered: where a blade reaches it's at least 0.001 (the +// filtered values fall off across a texel at the edge of the sweep) +vec4 water_blades_texel(uint slot, vec2 uv) +{ + ivec2 texel = clamp(ivec2(floor(uv * float(WATER_MAP))), ivec2(0), ivec2(WATER_MAP - 1)); + return water_blades[slot * uint(WATER_MAP * WATER_MAP) + uint(texel.y * WATER_MAP + texel.x)]; +} + +// a free place for a drop of the glass, taken; ~0u when none was found +uint water_claim(uint slot, uint seed) +{ + for (uint k = 0u; k < 8u; ++k) + { + uint place = water_pcg4d(uvec4(seed, k, slot, 77u)).x % uint(WATER_DROPS); + if (atomicCompSwap(water_state[slot * uint(WATER_DROPS) + place], 0u, 1u) == 0u) + return place; + } + return ~0u; +} + +// a new drop of the glass: Volume mm3 at a place, a flake of snow when Melted is below 1. false when the glass is full. the +// landing rain and snow get only nine tenths of the places, so there are always some for the droplets the moving drops leave +bool water_add(uint slot, uint seed, vec2 uv, float volume, float melted, float shape, bool landing) +{ + if ((volume < WATER_LEAST) || any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0)))) + return false; + if (landing && (water_counts_of[slot].x >= uint(WATER_DROPS) / 10u * 9u)) + return false; + uint place = water_claim(slot, seed); + if (place == ~0u) + return false; + atomicAdd(water_counts_of[slot].x, 1u); + uint index = slot * uint(WATER_DROPS) + place; + water_drops[index].place = vec4(uv, volume, 0.0); + water_drops[index].motion = vec4(0.0, 0.0, shape, melted); + water_extra[index] = vec4(0.0, 0.004 * (0.6 + 0.8 * shape), 0.0, 0.0); + water_target[index] = place; + for (uint k = 0u; k < 4u; ++k) + water_gather[index * 4u + k] = 0; + return true; +} + +// the slot of the drop on the glass freed, with what was flowing into it +void water_remove(uint index) +{ + water_drops[index].place.z = 0.0; + water_state[index] = 0u; + atomicAdd(water_counts_of[index / uint(WATER_DROPS)].x, ~0u); + for (uint k = 0u; k < 4u; ++k) + water_gather[index * 4u + k] = 0; +} + +// Volume mm3 of water flowing into a drop (index in the storage) from Offset (uv) away from its middle, taken by +// water_settle.comp +void water_pour(uint index, float volume, vec2 offset) +{ + atomicAdd(water_gather[index * 4u], int(volume * 1e4)); + atomicAdd(water_gather[index * 4u + 1u], int(offset.x * volume * 1e6)); + atomicAdd(water_gather[index * 4u + 2u], int(offset.y * volume * 1e6)); +} + +// the drop lying at a place of the glass which water of Radius (metres) landing there touches, the largest of them, by the grid +// of the step before (~0u: none) +uint water_landing_on(uint slot, water_frame_s frame, vec2 uv, float radius) +{ + uint cell = water_cell(uv); + uint base = water_grid_base(slot); + uint count = min(water_cells[base + cell], uint(WATER_CELL_DROPS)); + uint best = ~0u; + float most = 0.0; + for (uint k = 0u; k < count; ++k) + { + uint other = water_cells[base + uint(WATER_GRID * WATER_GRID) + cell * uint(WATER_CELL_DROPS) + k]; + water_drop_s drop = water_drops[slot * uint(WATER_DROPS) + other]; + if ((drop.place.z <= most) || (drop.motion.w < 1.0)) + continue; + vec2 step = drop.place.xy - uv; + if (length(frame.tu * step.x + frame.tv * step.y) < 0.9 * (radius + water_radius(drop.place.z))) + { + best = other; + most = drop.place.z; + } + } + return best; +} + +// water landing on the glass: it flows into a drop it touches, else it's a new drop where it's large enough to be seen +void water_land(uint slot, uint seed, water_frame_s frame, vec2 uv, float volume, float shape) +{ + if (any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0)))) + return; + uint onto = water_landing_on(slot, frame, uv, water_radius(volume)); + if (onto != ~0u) + { + uint index = slot * uint(WATER_DROPS) + onto; + water_pour(index, volume, uv - water_drops[index].place.xy); + } + else if (volume >= WATER_SEEN) + { + water_add(slot, seed, uv, volume, 1.0, shape, true); + } +} + +#endif diff --git a/shaders/ogl46/water_spawn.comp b/shaders/ogl46/water_spawn.comp new file mode 100644 index 00000000..fbc4bf1a --- /dev/null +++ b/shaders/ogl46/water_spawn.comp @@ -0,0 +1,65 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): the rain or the snow landing on the glasses in the step. a thread per landing +// tried, a row of groups per glass: a place of the uv picked at random is taken as often as the glass there gets hit (the +// metres squared the place holds, how much it faces the rain coming at it), so the landings go evenly over the glass. a raindrop +// spreads and throws a splash of droplets around, along the way it came, which flow into the drops they land on (water_settle.comp +// takes the water); a flake sticks where it lands + +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +void main() +{ + uint slot = gl_WorkGroupID.y; + uint landing = gl_GlobalInvocationID.x; + if ((water_slots[slot].z < 0.5) || (float(landing) >= water_slots[slot].x)) + return; + vec4 r = water_random(water_counts.x, slot, landing, 0u); + vec2 uv = r.xy; + water_frame_s frame = water_frame(slot, uv); + if (frame.valid < 0.75) + return; + // the rain coming at the glass, against the cab + vec3 coming = water_air.xyz - water_up.xyz * water_air.w; + float speed = length(coming); + if (speed < 1e-3) + return; + float facing = max(-dot(coming, frame.normal), 0.0) / speed; + float density = length(cross(frame.tu, frame.tv)); + if (r.z >= density * water_slots[slot].y * facing) + return; + float scale = max(water_slots[slot].w, 0.05); + vec4 s = water_random(water_counts.x, slot, landing, 1u); + uint seed = water_pcg4d(uvec4(water_counts.x, slot, landing, 2u)).x; + if (water_step.w > 0.5) + { + // a flake, a few mm of snow holding a little water + water_add(slot, seed, uv, mix(0.3, 2.5, s.x) * scale * scale * scale, 0.0, s.y, true); + return; + } + // the diameter of the raindrop, mm: above the least landing as a drop of its own they go as Marshall-Palmer (exponential) + float diameter = min(water_rain.x - log(max(r.w, 1e-6)) / max(water_rain.y, 0.1), 5.0) * scale; + float drop = 0.5236 * diameter * diameter * diameter; + // most of it stays where it hit, the rest is thrown around as droplets, forward along the glass + float kept = mix(0.55, 0.85, s.x); + water_land(slot, seed, frame, uv, drop * kept, s.y); + vec3 along = coming - frame.normal * dot(coming, frame.normal); + float glancing = length(along) / speed; + vec3 forward = (glancing > 1e-3) ? normalize(along) : normalize(frame.tu); + vec3 across = cross(frame.normal, forward); + float radius = water_radius(drop * kept); + int splash = 2 + int(s.z * 4.0); + for (int k = 0; k < splash; ++k) + { + vec4 e = water_random(water_counts.x, slot, landing, 3u + uint(k)); + float angle = e.x * 6.2831853; + vec3 way = cos(angle) * forward + sin(angle) * across + forward * (1.5 * glancing); + way = (length(way) > 1e-3) ? normalize(way) : forward; + float distance = radius * (1.3 + 2.5 * e.y) * (1.0 + glancing); + float volume = drop * (1.0 - kept) / float(splash) * mix(0.4, 1.6, e.z); + water_land(slot, seed + 101u * uint(k + 1), frame, uv + water_to_uv(frame, way * distance), volume, e.w); + } +} diff --git a/shaders/ogl46/water_trail.comp b/shaders/ogl46/water_trail.comp new file mode 100644 index 00000000..e6823424 --- /dev/null +++ b/shaders/ogl46/water_trail.comp @@ -0,0 +1,34 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): the blades going over the wetted paths in the step wipe them. a thread per texel +// of the map of the paths, a row of groups per glass + +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +void main() +{ + uint slot = gl_WorkGroupID.y; + uint texel = gl_GlobalInvocationID.x; + if ((water_slots[slot].z < 0.5) || (texel >= uint(WATER_TRAIL * WATER_TRAIL))) + return; + uint index = water_trail_base(slot) + texel; + if (water_cells[index] == 0u) + return; + vec2 uv = (vec2(float(texel % uint(WATER_TRAIL)), float(texel / uint(WATER_TRAIL))) + 0.5) / float(WATER_TRAIL); + vec4 mask = water_blades_at(slot, uv); + for (int k = 0; k < 4; ++k) + { + float before = water_blades_before[k]; + float now = water_blades_now[k]; + if ((mask[k] < 0.001) || (before < 0.0) || (now < 0.0) || (before == now)) + continue; + if ((mask[k] > min(before, now)) && (mask[k] <= max(before, now))) + { + water_cells[index] = 0u; + return; + } + } +} diff --git a/shaders/vk14/common.glsl b/shaders/vk14/common.glsl index 882dfe02..0fc5ae11 100644 --- a/shaders/vk14/common.glsl +++ b/shaders/vk14/common.glsl @@ -60,7 +60,8 @@ #define HEADLIGHT_SHADOW_TEX 23 #define SCENE_DEPTH_TEX 24 #define SSR_HISTORY_TEX 25 -#define PASS_BINDING_COUNT 26 +#define WATER_SSBO 26 +#define PASS_BINDING_COUNT 27 #define MATERIAL_TEXTURE_FIRST 0 #define CL_CLUSTER_UBO 0 #define CL_AABB_SSBO 1 @@ -121,6 +122,13 @@ #define SHADOW_FIT_GROUP_SIZE 16 #define LOCAL_SHADOW_NEAR 0.150000 #define PARTICLE_GROUP_SIZE 64 +#define WATER_SLOTS 4 +#define WATER_DROPS 65536 +#define WATER_GRID 128 +#define WATER_CELL_DROPS 32 +#define WATER_TRAIL 1024 +#define WATER_MAP 256 +#define WATER_GROUP_SIZE 64 #endif #include "settings.glsl" @@ -271,6 +279,8 @@ layout(std140, set = PASS_SET, binding = SCENE_UBO) uniform scene_ubo vec4 ssr_params; vec4 ssr_params2; vec4 env_params; + // the air against the vehicle of the camera, see gl::scene_ubs + vec4 rain_wind; }; #ifndef LAYOUT_COMPUTE diff --git a/shaders/vk14/mat_rain_windscreen.frag b/shaders/vk14/mat_rain_windscreen.frag index 341620de..4cc783f2 100644 --- a/shaders/vk14/mat_rain_windscreen.frag +++ b/shaders/vk14/mat_rain_windscreen.frag @@ -1,8 +1,30 @@ #version 460 core #define STAGE_FRAGMENT 1 #include "common.glsl" -// Windshield rain effects -// by @lcddisplay; wiper movement @MichauSto +// Windshield rain and snow +// by @lcddisplay; wiper movement @MichauSto; water on the glass rewritten as a model of drops lying on it +// +// the material: the textures diffuse (the glass) and wipermask (a blade per channel, r g b a: 0 where it doesn't reach, elsewhere +// the place of the blade over its sweep, 0 at rest .. 1 at the far end, at which it goes over the place). raindropsatlas and +// raindrop_grid_size aren't used any more, they stay so the materials which name them still load. rain_scale (1 when not given) +// scales the drops and the flakes. water_slot is given by the renderer (1 + the glass of the simulation, 0 none), not by the +// material. the block of the scene: rain_params (x: how heavy the rain or snow is, z: 1 for snow, w: temperature of the air), +// wiper_pos, wiper_timer_out, wiper_timer_return (the blades of the vehicle of the camera) and rain_wind (the air against it). +// +// the glasses of the cab of the train are simulated (gfx::windscreen_water, water_*.comp, water.glsl): the raindrops land as often +// as the rain hits the glass, splashing; the drops lie held by surface tension until gravity and the drag of the air pull them +// harder, then slide in stops and goes, wetting their paths (the next ones follow them into rivulets) and leaving droplets behind; +// the drops which touch flow together; the blades take the water along, gather it into a bead and leave it at the ends of their +// sweep, where it runs down; the flakes stick and melt. here the drops of the simulation are drawn where the fragment is, and the +// wetted paths as a thin film. elsewhere (the windows of other vehicles, a glass the simulation has no room for) the water is made +// up from the time alone: drops landing, growing and sliding along tracks, the blades pushing a ridge and leaving the water at the +// edge of their sweep. +// +// the water is a field of soft kernels (metaballs): drops coming close flow together, and a drop is a few kernels, so the drops +// are of many shapes. a drop is a lens holding a sharp picture of the place around it turned over (the image of the last frame +// where the screen holds it, the sky and the ground elsewhere), its edge shows the light of the sky reflected inside it; the lamps +// and the headlights light the water up; the film the blades leave breaks up behind them and the lights seen through it are drawn +// into lines towards their pivot; the wet glass is a little milky layout(location = 0) in vec3 f_normal; layout(location = 2) in vec2 f_coord; @@ -26,191 +48,1170 @@ layout(location = 1) out vec4 out_motion; #pragma param(reflection, 1, 2, 1, zero) #pragma param(glossiness, 1, 3, 1, glossiness) #pragma param(raindrop_grid_size, 2, 0, 1, one) +#pragma param(rain_scale, 2, 1, 1, one) #include "light_common.glsl" #include "apply_fog.glsl" #include "tonemapping.glsl" -#include "random.glsl" +#include "water.glsl" layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 0) uniform sampler2D diffuse; layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 1) uniform sampler2D raindropsatlas; layout(set = MATERIAL_SET, binding = MATERIAL_TEXTURE_FIRST + 2) uniform sampler2D wipermask; -const float specular_intensity = 1.0; -const float wobble_strength = 0.002; -const float wobble_speed = 30.0; +const float RW_PI = 3.14159265; +// refractive index of water +const float RW_IOR = 1.333; +// the falling particles (particles_precipitation.comp, rendering/common/gpuparticles.cpp): the diameters of the flakes, metres, +// and how fast the rain and the snow fall, m/s +const vec2 RW_SNOW_DIAMETER = vec2(0.004, 0.012); +const float RW_RAIN_FALL = 7.0; +const float RW_SNOW_FALL = 1.1; +// a drop is a lens: it holds a sharp picture of the place around it, turned over through its middle, RW_LENS times as far out +// as the fragment is from the middle (and as much smaller), with this share of the bending of the view by the surface on top +const float RW_LENS = 2.0; +const float RW_LENS_BEND = 0.15; +// the drag of the air going along the glass: the speed at which it equals the weight of a drop, m/s; and the share of the air +// meeting the glass head on which turns up along it. so the water goes up a glass the sooner the more it leans back: on a +// vertical glass at about 250 km/h, leaning 20 degrees at about 130 km/h, 45 degrees at about 80 km/h +const float RW_DRAG_SPEED = 24.0; +const float RW_HEAD_ON = 0.35; +// the water is a field summed from soft kernels (metaballs) and its surface is where the field is RW_SURFACE: drops which come +// close flow together the way surface tension pulls them. a kernel alone reaches the surface at RW_EDGE of its radius +const float RW_SURFACE = 0.25; +const float RW_EDGE = 0.61; -vec4 getDropTex(float choice, vec2 uv) { - vec2 offset; - if (choice < 0.25) offset = vec2(0.0, 0.0); - else if (choice < 0.5) offset = vec2(0.5, 0.0); - else if (choice < 0.75) offset = vec2(0.0, 0.5); - else offset = vec2(0.5, 0.5); - return texture(raindropsatlas, offset + uv * 0.5); +// the glass at the fragment: the view space steps along it for a step of the coordinates of the glass (metres along the axes +// of the model, see main()), and the size of a pixel on it, metres +vec3 rw_tu; +vec3 rw_tv; +float rw_pixel; + +// how the water on the glass behaves here, set by main() +float rw_scale; // size of the drops (param rain_scale) +float rw_since; // seconds since a blade last cleaned the place +float rw_period; // seconds between the small drops landing on a place of one +float rw_grow; // seconds the drops need after a wipe to grow big enough to slide +float rw_rate; // how fast the glass gathers the rain against a standing vertical glass in the heaviest rain +float rw_push; // the force on a drop along the glass, in units of its weight +vec2 rw_down; // the way gravity pulls along the glass (coordinates of the glass, unit) +vec2 rw_flow; // the way the air goes along the glass (unit, 0 in still air) + +// the field of the water: the sum of the kernels, its gradient along the glass (view space) and the sums for the mean radius of +// the drops it is made of +float rw_field = 0.0; +vec3 rw_fgrad = vec3(0.0); +float rw_fsize = 0.0; +float rw_fweight = 0.0; + +// the water at the fragment: the thickness of the highest water there (metres), its gradient (view space), the radius of the +// curvature of its surface (metres: how much it bends the view across a pixel) and how much of the pixel it covers +float rw_height = 0.0; +vec3 rw_slope = vec3(0.0); +float rw_curve = 1.0; +float rw_cover = 0.0; +// from the middle of the drop which gives the water to the fragment (view space, metres), 0 for spread water +vec3 rw_offset = vec3(0.0); +// the last time the water a blade pushes went over the fragment, taking the drops lying there with it +float rw_cleared = -1.0e9; + +uvec4 rw_pcg4d(uvec4 v) +{ + v = v * 1664525u + 1013904223u; + v.x += v.y * v.w; + v.y += v.z * v.x; + v.z += v.x * v.y; + v.w += v.y * v.z; + v ^= v >> 16u; + v.x += v.y * v.w; + v.y += v.z * v.x; + v.z += v.x * v.y; + v.w += v.y * v.z; + return v; } -float GetMixFactor(in vec2 co, out float side); - -void main() { - vec4 tex_color = texture(diffuse, f_coord); - if (tex_color.a < 0.01) discard; - - vec2 rainCoord = f_coord; - float gridSize = ceil(param[2].x); - - const float numDrops = 20000.0; - const float cycleDuration = 4.0; - - float squareMin = 0.5 / gridSize; - float squareMax = 1.2 / gridSize; - - vec2 cell = floor(rainCoord * gridSize); - - vec3 dropLayer = vec3(0.0); - float dropMaskSum = 0.0; - - // Grid of 9 droplets in immediate neighbourhood - for (int oy = -1; oy <= 1; oy++) { - for (int ox = -1; ox <= 1; ox++) { - - vec2 neighborCell = cell + vec2(ox, oy); - vec2 neighborCenter = (neighborCell + .5) / gridSize; - - float side; - float mixFactor = GetMixFactor(neighborCenter, side); - - uint seed = Hash(uvec3(neighborCell, side)); - - if(mixFactor < RandF(seed)) { - continue; - } - - // Show a percentage of droplets given by rain intensity param - float activationSeed = RandF(seed); - if (activationSeed > rain_params.x) - continue; // kropla nieaktywna - - // Randomly modulate droplet center & size - vec2 dropCenter = (neighborCell + vec2(RandF(seed), RandF(seed))) / gridSize; - float squareSize = mix(squareMin, squareMax, RandF(seed)); - - float lifeTime = time + RandF(seed) * cycleDuration; - float phase = fract(lifeTime / cycleDuration); - float actiwe = clamp(1.0 - phase, 0.0, 1.0); - - // Gravity influence (TODO add vehicle speed & wind here!) - float gravityStart = 0.5; - float gravityPhase = smoothstep(gravityStart, 1.0, phase); - float dropMass = mix(0.3, 1.2, RandF(seed)); - float gravitySpeed = 0.15 * dropMass; - vec2 gravityOffset = vec2(0.0, gravityPhase * gravitySpeed * phase); - - // Random wobble - bool hasWobble = (RandF(seed) < 0.10); - vec2 wobbleOffset = vec2(0.0); - if (hasWobble && gravityPhase > 0.0) { - float intensity = sin(time * wobble_speed + RandF(seed) * 100.) * wobble_strength * gravityPhase; - wobbleOffset = vec2(intensity, 0.0); - } - - vec2 slideOffset = gravityOffset + wobbleOffset; - - // Flatten droplets influenced by gravity - float flattenAmount = smoothstep(0.1, 0.5, gravityPhase); - float flattenX = mix(1.0, 0.4, flattenAmount); - float stretchY = mix(1.0, 1.6, flattenAmount); - - // Droplet local position & mask - vec2 diff = (rainCoord + slideOffset) - dropCenter; - diff.x *= 1.0 / flattenX; - diff.y *= 1.0 / stretchY; - float mask = smoothstep(squareSize * 0.5, squareSize * 0.45, max(abs(diff.x), abs(diff.y))); - - if (mask > 0.001) { - vec2 localUV = (diff + squareSize * 0.5) / squareSize; - float choice = RandF(seed); - vec4 dropTex = getDropTex(choice, localUV); - float sharpAlpha = smoothstep(0.3, 0.9, dropTex.a); - - float ambLum = clamp(dot(ambient, vec3(0.2126, 0.7152, 0.0722)), 0.0, 1.0); - float sunFactor = pow(clamp(ambLum * 6.0, 0.0, 1.0), 0.5); - float dynBright = mix(0.0, 1.0, sunFactor); - - float colorLuma = length(dropTex.rgb); - float alphaRange = smoothstep(0.1, 0.3, colorLuma); - float blackAlpha = mix(0.25, 0.85, alphaRange); - - float sparkle = 0.0; - if (hasWobble) { - sparkle = pow(abs(sin(f_coord.x * 8.0 + time * 2.0 + RandF(seed) * 6.2831)), 40.0) - * mix(0.2, 1.0, sunFactor); - } - - vec3 specularColor = vec3(1.0) * specular_intensity * sparkle; - vec3 dropLit = dropTex.rgb * dynBright + specularColor * sharpAlpha; - - dropLayer += dropLit * sharpAlpha * actiwe * blackAlpha * mask; - dropMaskSum += sharpAlpha * actiwe * blackAlpha * mask; - } - } - } - vec3 finalMix = dropLayer; - float alphaOut = clamp(dropMaskSum, 0.0, 1.0); - out_color = vec4(finalMix, alphaOut); - - { // Overlay windshield texture with alpha - vec3 fragcolor = ambient; - vec3 fragnormal = normalize(f_normal); - float reflectivity = param[1].z; - float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; - legacy_gloss(); - - fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); - vec4 color = vec4(fragcolor, tex_color.a * alpha_mult); - - out_color.xyz = apply_fog(mix(out_color.xyz, color.xyz, color.a)); - out_color.a = mix(out_color.a, 1., color.a); - } - - if (MOTIONBLUR_ENABLED) - { - out_motion = vec4(0.0); - } +// four numbers 0 .. 1 of a cell of a layer +vec4 rw_random(ivec2 cell, uint layer, uint index) +{ + return vec4(rw_pcg4d(uvec4(uvec2(cell), layer, index))) * (1.0 / 4294967296.0); } -float GetMixFactor(in vec2 co, out float side) { - vec4 movePhase = wiper_pos; - bvec4 is_out = lessThanEqual(movePhase, vec4(1.)); - movePhase = mix(vec4(2.) - movePhase, movePhase, is_out); - - vec4 mask = texture(wipermask, co); - - vec4 areaMask = step(.001, mask); - - vec4 maskVal = mix(1. - mask, mask, is_out); - vec4 wipeWidth = smoothstep(1., .9, movePhase) * .25; - vec4 cleaned = smoothstep(movePhase - wipeWidth, movePhase, maskVal) * areaMask; - vec4 side_v = step(maskVal, movePhase); - cleaned *= side_v; - side_v = mix(side_v, vec4(1.) - side_v, is_out); - - // "regeneration", raindrops gradually returning after wiper pass: - vec4 regenPhase = clamp((vec4(time) - mix(wiper_timer_out, wiper_timer_return, side_v) - vec4(.2)) / vec4(rain_params.y), vec4(0.), vec4(1.)); - - side_v = mix(vec4(0.), vec4(1.) - side_v, areaMask); - - vec4 factor_v = mix(vec4(1.), regenPhase * (vec4(1.) - cleaned), areaMask); - - side = 0.; - float out_factor = 1.; - - // Find out the wiper blade that influences given grid cell the most - for(int i = 0; i < 4; ++i) - { - bool is_candidate = factor_v[i] < out_factor; - out_factor = mix(out_factor, factor_v[i], is_candidate); - side = mix(side, side_v[i], is_candidate); - } - - return out_factor; +float rw_noise(float x, uint layer) +{ + float i = floor(x); + float f = x - i; + float a = rw_random(ivec2(int(i), 0), layer, 7u).x; + float b = rw_random(ivec2(int(i) + 1, 0), layer, 7u).x; + return mix(a, b, f * f * (3.0 - 2.0 * f)); +} + +// value noise 0 .. 1 (x) with its gradient (yz) +vec3 rw_noise2(vec2 p, uint layer) +{ + vec2 i = floor(p); + vec2 f = p - i; + vec2 u = f * f * (3.0 - 2.0 * f); + vec2 du = 6.0 * f * (1.0 - f); + ivec2 c = ivec2(i); + float a = rw_random(c, layer, 8u).x; + float b = rw_random(c + ivec2(1, 0), layer, 8u).x; + float d = rw_random(c + ivec2(0, 1), layer, 8u).x; + float e = rw_random(c + ivec2(1, 1), layer, 8u).x; + float k1 = b - a; + float k2 = d - a; + float k4 = a - b - d + e; + return vec3(a + k1 * u.x + k2 * u.y + k4 * u.x * u.y, du * vec2(k1 + k4 * u.y, k2 + k4 * u.x)); +} + +// P turned by A (radians). every set of drops is laid on its own grid, turned and shifted against the others, so the drops line +// up neither with each other nor along the glass +vec2 rw_turn(vec2 p, float a) +{ + float c = cos(a); + float s = sin(a); + return vec2(c * p.x - s * p.y, s * p.x + c * p.y); +} + +// a gradient given along the coordinates of the glass, view space +vec3 rw_along(vec2 gradient) +{ + return rw_tu * (gradient.x / dot(rw_tu, rw_tu)) + rw_tv * (gradient.y / dot(rw_tv, rw_tv)); +} + +// the gradient along the glass (view space) of a value of the fragments, from how it changes from pixel to pixel; Dpdx, Dpdy: +// the steps of the position, Normal: of the glass +vec3 rw_gradient(float value, vec3 dpdx, vec3 dpdy, vec3 normal) +{ + vec3 r1 = cross(dpdy, normal); + vec3 r2 = cross(normal, dpdx); + float det = dot(dpdx, r1); + float dx = dFdx(value); + float dy = dFdy(value); + return (abs(det) > 1e-14) ? (dx * r1 + dy * r2) / det : vec3(0.0); +} + +// a kernel of the water, Offset (view space) from its centre to the fragment: alone it makes water of the radius Visible, +// Weight 1 .. 0 as it goes. water smaller than a pixel would only flicker, it is left to the haze of the wet glass +void rw_kernel(vec3 offset, float visible, float weight) +{ + if ((weight <= 0.0) || (visible < 0.5 * rw_pixel)) + return; + // a drop a pixel or two across is grain rather than a lens: it fades into the haze of the wet glass + weight *= smoothstep(0.5 * rw_pixel, 2.0 * rw_pixel, visible); + float support = visible / RW_EDGE; + float q = dot(offset, offset) / (support * support); + if (q >= 1.0) + return; + float k = 1.0 - q; + float k3 = weight * k * k * k; + rw_field += k3; + rw_fgrad += offset * (-6.0 * weight * k * k / (support * support)); + rw_fsize += k3 * visible; + rw_fweight += k3; +} + +// a kernel at the Offset in the coordinates of the glass +void rw_blob(vec2 offset, float visible, float weight) +{ + rw_kernel(offset.x * rw_tu + offset.y * rw_tv, visible, weight); +} + +// a drop lying on the glass: a few kernels, sagging down the glass and lopsided as Shape (random 0 .. 1) says +void rw_drop(vec2 offset, float visible, float weight, vec4 shape) +{ + rw_blob(offset, visible, weight); + if (visible < 1.5 * rw_pixel) + return; + vec2 side = vec2(-rw_down.y, rw_down.x); + vec2 sag = (rw_down * (0.2 + 0.35 * shape.x) + side * ((shape.y - 0.5) * 0.7)) * visible; + rw_blob(offset - sag, visible * mix(0.6, 0.85, shape.z), weight); + if (shape.w > 0.4) + { + float angle = shape.w * 40.0; + rw_blob(offset - vec2(cos(angle), sin(angle)) * (0.6 * visible), visible * mix(0.4, 0.6, shape.x), weight); + } +} + +// water spread over the glass (a film, a ridge): its thickness, the gradient of it (view space), the radius of the curvature of +// its surface and how much of the pixel it covers +void rw_sheet(float thickness, vec3 slope, float curve, float cover) +{ + if (cover <= 0.0) + return; + rw_cover = max(rw_cover, cover); + if (thickness > rw_height) + { + rw_height = thickness; + rw_slope = slope; + rw_curve = curve; + rw_offset = vec3(0.0); + } +} + +// the surface of the water of the field: a drop is a cap of a sphere, flat lens of the contact angle Contact (radians) the water +// makes with the glass. how far across a drop the fragment is follows from the field (inverting the kernel), the height and +// the slope of the cap from that: the slope rises slowly from the middle and steeply only close to the edge, where it reaches +// the contact angle; where drops flowed together the field is above that of a drop alone and the water is flat. the edge is +// antialiased by how fast the field changes across a pixel +void rw_settle(float contact) +{ + if (rw_field <= 0.5 * RW_SURFACE) + return; + float gradient = length(rw_fgrad); + float cover = clamp((rw_field - RW_SURFACE) / max(gradient * rw_pixel, 1e-3) + 0.5, 0.0, 1.0); + if (cover <= 0.0) + return; + float mean = rw_fsize / max(rw_fweight, 1e-12); + // 0 in the middle of a drop, 1 at its edge + float across = min(sqrt(max(1.0 - pow(min(rw_field, 1.0), 1.0 / 3.0), 0.0)) / RW_EDGE, 1.0); + float s = sin(contact); + float rs = across * s; + float root = sqrt(max(1.0 - rs * rs, 1e-4)); + float sphere = mean / s; + vec3 slope = (gradient > 1e-9) ? rw_fgrad * ((rs / root) / gradient) : vec3(0.0); + float height = max(sphere * (root - cos(contact)), 0.0); + bool top = height > rw_height; + rw_sheet(height, slope, sphere, cover); + // the field rises towards the middle of the drop + if (top && (gradient > 1e-9)) + rw_offset = rw_fgrad * (-across * mean / gradient); +} + +// the blades. a channel of the mask of the wipers is a blade: 0 where it doesn't reach, elsewhere the place of the blade over its +// sweep (0 at rest .. 1 at the far end) at which it goes over the place. wiper_pos: where the blade is (0 .. 1 on its way out, +// 1 + where it is on its way back), wiper_timer_out / _return: the last time it lay at its start / was at its far end +struct rw_wipe_s +{ + float since; // seconds since a blade last went over the place (large where none reaches) + float ahead; // 0 .. 1, how close in front of a moving blade the place is (the water the blade pushes) + float front; // how far in front of the nearest moving blade the place is, units of the mask (1 where none) + float swept; // 0 .. 1, how much of its way the blade in front did, the water it gathered on the way + float working; // 0 .. 1, the blades went lately +}; + +rw_wipe_s rw_wipe(vec4 mask) +{ + rw_wipe_s wipe = rw_wipe_s(1.0e4, 0.0, 1.0, 0.0, 0.0); + for (int i = 0; i < 4; ++i) + { + float m = mask[i]; + if (m < 0.001) + continue; + float pos = wiper_pos[i]; + bool back = pos > 1.0; + float blade = back ? pos - 1.0 : pos; + float t0 = wiper_timer_out[i]; + float t1 = wiper_timer_return[i]; + // when the blade went over the place last: on this stroke, the blade moving at an even pace from its end, or on the + // stroke before, which took as long as the time between the ends (if the blade lay at its end longer, two seconds) + float pass; + float front; // how far in front of the blade the place is (units of the mask), below 0 behind it + if (back) + { + pass = (m >= blade) ? t1 + (time - t1) * ((1.0 - m) / max(1.0 - blade, 1e-3)) : t0 + m * clamp(t1 - t0, 0.0, 2.0); + front = blade - m; + } + else + { + pass = (m <= blade) ? t0 + (time - t0) * (m / max(blade, 1e-3)) : t1 + (1.0 - m) * clamp(t0 - t1, 0.0, 2.0); + front = m - blade; + } + wipe.since = min(wipe.since, max(time - pass, 0.0)); + float moving = smoothstep(0.0, 0.03, blade) * smoothstep(1.0, 0.97, blade); + float ahead = (front > 0.0) ? (1.0 - smoothstep(0.0, 0.035, front)) * moving : 0.0; + wipe.ahead = max(wipe.ahead, ahead); + if ((front > 0.0) && (moving > 0.5)) + wipe.front = min(wipe.front, front); + wipe.swept = max(wipe.swept, ahead * (back ? 1.0 - blade : blade)); + float working = 1.0 - smoothstep(4.0, 30.0, time - t1); + wipe.working = max(wipe.working, working); + } + return wipe; +} + +// a place of the glass a blade reaches +bool rw_reached(vec2 coords) +{ + return any(greaterThanEqual(textureLod(wipermask, clamp(coords, vec2(0.0), vec2(1.0)), 0.0), vec4(0.001))); +} + +// the edge of the sweep of the blades, where they push the water out to: 0 where a blade reaches, else the share of the places +// around at Reach (metres along the glass) which a blade reaches. found from where the blades reach and not from the values of +// the mask, which may stay at the end of the sweep over a wide part of the glass. Maskstep: the coordinates of the mask for a +// step along the glass +float rw_rim(vec2 coords, float reach, mat2 maskstep) +{ + if (rw_reached(coords)) + return 0.0; + float inside = 0.0; + for (int k = 0; k < 6; ++k) + { + float angle = float(k) * (RW_PI / 3.0); + inside += rw_reached(coords + maskstep * (vec2(cos(angle), sin(angle)) * reach)) ? 1.0 : 0.0; + } + return inside / 6.0; +} + +// whether the edge of the sweep of the blades is within Reach of the place +bool rw_border(vec2 coords, float reach, mat2 maskstep) +{ + bool here = rw_reached(coords); + for (int k = 0; k < 6; ++k) + { + float angle = float(k) * (RW_PI / 3.0); + if (rw_reached(coords + maskstep * (vec2(cos(angle), sin(angle)) * reach)) != here) + return true; + } + return false; +} + +// how far along its way a sliding drop is at the share T of the time it takes: it stops and goes, the more smoothly the harder +// it is pushed +float rw_stickslip(float t, float seed) +{ + t = clamp(t, 0.0, 1.0); + float s = t * 3.0; + float i = floor(s); + float stuck = (i + smoothstep(0.25 + 0.4 * seed, 1.0, s - i)) / 3.0; + return mix(stuck, t, smoothstep(0.8, 2.0, rw_push)); +} + +// the track of the water down a column of the glass (Column: its index, Width: of a column, Y: metres up the way): where it +// is across, metres. the water keeps to the way wetted before, so every drop of a column goes the same way, wavy, with kinks +// where the glass held it +float rw_path(int column, float y, float width, uint layer) +{ + float slow = rw_noise(y / (0.05 * rw_scale) + float(column) * 7.31, layer) - 0.5; + float k = y / (0.008 * rw_scale); + float i = floor(k); + float a = rw_random(ivec2(column, int(i)), layer + 500u, 3u).x; + float b = rw_random(ivec2(column, int(i) + 1), layer + 500u, 3u).x; + float kinks = mix(a, b, smoothstep(0.15, 0.85, k - i)) - 0.5; + return (float(column) + 0.5 + 0.4 * slow + 0.2 * kinks) * width; +} + +// the drops which grew big enough to slide, and the streams they leave: Q the place across and up the way they go (Side, Down: +// those directions in the coordinates of the glass), Share of them going this way. a column of the glass carries the water of +// a track; a drop starts somewhere in a cell of the column now and then, slides down with stops, thinning, and leaves a stream +// which drains into beads and dries +void rw_sliders(vec2 q, vec2 side, vec2 down, uint layer, float share) +{ + float width = 0.04 * rw_scale; + float height = 0.15 * rw_scale; + int column = int(floor(q.x / width)); + int row = int(floor(q.y / height)); + vec4 a = rw_random(ivec2(column, 7), layer, 0u); + if (a.x > share * mix(0.35, 0.9, clamp(rw_rate / 3.0, 0.0, 1.0))) + return; + float speed = clamp(0.035 + 0.06 * (rw_push - 0.4) + 0.3 * max(rw_push - 1.5, 0.0), 0.01, 0.8); + float period = clamp(25.0 / rw_rate, 3.0, 60.0); + float trail = 0.0; + for (int r = 0; r < 3; ++r) + { + ivec2 cell = ivec2(column, row + r); + vec4 b = rw_random(cell, layer, 1u); + float span = period * mix(0.7, 1.3, b.x); + float now = floor(time / span + b.y); + for (int k = 0; k < 2; ++k) + { + float cycle = now - float(k); + float start = (cycle - b.y) * span; + float t = time - start; + // the drop gathered from the drops landed since the last wipe + if ((t < 0.0) || (rw_since - t < rw_grow)) + continue; + vec4 c = rw_random(cell, layer + 1000u, uint(int(cycle) + 1000000)); + float top = (float(cell.y) + 0.8 - 0.6 * c.x) * height; + float travel = mix(0.06, 0.28, c.y) * rw_scale; + float v = speed * mix(0.7, 1.3, c.z); + float gather = 0.6; + float moved = travel * rw_stickslip((t - gather) * v / travel, c.w); + float y = top - moved; + float radius = mix(0.003, 0.0042, c.w) * rw_scale * (1.0 - 0.35 * moved / travel) * mix(0.6, 1.0, smoothstep(0.0, gather, t)); + // at the end of its way what is left of it stays and flows out into the stream + float body = 1.0 - smoothstep(0.0, 1.5, t - gather - travel / v); + if ((body > 0.0) && (abs(q.y - y) < 3.5 * radius)) + { + vec2 o = vec2(q.x - rw_path(column, y, width, layer), q.y - y); + rw_blob(side * o.x - down * o.y, radius, body); + // its rear up the stream: a corner, drawn out into a cusp the faster it goes (the front stays round) + float drawn = clamp(v * 5.0, 0.0, 1.5); + for (int k = 1; k <= 2; ++k) + { + float yt = y + radius * (0.45 + 0.5 * drawn) * float(k); + vec2 ot = vec2(q.x - rw_path(column, yt, width, layer), q.y - yt); + rw_blob(side * ot.x - down * ot.y, radius * ((k == 1) ? 0.7 : 0.42), body); + } + } + if ((moved > 0.0) && (q.y > y) && (q.y < top)) + { + float age = t - gather - (top - q.y) / v; + if ((age >= 0.0) && (age < rw_since)) + { + // fresh, the stream is whole; draining, it breaks into beads, which dry + float beads = rw_noise(q.y / (0.004 * rw_scale), layer + 9u); + float wet = mix(1.15, 0.2 + 1.0 * beads, smoothstep(0.0, 15.0, age)) * (1.0 - smoothstep(30.0, 60.0, age)); + trail = max(trail, wet); + } + } + } + } + if (trail > 0.0) + { + float across = q.x - rw_path(column, q.y, width, layer); + float half_width = mix(0.0005, 0.0011, a.y) * rw_scale * (0.6 + 0.8 * rw_noise(q.y / (0.006 * rw_scale), layer + 11u)); + rw_blob(side * across, half_width, trail); + } +} + +// a drop lying on the glass, as rw_drop() with the directions along the glass given in the view space (Down: the way gravity +// pulls along the glass, Side across it) +void rw_drop_view(vec3 offset, float visible, float weight, vec4 shape, vec3 down, vec3 side) +{ + rw_kernel(offset, visible, weight); + if (visible < 1.5 * rw_pixel) + return; + vec3 sag = (down * (0.2 + 0.35 * shape.x) + side * ((shape.y - 0.5) * 0.7)) * visible; + rw_kernel(offset - sag, visible * mix(0.6, 0.85, shape.z), weight); + if (shape.w > 0.4) + { + float angle = shape.w * 40.0; + rw_kernel(offset - (cos(angle) * side + sin(angle) * down) * (0.6 * visible), visible * mix(0.4, 0.6, shape.x), weight); + } +} + +// the water the simulation keeps on the glass (gfx::windscreen_water, water.glsl), where the material is a glass of the cab of the +// train: the drops listed by the cell of the grid the fragment is in. Slot: of the glass, Uv: the coordinates of the fragment, +// Puv: the view space step along the glass for a step of the uv; Down, Side: along the glass. a sliding drop is drawn out behind +// itself into a cusp, the faster the longer; one lying is lopsided and sags down the glass; a flake of snow is lace, grey and +// clear as it melts (Snowcover, Wet: as the flakes of rw_snow()) +void rw_simulated(uint slot, vec2 uv, mat2x3 puv, vec3 down, vec3 side, inout float snowcover, inout float wet) +{ + uint base = water_grid_base(slot); + uint cell = water_cell(uv); + uint count = min(water_cells[base + cell], uint(WATER_CELL_DROPS)); + for (uint k = 0u; k < count; ++k) + { + uint place = water_cells[base + uint(WATER_GRID * WATER_GRID) + cell * uint(WATER_CELL_DROPS) + k]; + water_drop_s drop = water_drops[slot * uint(WATER_DROPS) + place]; + float volume = drop.place.z; + if (volume <= 0.0) + continue; + vec3 offset = puv * (uv - drop.place.xy); + vec4 shape = vec4(rw_pcg4d(uvec4(place, slot, 41u, 0u))) * (1.0 / 4294967296.0); + if (drop.motion.w < 1.0) + { + // a flake: lobed lace, shrinking, grey and clear as it melts + float m = drop.motion.w; + vec2 flat2 = vec2(dot(offset, side), dot(offset, down)); + float radius = 0.5 * water_flake(volume); + float dist = length(flat2); + if (dist < radius + rw_pixel) + { + float angle = atan(flat2.y, flat2.x); + float lobes = 0.55 + 0.45 * (0.6 * rw_noise(angle * 1.9 + shape.z * 40.0, 13u) + 0.4 * abs(cos(3.0 * angle + shape.w * 6.2831853))); + float rim = radius * lobes * (1.0 - 0.45 * m); + float body = smoothstep(rim + rw_pixel, 0.55 * rim - rw_pixel, dist); + float lace = rw_noise2(flat2 / 0.0007 + shape.zw * 100.0, 14u).x; + float flake = body * mix(0.55, 1.0, lace) * (1.0 - 0.7 * m * m); + snowcover = 1.0 - (1.0 - snowcover) * (1.0 - flake); + wet = max(wet, m * body); + } + continue; + } + float radius = water_radius(volume); + vec3 velocity = puv * drop.motion.xy; + float speed = length(velocity); + if (speed > 0.002) + { + // its rear up the way it came: a corner, drawn out into a cusp the faster it goes (the front stays round) + vec3 way = velocity / speed; + rw_kernel(offset, radius, 1.0); + float drawn = clamp(speed * 5.0, 0.0, 1.5); + for (int t = 1; t <= 2; ++t) + rw_kernel(offset + way * (radius * (0.45 + 0.5 * drawn) * float(t)), radius * ((t == 1) ? 0.7 : 0.42), 1.0); + } + else + { + rw_drop_view(offset, radius, 1.0, shape, down, side); + } + } +} + +// how fresh the wetted paths of the simulation are at a place of the glass (x) and how that changes along u and v (yz): the +// texels of their map filtered by a cubic b-spline over 4 x 4 of them, so the edges of a path are smooth curves and its slopes +// change smoothly (a bilinear filter would leave steps at every texel in the bending of the view) +vec3 rw_trail(uint slot, vec2 uv, float now) +{ + // a path stays wet (and seen) longer than the drops keep to it + float lasting = 25.0; + vec2 p = uv * float(WATER_TRAIL) - 0.5; + vec2 i = floor(p); + vec2 f = p - i; + // the weights of the b-spline and their derivatives, for the texels i - 1 .. i + 2 + vec2 g = 1.0 - f; + vec4 wx = vec4(g.x * g.x * g.x, 3.0 * f.x * f.x * f.x - 6.0 * f.x * f.x + 4.0, -3.0 * f.x * f.x * f.x + 3.0 * f.x * f.x + 3.0 * f.x + 1.0, f.x * f.x * f.x) / 6.0; + vec4 wy = vec4(g.y * g.y * g.y, 3.0 * f.y * f.y * f.y - 6.0 * f.y * f.y + 4.0, -3.0 * f.y * f.y * f.y + 3.0 * f.y * f.y + 3.0 * f.y + 1.0, f.y * f.y * f.y) / 6.0; + vec4 dx = vec4(-g.x * g.x, 3.0 * f.x * f.x - 4.0 * f.x, -3.0 * f.x * f.x + 2.0 * f.x + 1.0, f.x * f.x) * 0.5; + vec4 dy = vec4(-g.y * g.y, 3.0 * f.y * f.y - 4.0 * f.y, -3.0 * f.y * f.y + 2.0 * f.y + 1.0, f.y * f.y) * 0.5; + vec3 result = vec3(0.0); + ivec2 base = ivec2(i) - 1; + for (int y = 0; y < 4; ++y) + { + for (int x = 0; x < 4; ++x) + { + float value = water_wetted(slot, base + ivec2(x, y), now, lasting); + result += value * vec3(wx[x] * wy[y], dx[x] * wy[y], wx[x] * dy[y]); + } + } + result.yz *= float(WATER_TRAIL); + return result; +} + +// the surroundings in a view space direction where the screen doesn't hold them: the environment map above the horizon (the sky +// and the clouds), the ground below it, blurred by Lod +vec3 rw_surroundings(vec3 direction, float lod) +{ + vec3 world = mat3(inv_view) * direction; + vec3 sky = envmap_sample_rough(normalize(vec3(world.x, max(world.y, 0.0), world.z)), clamp(lod / 8.0, 0.0, 1.0)); + // the horizon as blurred as the view is + float below = smoothstep(-0.03 - 0.05 * lod, 0.25 + 0.05 * lod, -world.y); + if ((below <= 0.0) || (false == ENVMAP_ENABLED)) + return sky; + return mix(sky, reflection_ground(), below); +} + +// what is seen through the glass in a view space direction: the image of the last frame (the screen space reflections keep it) +// where it holds the place, Distance away along the direction (0: far), blurred by Lod; Share: how much of it the image gave. +// Fade: towards the edges of the image the surroundings take over (else the edges are stretched) +vec3 rw_behind_edges(vec3 direction, float lod, float distance, out float share, bool fade) +{ + share = 0.0; + vec3 around = rw_surroundings(direction, lod); + if (ssr_params.x < 1.0) + return around; + vec4 previous = ssr_reprojection * ((distance > 0.0) ? vec4(direction * distance, 1.0) : vec4(direction, 0.0)); + if (previous.w <= 1e-5) + return around; + vec2 coords = (previous.xy / previous.w) * 0.5 + 0.5; + vec2 edges = min(coords, 1.0 - coords); + share = fade ? smoothstep(0.0, 0.04, min(edges.x, edges.y)) : step(-0.02, min(edges.x, edges.y)); + coords = clamp(coords, vec2(0.0), vec2(1.0)); + if (share <= 0.0) + return around; + // the view out through the water goes on outside, where the image shows the inside of the cab around the windows the + // surroundings stand for it (else the drops show the dark walls and roof of the cab) + ivec2 size = textureSize(scene_depth, 0); + float depth = texelFetch(scene_depth, clamp(ivec2(coords * vec2(size)), ivec2(0), size - 1), 0).r; + float near = projection[3][2]; + if ((depth > 0.0) && (near / depth < -f_pos.z + 2.5)) + { + share = 0.0; + return around; + } + vec3 color = textureLod(ssr_history, coords, clamp(lod, 0.0, ssr_params2.x)).rgb; + if (any(isnan(color)) || any(isinf(color))) + { + share = 0.0; + return around; + } + return mix(around, max(color, vec3(0.0)), share); +} + +vec3 rw_behind(vec3 direction, float lod, float distance, out float share) +{ + return rw_behind_edges(direction, lod, distance, share, true); +} + +// a layer Over put over Under, the colours not premultiplied +vec4 rw_over(vec4 over, vec4 under) +{ + float alpha = over.a + under.a * (1.0 - over.a); + if (alpha <= 1e-5) + return vec4(0.0); + return vec4((over.rgb * over.a + under.rgb * under.a * (1.0 - over.a)) / alpha, alpha); +} + +void main() +{ + vec4 tex_color = texture(diffuse, f_coord); + vec4 mask = texture(wipermask, f_coord); + + // the coordinates of the glass: metres along the axes of the model, so the water stays where it lies on the glass whatever the + // vehicle and the camera do. the axis of the model nearest to the vertical is up, the glass is projected along the + // horizontal one it faces most (along the vertical one if it lies flat) + mat3 linear = mat3(modelview); + mat3 rot = mat3(normalize(linear[0]), normalize(linear[1]), normalize(linear[2])); + mat3 unrot = transpose(rot); + vec3 local = unrot * (f_pos.xyz - modelview[3].xyz); + vec3 viewnormal = normalize(f_normal); + // the water is on the side away from the camera, the outside of the cab + vec3 outward = (dot(viewnormal, f_pos.xyz) < 0.0) ? -viewnormal : viewnormal; + vec3 normal = unrot * outward; + vec3 up = unrot * (vec3(0.0, 1.0, 0.0) * mat3(inv_view)); + vec3 aup = abs(up); + int iu = (aup.x > aup.y) ? ((aup.x > aup.z) ? 0 : 2) : ((aup.y > aup.z) ? 1 : 2); + int ia = (iu + 1) % 3; + int ib = (iu + 2) % 3; + vec3 axis_up = vec3(0.0); + axis_up[iu] = (up[iu] < 0.0) ? -1.0 : 1.0; + vec3 axis_a = vec3(0.0); + axis_a[ia] = 1.0; + vec3 axis_b = vec3(0.0); + axis_b[ib] = 1.0; + vec3 an = abs(normal); + vec3 axis_u, axis_v, axis_n; + if (an[iu] > max(an[ia], an[ib])) + { + axis_n = axis_up; + axis_u = axis_a; + axis_v = axis_b; + } + else if (an[ia] >= an[ib]) + { + axis_n = axis_a; + axis_u = axis_b; + axis_v = axis_up; + } + else + { + axis_n = axis_b; + axis_u = axis_a; + axis_v = axis_up; + } + vec2 glass = vec2(dot(local, axis_u), dot(local, axis_v)); + float facing = dot(normal, axis_n); + facing = ((facing < 0.0) ? -1.0 : 1.0) * max(abs(facing), 0.35); + // a step of the coordinates is a step along the glass + rw_tu = rot * (axis_u - axis_n * (dot(normal, axis_u) / facing)); + rw_tv = rot * (axis_v - axis_n * (dot(normal, axis_v) / facing)); + + // the derivatives, before anything is discarded: the size of a pixel on the glass and in the view, and how the coordinates of + // the mask follow the ones of the glass + vec3 view = normalize(f_pos.xyz); + vec3 dpdx = dFdx(f_pos.xyz); + vec3 dpdy = dFdy(f_pos.xyz); + rw_pixel = max(max(length(dpdx), length(dpdy)), 1e-6); + float pixelangle = max(max(length(dFdx(view)), length(dFdy(view))), 1e-6); + vec2 gdx = dFdx(glass); + vec2 gdy = dFdy(glass); + mat2 steps = mat2(gdx, gdy); + float det = determinant(steps); + mat2 maskstep = (abs(det) > 1e-14) ? mat2(dFdx(f_coord), dFdy(f_coord)) * inverse(steps) : mat2(0.0); + + // the water the simulation keeps, where the material is a glass of the cab of the train (the param water_slot, given by the + // renderer: 1 + the glass): the view space step along the glass for a step of the uv, and how fresh the wetted path the water + // went along is, with how that changes along the glass (before anything is discarded) + int slot = int(param[2].z + 0.5) - 1; + mat2 uvsteps = mat2(dFdx(f_coord), dFdy(f_coord)); + float uvdet = determinant(uvsteps); + bool simulated = (slot >= 0) && (slot < WATER_SLOTS) && (abs(uvdet) > 1e-24); + simulated = simulated && (water_header[clamp(slot, 0, WATER_SLOTS - 1)].x > 0.5); + mat2x3 puv = (abs(uvdet) > 1e-24) ? mat2x3(dpdx, dpdy) * inverse(uvsteps) : mat2x3(0.0); + vec3 trailfield = simulated ? rw_trail(uint(slot), f_coord, water_header[clamp(slot, 0, WATER_SLOTS - 1)].y) : vec3(0.0); + float trail = trailfield.x; + // its gradient along the glass, view space: the uv gradient taken through the steps of the uv along the glass + vec3 trailslope = vec3(0.0); + { + mat2 metric = transpose(puv) * puv; + if (abs(determinant(metric)) > 1e-30) + trailslope = puv * (inverse(metric) * trailfield.yz); + } + + if (tex_color.a < 0.01) + discard; + + vec4 layer = vec4(0.0); + float heavy = clamp(rain_params.x, 0.0, 1.0); + bool snow = rain_params.z > 0.5; + if ((heavy > 0.0) || simulated) + { + rw_scale = max(param[2].y, 0.05); + + // what meets the glass. the air against the vehicle (rain_wind) drags the drops along the glass with what of it goes + // along the glass and with the share of what meets it head on which turns up it: the more the glass leans back the more + // of the air goes along it, and the less of their weight pulls the drops down it + vec3 air = unrot * (rain_wind.xyz * mat3(inv_view)); + float fall = snow ? RW_SNOW_FALL : RW_RAIN_FALL; + float hits = max(-dot(air - up * fall, normal), 0.0); + rw_rate = heavy * clamp(0.7 + hits / fall, 0.7, 6.0); + rw_period = clamp((snow ? 10.0 : 5.0) / rw_rate, 0.4, 300.0); + rw_grow = 2.0 * rw_period; + vec3 gravity = -up - normal * dot(-up, normal); + vec3 rise = up - normal * dot(up, normal); + rise = (length(rise) > 0.1) ? normalize(rise) : vec3(0.0); + vec3 flow = air - normal * dot(air, normal) + rise * (max(-dot(air, normal), 0.0) * RW_HEAD_ON); + vec3 push = gravity + flow * (length(flow) / (RW_DRAG_SPEED * RW_DRAG_SPEED)); + vec2 force = vec2(dot(push, axis_u), dot(push, axis_v)); + rw_push = length(force); + vec2 fallway = vec2(dot(gravity, axis_u), dot(gravity, axis_v)); + rw_down = (length(fallway) > 0.1) ? normalize(fallway) : vec2(0.0, -1.0); + vec2 flowway = vec2(dot(flow, axis_u), dot(flow, axis_v)); + rw_flow = (length(flowway) > 1.0) ? normalize(flowway) : vec2(0.0); + + rw_wipe_s wipe = rw_wipe(mask); + rw_since = wipe.since; + // the blades went lately (any of them, also where none of them reaches) + float working = 0.0; + for (int i = 0; i < 4; ++i) + working = max(working, 1.0 - smoothstep(4.0, 30.0, time - wiper_timer_return[i])); + // how much of the water the place gathers it has + float gathered = 1.0 - exp(-rw_since / (6.0 * rw_period)); + // the film a blade leaves is drawn into fine streaks along its arcs, each a thin cylindrical lens which draws a light seen + // through it into a line across the streaks, towards the pivot of the blade: the way across the streaks on the screen + vec2 streakway = vec2(0.0); + if ((working > 0.0) && (false == snow) && (abs(det) > 1e-14)) + { + int blade = 0; + for (int i = 1; i < 4; ++i) + blade = (mask[i] > mask[blade]) ? i : blade; + if (mask[blade] >= 0.001) + { + // the mask grows along the arcs; a step of a centimetre, so the steps of its values don't matter + float mu = textureLod(wipermask, f_coord + maskstep * vec2(0.01, 0.0), 0.0)[blade]; + float mv = textureLod(wipermask, f_coord + maskstep * vec2(0.0, 0.01), 0.0)[blade]; + vec2 arc = vec2(mu, mv) - mask[blade]; + vec2 onscreen = inverse(steps) * vec2(-arc.y, arc.x); + if ((length(arc) > 1e-4) && (length(onscreen) > 1e-9)) + streakway = normalize(onscreen); + } + } + vec3 ripple = rw_noise2(glass / (0.006 * rw_scale), 5u); + + // the water a blade pushes: a bead in front of it while it moves, as wide as the water it gathered on its way, with a steep + // lumpy front (its edge shows dark, the view can't get out there). the drops the bead reaches flow into it + vec3 lumps = rw_noise2(glass / (0.009 * rw_scale), 7u); + float gatheredwater = snow ? 0.0 : min(gathered * 1.5, 1.0) * (0.35 + 0.65 * min(wipe.swept * 2.5, 1.0)); + float reach = 0.004 + 0.018 * gatheredwater + 0.008 * (ripple.x - 0.5); + float ridge = gatheredwater * smoothstep(0.0, 0.003, wipe.front) * (1.0 - smoothstep(reach - 0.004, reach, wipe.front)); + // the smooth shape from how it changes from pixel to pixel, the lumps on it as they are + float pooledbase = 0.004 * ridge; + float pooled = pooledbase * (0.75 + 0.5 * lumps.x); + vec3 pooledslope = rw_gradient(pooledbase, dpdx, dpdy, outward) * (0.75 + 0.5 * lumps.x) + rw_along(lumps.yz * (0.5 * pooledbase / (0.009 * rw_scale))); + if ((pooled > 0.0) && (false == simulated)) + { + rw_sheet(pooled, pooledslope, 0.03, smoothstep(0.0, 0.1, ridge)); + if (ridge > 0.05) + rw_cleared = time; + } + + float snowcover = 0.0; + float wet = 0.0; + if (simulated) + { + // the directions along the glass, view space + vec3 upview = vec3(0.0, 1.0, 0.0) * mat3(inv_view); + vec3 downview = -(upview - outward * dot(upview, outward)); + downview = (length(downview) > 0.05) ? normalize(downview) : normalize(cross(outward, normalize(rw_tu))); + vec3 sideview = cross(outward, downview); + rw_simulated(uint(slot), f_coord, puv, downview, sideview, snowcover, wet); + // the paths the water went along lately: a rivulet, a band of water rounded across (a cylinder lens: it holds a picture of + // the place around it turned over across the band, its edges dark as the edges of drops), thicker the fresher. the + // smoothed freshness falls off from the middle of a path to its edges + if (trail > 0.08) + { + float p = clamp((trail - 0.08) / 0.5, 0.0, 1.0); + float thickness = 0.0005 * (1.0 - (1.0 - p) * (1.0 - p)); + vec3 rise = trailslope * (0.0005 * 2.0 * (1.0 - p) / 0.5); + float steep = length(rise); + if (steep > 0.7) + rise *= 0.7 / steep; + bool top = thickness > rw_height; + rw_sheet(thickness, rise, 0.004, smoothstep(0.0, 0.15, p) * 0.95); + // from the middle of the band to the fragment + float across = length(trailslope) / 0.5; + if (top && (across > 1.0)) + rw_offset = -trailslope * ((1.0 - p) / (across * length(trailslope))); + } + } + else if (false == snow) + { + // the drops which grew big enough slide off, with gravity and the air: a set of tracks for each of the directions of the + // push nearest to the one now, each laid along its own way, so none of them moves when the push turns + float sliding = smoothstep(0.25, 0.6, rw_push); + if (sliding > 0.0) + { + float bin = atan(force.y, force.x) / (RW_PI / 6.0); + float lower = floor(bin); + for (int k = 0; k < 2; ++k) + { + float share = ((k == 0) ? 1.0 - (bin - lower) : bin - lower) * sliding; + if (share < 0.02) + continue; + float angle = (lower + float(k)) * (RW_PI / 6.0); + vec2 down = vec2(cos(angle), sin(angle)); + vec2 side = vec2(-down.y, down.x); + rw_sliders(vec2(dot(glass, side), -dot(glass, down)), side, down, uint(int(lower) + k + 64) * 2u + 16u, share); + } + } + + // the drops as they land, each time somewhere else in its cell, as often as the rain hits the glass: the drops of the + // rain spread on the glass, thrown with a fine splash which runs back into them + // each drop anywhere in its cell, the cell as large as a drop with its splash reaches + float cell = 0.007 * rw_scale; + vec2 laid = rw_turn(glass, 0.61) + vec2(0.31, 0.17); + ivec2 home = ivec2(floor(laid / cell)); + float density = mix(0.08, 0.3, heavy); + for (int y = -1; y <= 1; ++y) + { + for (int x = -1; x <= 1; ++x) + { + ivec2 c = home + ivec2(x, y); + vec4 a = rw_random(c, 1u, 0u); + if (a.x > density) + continue; + vec4 b = rw_random(c, 1u, 1u); + float span = rw_period * mix(0.7, 1.3, a.w); + float cycle = floor(time / span + b.x); + float age = time - (cycle - b.x) * span; + float life = span * mix(0.5, 0.9, b.y); + if ((age > life) || (age > rw_since) || (time - age < rw_cleared)) + continue; + vec4 p = rw_random(c, 2u, uint(cycle)); + vec2 offset = rw_turn(laid - (vec2(c) + p.xy) * cell, -0.61); + float visible = mix(0.0011, 0.0019, b.z) * rw_scale; + // a raindrop hits the glass far above the speed at which it splashes (Weber number in the hundreds and thousands): + // it spreads, throws a spray of droplets which stay on the glass around it, and draws back + float splash = exp(-age / 0.06); + float fade = 1.0 - smoothstep(life - min(0.4, 0.3 * life), life, age); + if (visible * 0.3 > 0.5 * rw_pixel) + { + int spray = 3 + int(p.z * 4.0); + for (int s = 0; s < spray; ++s) + { + vec4 e = rw_random(c, 3u, uint(cycle) * 8u + uint(s)); + vec2 way = normalize(vec2(cos(e.x * 6.2831853), sin(e.x * 6.2831853)) + rw_flow * 0.9); + rw_blob(offset - way * (visible * (1.4 + 1.6 * e.y)), visible * mix(0.18, 0.35, e.z), fade * (0.55 + 0.45 * splash)); + } + } + rw_drop(offset, visible * (1.0 + 0.3 * splash), fade, p.wzyx); + } + } + // the drops the landing ones gathered into, growing until they are big enough to slide (the air blows them off sooner) + cell = 0.011 * rw_scale; + laid = rw_turn(glass, 1.07) + vec2(0.53, 0.29); + home = ivec2(floor(laid / cell)); + density = mix(0.55, 1.0, heavy); + for (int y = -1; y <= 1; ++y) + { + for (int x = -1; x <= 1; ++x) + { + ivec2 c = home + ivec2(x, y); + vec4 a = rw_random(c, 4u, 0u); + if (a.x > density) + continue; + vec4 b = rw_random(c, 4u, 1u); + float span = rw_period * mix(2.5, 4.0, a.w); + float cycle = floor(time / span + b.x); + float age = time - (cycle - b.x) * span; + float life = span * mix(0.75, 0.98, b.y) / (1.0 + 2.0 * max(rw_push - 1.0, 0.0)); + if ((age > life) || (age > rw_since) || (time - age < rw_cleared)) + continue; + vec4 p = rw_random(c, 5u, uint(cycle)); + // the big drops are much alike: they grow as they gather the small ones (the radius in step with the time) up to the + // size at which they slide off (on a vertical glass a footprint of about 9 mm, larger on a glass leaning back, smaller + // in a strong air flow), and go + float slides = min(0.0045 / sqrt(max(rw_push, 0.3)), 0.005); + float visible = slides * mix(0.6, 1.0, b.z) * rw_scale * mix(0.35, 1.0, clamp(age / (0.6 * life), 0.0, 1.0)); + rw_drop(rw_turn(laid - (vec2(c) + p.xy) * cell, -1.07), visible, 1.0 - smoothstep(life - min(0.5, 0.3 * life), life, age), p.zwxy); + } + } + + // the water the blades push out of their sweep: a broken row of drops flowed together along its edge, just outside it + float leave = working * min(heavy * 1.5, 1.0); + if ((leave > 0.0) && rw_border(f_coord, 0.009 * rw_scale, maskstep)) + { + cell = 0.0065 * rw_scale; + laid = rw_turn(glass, 0.29) + vec2(0.11, 0.43); + home = ivec2(floor(laid / cell)); + for (int y = -1; y <= 1; ++y) + { + for (int x = -1; x <= 1; ++x) + { + ivec2 c = home + ivec2(x, y); + vec4 a = rw_random(c, 20u, 0u); + if (a.x > 0.85) + continue; + // from the fragment to the middle of the drop, along the glass + vec2 centre = rw_turn((vec2(c) + a.yz) * cell - laid, -0.29); + float rim = rw_rim(f_coord + maskstep * centre, 0.004 * rw_scale, maskstep) * leave; + if (rim <= 0.05) + continue; + rw_drop(-centre, mix(0.0016, 0.003, a.w) * rw_scale * mix(0.6, 1.0, rim), smoothstep(0.05, 0.35, rim), rw_random(c, 21u, 0u)); + } + } + } + + // the streams running down from the water the blades leave at the ends of their sweep: a stream in some of the columns + // along the way down, where up the glass there is such water, as far down as the water had time to run since the blade + // went over the place + if ((working > 0.0) && (length(fallway) > 0.3)) + { + vec2 side = vec2(-rw_down.y, rw_down.x); + float width = 0.035 * rw_scale; + float across = dot(glass, side); + float along = -dot(glass, rw_down); + int column = int(floor(across / width)); + vec4 a = rw_random(ivec2(column, 0), 30u, 0u); + if (a.x < 0.6 * heavy * working) + { + float lateral = across - rw_path(column, along, width, 31u); + float half_width = mix(0.0006, 0.0012, a.z) * rw_scale * (0.6 + 0.8 * rw_noise(along / (0.006 * rw_scale), 32u)); + if (abs(lateral) < 2.0 * half_width + rw_pixel) + { + // up the glass, the first place where the reach of the blades begins or ends + const float reach[6] = float[](0.01, 0.025, 0.05, 0.1, 0.18, 0.3); + bool here = rw_reached(f_coord); + float source = -1.0; + for (int s = 0; s < 6; ++s) + { + if (rw_reached(f_coord + maskstep * (-rw_down * reach[s])) != here) + { + source = reach[s]; + break; + } + } + if ((source > 0.0) && (source < max(rw_since - 0.1, 0.0) * 0.2)) + { + float beads = rw_noise(along / (0.004 * rw_scale), 33u); + float flowing = working * (1.0 - 0.5 * smoothstep(0.15, 0.3, source)); + rw_blob(side * lateral, half_width, flowing * mix(0.6, 1.2, beads)); + } + } + } + } + } + else + { + // the flakes stick where they land and melt into drops: the glass is warmed by the cab, faster in the air above zero + float warm = rain_params.w; + float cell = 0.016 * rw_scale; + vec2 laid = rw_turn(glass, 0.83) + vec2(0.07, 0.61); + ivec2 home = ivec2(floor(laid / cell)); + float density = mix(0.5, 0.9, heavy); + for (int y = -1; y <= 1; ++y) + { + for (int x = -1; x <= 1; ++x) + { + ivec2 c = home + ivec2(x, y); + vec4 a = rw_random(c, 11u, 0u); + if (a.x > density) + continue; + vec4 b = rw_random(c, 11u, 1u); + float span = rw_period * mix(0.7, 1.3, a.w); + float cycle = floor(time / span + b.x); + float age = time - (cycle - b.x) * span; + float life = span * mix(0.6, 0.95, b.y); + if ((age > life) || (age > rw_since)) + continue; + vec4 p = rw_random(c, 12u, uint(cycle)); + vec2 offset = rw_turn(laid - (vec2(c) + p.xy) * cell, -0.83); + float diameter = mix(RW_SNOW_DIAMETER.x, RW_SNOW_DIAMETER.y, b.z) * rw_scale; + float melt = ((warm > 0.5) ? mix(3.0, 10.0, b.w) / (1.0 + 0.25 * warm) : mix(15.0, 45.0, b.w)); + if (age < melt) + { + float m = age / melt; + float dist = length(offset.x * rw_tu + offset.y * rw_tv); + float radius = 0.5 * diameter; + if (dist < radius + rw_pixel) + { + float angle = atan(offset.y, offset.x); + float lobes = 0.55 + 0.45 * (0.6 * rw_noise(angle * 1.9 + p.z * 40.0, 13u) + 0.4 * abs(cos(3.0 * angle + p.w * 6.2831853))); + float rim = radius * lobes * (1.0 - 0.45 * m); + float body = smoothstep(rim + rw_pixel, 0.55 * rim - rw_pixel, dist); + float lace = rw_noise2(glass / (0.0007 * rw_scale) + p.zw * 100.0, 14u).x; + float flake = body * mix(0.55, 1.0, lace) * (1.0 - 0.7 * m * m); + snowcover = 1.0 - (1.0 - snowcover) * (1.0 - flake); + wet = max(wet, m * body); + } + } + else + { + // the water of the flake + rw_drop(offset, 0.0018 * pow(diameter / 0.008, 0.667) * rw_scale, 1.0 - smoothstep(life - min(0.5, 0.3 * life), life, age), p.wzyx); + } + } + } + // the snow the blades push in front of them and leave at the ends of their sweep + float pile = max(wipe.ahead * min(gathered * 2.0, 1.0) * (0.4 + 0.6 * min(wipe.swept * 2.5, 1.0)), rw_rim(f_coord, 0.006 * rw_scale, maskstep) * working * heavy); + float packed = rw_noise2(glass / (0.0015 * rw_scale), 15u).x; + snowcover = max(snowcover, smoothstep(0.15, 0.5, pile * 1.2 + 0.4 * (ripple.x - 0.5)) * mix(0.7, 1.0, packed)); + } + // untreated glass, weathered and a little dirty: flat drops (a rain repellent coating would give beads above 90 degrees) + rw_settle(radians(mix(30.0, 48.0, rw_noise2(glass / (0.005 * rw_scale), 16u).x))); + + // the lamps (of the cab, of the scenery) and the headlights light the water up, from both sides of the glass (worked out + // here, where the fragments of a quad go together) + vec3 lamps = vec3(0.0); + vec3 lampgloss = vec3(0.0); + add_local_lights(outward, lamps, lampgloss); + add_local_lights(-outward, lamps, lampgloss); + + // the film just behind a blade, breaking up + float film = snow ? 0.0 : heavy * exp(-rw_since / 0.8) * smoothstep(0.02, 0.08, rw_since); + if (film > 0.01) + { + vec3 fine = rw_noise2(glass / (0.0021 * rw_scale), 6u); + vec2 tilt = fine.yz * (0.004 * film); + rw_sheet(0.00005 * film, rw_along(tilt), 0.3, 0.8 * film); + } + + // what the water shows: the view goes through the glass (a plate: only the way from the air into the water counts) and out + // through the surface of the water, bent as much as it is curved. it is the world turned upside down in a drop, and the + // edges of a drop, where the view can't get out (total internal reflection), show the inside of the cab + float background = 0.0; + bool image = ssr_params.x >= 1.0; + if (image) + { + ivec2 size = textureSize(scene_depth, 0); + float depth = texelFetch(scene_depth, clamp(ivec2(gl_FragCoord.xy), ivec2(0), size - 1), 0).r; + if (depth > 0.0) + background = (projection[3][2] / depth) / max(-view.z, 0.05); + } + vec3 inside = ambient * (AMBIENT_SCALE * 0.5); + vec4 water = vec4(0.0); + if (rw_cover > 0.0) + { + vec3 surface = normalize(outward - rw_slope); + vec3 inwater = refract(view, -outward, 1.0 / RW_IOR); + vec3 through = refract(inwater, -surface, RW_IOR); + // what the surface reflects back inside the water: the light which came in at the other side of the drop, from the sky, + // and the inside of the cab. where the view can't get out (the edge of a drop) that is all it shows: a bright edge + vec3 bounced = reflect(inwater, surface); + if (dot(bounced, outward) < 0.0) + bounced = reflect(bounced, outward); + vec3 inner = mix(inside, rw_surroundings(normalize(bounced), 5.0), 0.3); + vec3 color = inner; + if (dot(through, through) > 0.5) + { + float fresnel = 0.02 + 0.98 * pow(1.0 - abs(dot(through, surface)), 5.0); + // the view through the water spreads over this much of the world across a pixel (radians) + float spread = (RW_IOR - 1.0) * rw_pixel / max(rw_curve, 1e-4); + // the picture the drop holds as a lens, and a share of the bending by the surface + vec3 lens = normalize(f_pos.xyz - (1.0 + RW_LENS) * rw_offset) + (through - view) * RW_LENS_BEND; + float lod = mix(log2(1.0 + RW_LENS) - 0.5, log2(max(spread / pixelangle, 1.0)), RW_LENS_BEND); + // a drop a few pixels across would hold a picture of single pixels, a grain: blurred more the smaller it looks + lod += clamp(2.5 - log2(max(rw_curve / rw_pixel, 1.0)), 0.0, 1.5); + float share; + vec3 seen = rw_behind(normalize(lens), lod, background, share); + // the sun through the drop, where the image doesn't hold it: as bright as the drop gathers its light. in heavier rain + // the sun is behind the clouds, its disc is not there to be focused + float sunshare = (1.0 - heavy) * (1.0 - heavy); + if ((lights_count > 0U) && (share < 1.0) && (sunshare > 0.01)) + { + vec3 sun = normalize(-lights[0].dir); + float width = max(spread, 0.03); + float off = acos(clamp(dot(through, sun), -1.0, 1.0)); + if (off < 4.0 * width) + { + float lit = 1.0 - clamp(calc_shadow(), 0.0, 1.0); + vec3 sunlight = lights[0].color * (SUN_DIFFUSE_SCALE * lights[0].intensity * cloud_shadow(mat3(inv_view) * f_pos.xyz)); + // the light of the sun over the spread of the view: a pixel shows pi * irradiance * the share of a steradian + seen += sunlight * (sunshare * lit * (1.0 - share) * exp(-0.5 * off * off / (width * width)) / (2.0 * width * width)); + } + } + color = mix(seen, inner, fresnel); + } + color += lamps * 0.6; + // without the image of the last frame the surroundings stand for what the water bends the view to; nearly flat water + // would show them in place of the scene behind it, so it is left mostly clear + float opaque = image ? 1.0 : mix(0.3, 1.0, smoothstep(0.02, 0.15, 1.0 - dot(through, view))); + water = vec4(color, rw_cover * opaque); + } + // the wet glass where the rain gathers is a little hazy: the tiniest drops spread the light + if (image && (false == snow)) + { + float mist = heavy * gathered * (simulated ? 0.18 : 0.3); + if (mist > 0.01) + { + float share; + vec3 hazy = rw_behind_edges(view, 3.5, background, share, false); + // the tiniest drops spread the light of the sky coming through the glass + hazy = mix(hazy, rw_surroundings(outward, 7.0), 0.1); + layer = vec4(hazy, mist * max(share, 0.5)); + } + } + layer = rw_over(water, layer); + // the lights seen through the streaks of the film the blades leave, drawn into lines towards their pivot + if (image && (length(streakway) > 0.5)) + { + vec4 previous = ssr_reprojection * ((background > 0.0) ? vec4(view * background, 1.0) : vec4(view, 0.0)); + if (previous.w > 1e-5) + { + vec2 coords = (previous.xy / previous.w) * 0.5 + 0.5; + vec2 texel = 1.0 / vec2(textureSize(ssr_history, 0)); + ivec2 size = textureSize(scene_depth, 0); + float near = projection[3][2]; + // a light: much brighter than the place around it (the bright sky is not) + vec3 around = textureLod(ssr_history, clamp(coords, vec2(0.0), vec2(1.0)), 6.0).rgb; + float threshold = max(1.0, 3.0 * max(around.r, max(around.g, around.b))); + vec3 glare = vec3(0.0); + float total = 0.0; + for (int k = -8; k <= 8; ++k) + { + if (k == 0) + continue; + float w = 1.0 - abs(float(k)) / 9.0; + total += w; + vec2 tap = coords + streakway * texel * (float(k) * 5.0); + if (any(lessThan(tap, vec2(0.0))) || any(greaterThan(tap, vec2(1.0)))) + continue; + // only what is outside: the lamps of the cab are not behind the glass + float depth = texelFetch(scene_depth, clamp(ivec2(tap * vec2(size)), ivec2(0), size - 1), 0).r; + if ((depth > 0.0) && (near / depth < -f_pos.z + 2.5)) + continue; + vec3 c = textureLod(ssr_history, tap, 1.5).rgb; + if (any(isnan(c)) || any(isinf(c))) + continue; + glare += max(c - vec3(threshold), vec3(0.0)) * w; + } + glare *= 0.6 * heavy * working / total; + if (any(greaterThan(glare, vec3(1e-4)))) + { + // the scene itself under the water carries the light added (it changes nothing seen) + vec3 scene = textureLod(ssr_history, clamp(coords, vec2(0.0), vec2(1.0)), 0.0).rgb; + layer = rw_over(layer, vec4(max(scene, vec3(0.0)), 0.2)); + layer.rgb += glare / layer.a; + } + } + } + if (snowcover > 0.0) + { + // the flakes are lit through from the outside, by the sky and the sun, and from the inside of the cab; melting they turn + // grey and clear + vec3 outside = rw_surroundings(outward, 6.0); + if (lights_count > 0U) + { + vec3 sun = normalize(-lights[0].dir); + outside += lights[0].color * (SUN_DIFFUSE_SCALE * lights[0].intensity * max(dot(outward, sun), 0.0) * (1.0 - clamp(calc_shadow(), 0.0, 1.0))); + } + vec3 flake = mix(vec3(0.92), vec3(0.62, 0.66, 0.7), wet); + vec3 color = flake * (outside * (0.35 * (1.0 - 0.5 * wet)) + ambient * (AMBIENT_SCALE * 0.7)); + layer = rw_over(vec4(color, snowcover * mix(0.97, 0.6, wet)), layer); + } + } + out_color = layer; + + { // Overlay windshield texture with alpha + vec3 fragcolor = ambient; + vec3 fragnormal = normalize(f_normal); + float reflectivity = param[1].z; + float specularity = (tex_color.r + tex_color.g + tex_color.b) * 0.5; + legacy_gloss(); + + fragcolor = apply_lights(fragcolor, fragnormal, tex_color.rgb, reflectivity, specularity, shadow_tone); + vec4 color = vec4(fragcolor, tex_color.a * alpha_mult); + + out_color.xyz = apply_fog(mix(out_color.xyz, color.xyz, color.a)); + out_color.a = mix(out_color.a, 1., color.a); + } + if (false == POSTFX_ENABLED) + { + out_color = tonemap(out_color); + } + + if (MOTIONBLUR_ENABLED) + { + out_motion = vec4(0.0); + } } diff --git a/shaders/vk14/water.glsl b/shaders/vk14/water.glsl new file mode 100644 index 00000000..41d672d6 --- /dev/null +++ b/shaders/vk14/water.glsl @@ -0,0 +1,114 @@ +// the water on the windscreens (gfx::windscreen_water, gl::WATER_* in gl/ubo.h, the vulkan renderer: vk14::windscreen_water): what the passes of the simulation (water_*.comp) +// keep and the glasses (mat_rain_windscreen.frag) read. a glass is a material of mat_rain_windscreen in the cab of the train, a +// slot of the storage; its drops lie in the coordinates of its texture (uv), as its mask of the wipers does, so they stay where +// they lie on the glass whatever the vehicle and the camera do. the map of the glass tells what a step of the uv is along the +// glass (metres, in the space of the model of the cab) and where the blades go over it; the drops are looked up through a grid +// of cells over the uv, each listing the drops which reach into it; the wetted paths are marked on a map of their own with the +// time the water went there + +#ifndef WATER_GLSL +#define WATER_GLSL + +struct water_drop_s +{ + // xy: the place, uv; z: the water, mm3 (0: no drop there); w: seconds it lies on the glass + vec4 place; + // xy: the velocity, uv per second; z: random 0 .. 1, its shape; w: 0 .. 1 how far it melted from a flake of snow (1: water) + vec4 motion; +}; + +#if defined(STAGE_COMPUTE) +// the compute kernels: set COMPUTE_SET pushed with the dispatch, the storage at 0, the block of the step at 1 (water_sim.glsl) +layout(std430, set = COMPUTE_SET, binding = 0) buffer water_ssbo +#else +layout(std430, set = PASS_SET, binding = WATER_SSBO) readonly buffer water_ssbo +#endif +{ + // every glass: x 1 when it's simulated, y seconds its simulation runs (the time of the wetted paths), zw unused + vec4 water_header[WATER_SLOTS]; + // every glass: x the drops on it, yzw unused + uvec4 water_counts_of[WATER_SLOTS]; + water_drop_s water_drops[WATER_SLOTS * WATER_DROPS]; + // the map, two for a texel: xyz the step along the glass for a step of u (metres), w 1 on the glass, 0.5 next to it (the edges + // spread out, for the filtering), 0 elsewhere; xyz the same for v, w 1 when the cross product of the two points out of the cab, + // -1 when it points in + vec4 water_axes[WATER_SLOTS * WATER_MAP * WATER_MAP * 2]; + // the mask of the wipers baked into the map: the place of a blade over its sweep at which it goes over the texel, 0 where it + // doesn't reach + vec4 water_blades[WATER_SLOTS * WATER_MAP * WATER_MAP]; + // every drop: 0 the place is free, else bit 0 set, bits 1 .. 3 the blade carrying it (1 + its number, 0 none), bit 4 set when + // the blade carries it towards the far end of its sweep + uint water_state[WATER_SLOTS * WATER_DROPS]; + // the drop it flows into (itself when none) + uint water_target[WATER_SLOTS * WATER_DROPS]; + // what flowed into it: the water (1e-4 mm3) and the moment of the places of the drops about its own (1e-6 uv mm3) + int water_gather[WATER_SLOTS * WATER_DROPS * 4]; + // x: the way it went since it left a droplet behind, metres; y: the way it goes on before it leaves the next one, metres + vec4 water_extra[WATER_SLOTS * WATER_DROPS]; + // the grids of the glasses, WATER_GRID x WATER_GRID cells each: the counts of the drops of the cells, then the drops of each + // cell (WATER_CELL_DROPS for a cell, the ones over the count aren't there). then the wetted paths of the glasses, + // WATER_TRAIL x WATER_TRAIL each: in the upper 24 bits 1 + the time the water went over the texel last, 1/2 s (0: never), + // in the lower 8 how near the middle of the path the texel is, 0 .. 255 (filtered between the texels the edges of the paths + // come out smooth wherever they go across the texels; the time is coarse, so a drop going along leaves the nearness of its + // whole passage, not of the step which happened to come last) + uint water_cells[]; +}; + +// the radius of the footprint of a drop holding Volume mm3 of water, metres: a cap of a sphere meeting the glass at 40 degrees +float water_radius(float volume) +{ + return 0.001 * pow(1.675 * max(volume, 0.0), 1.0 / 3.0); +} + +// the diameter of a flake of snow holding Volume mm3 of water, metres +float water_flake(float volume) +{ + return 0.006 * pow(max(volume, 0.0), 1.0 / 3.0); +} + +// where the grid of a glass begins in water_cells: the count of a cell at the base + the cell, its drops from the base + +// WATER_GRID * WATER_GRID + the cell * WATER_CELL_DROPS +uint water_grid_base(uint slot) +{ + return slot * uint(WATER_GRID * WATER_GRID * (1 + WATER_CELL_DROPS)); +} + +uint water_trail_base(uint slot) +{ + return uint(WATER_SLOTS * WATER_GRID * WATER_GRID * (1 + WATER_CELL_DROPS)) + slot * uint(WATER_TRAIL * WATER_TRAIL); +} + +// the cell of the grid holding a place of the glass +uint water_cell(vec2 uv) +{ + ivec2 cell = clamp(ivec2(floor(uv * float(WATER_GRID))), ivec2(0), ivec2(WATER_GRID - 1)); + return uint(cell.y * WATER_GRID + cell.x); +} + +// how fresh the wetted path is at a texel of the map of the paths, 1 just wetted in its middle .. 0 dry long ago or never; Now: +// seconds the simulation of the glass runs, Lasting: seconds in which the freshness falls to 1/e +float water_wetted(uint slot, ivec2 texel, float now, float lasting) +{ + texel = clamp(texel, ivec2(0), ivec2(WATER_TRAIL - 1)); + uint value = water_cells[water_trail_base(slot) + uint(texel.y * WATER_TRAIL + texel.x)]; + uint stamp = value >> 8u; + if (stamp == 0u) + return 0.0; + float age = max(now - float(stamp - 1u) / 2.0, 0.0); + return exp(-age / lasting) * (float(value & 255u) / 255.0); +} + +// the same filtered between the texels at a place of the glass +float water_wetted_at(uint slot, vec2 uv, float now, float lasting) +{ + vec2 p = uv * float(WATER_TRAIL) - 0.5; + ivec2 i = ivec2(floor(p)); + vec2 f = p - vec2(i); + float a = water_wetted(slot, i, now, lasting); + float b = water_wetted(slot, i + ivec2(1, 0), now, lasting); + float c = water_wetted(slot, i + ivec2(0, 1), now, lasting); + float d = water_wetted(slot, i + ivec2(1, 1), now, lasting); + return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); +} + +#endif diff --git a/shaders/vk14/water_bake.comp b/shaders/vk14/water_bake.comp new file mode 100644 index 00000000..963f367b --- /dev/null +++ b/shaders/vk14/water_bake.comp @@ -0,0 +1,27 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): the mask of the wipers of a glass baked into its map, once, when the glass is +// set up. a thread per texel of the map; the glass is water_counts.z + +// none of the pass set: everything the kernel reads is pushed with its dispatch (set COMPUTE_SET, water_sim.glsl), so it runs +// before the passes of the frame set theirs up +#define LAYOUT_UI 1 +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +// the mask of the wipers of the material of the glass (wipermask) +layout(set = COMPUTE_SET, binding = 2) uniform sampler2D water_mask; + +void main() +{ + uint slot = water_counts.z; + uint texel = gl_GlobalInvocationID.x; + if ((slot >= uint(WATER_SLOTS)) || (texel >= uint(WATER_MAP * WATER_MAP))) + return; + vec2 uv = (vec2(float(texel % uint(WATER_MAP)), float(texel / uint(WATER_MAP))) + 0.5) / float(WATER_MAP); + // as much of the mask as a texel of the map covers + float lod = log2(max(float(textureSize(water_mask, 0).x) / float(WATER_MAP), 1.0)); + water_blades[slot * uint(WATER_MAP * WATER_MAP) + texel] = textureLod(water_mask, uv, lod); +} diff --git a/shaders/vk14/water_clear.comp b/shaders/vk14/water_clear.comp new file mode 100644 index 00000000..aa6caed0 --- /dev/null +++ b/shaders/vk14/water_clear.comp @@ -0,0 +1,21 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): the counts of the cells of the grids emptied before the drops are put in them +// (water_grid.comp). a thread per cell, a row of groups per glass + +// none of the pass set: everything the kernel reads is pushed with its dispatch (set COMPUTE_SET, water_sim.glsl), so it runs +// before the passes of the frame set theirs up +#define LAYOUT_UI 1 +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +void main() +{ + uint slot = gl_WorkGroupID.y; + uint cell = gl_GlobalInvocationID.x; + if ((water_slots[slot].z < 0.5) || (cell >= uint(WATER_GRID * WATER_GRID))) + return; + water_cells[water_grid_base(slot) + cell] = 0u; +} diff --git a/shaders/vk14/water_gather.comp b/shaders/vk14/water_gather.comp new file mode 100644 index 00000000..8eb82a2d --- /dev/null +++ b/shaders/vk14/water_gather.comp @@ -0,0 +1,43 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): the drops flow into the ones water_merge.comp found for them, where those don't +// flow on themselves (they do in a later step): the water and the moment of the places of the drops gathered by the one they +// flow into (water_settle.comp moves it to the middle of the water), the drops gone. a thread per drop, a row of groups per glass + +// none of the pass set: everything the kernel reads is pushed with its dispatch (set COMPUTE_SET, water_sim.glsl), so it runs +// before the passes of the frame set theirs up +#define LAYOUT_UI 1 +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +void main() +{ + uint slot = gl_WorkGroupID.y; + uint place = gl_GlobalInvocationID.x; + if ((water_slots[slot].z < 0.5) || (place >= uint(WATER_DROPS))) + return; + uint base = slot * uint(WATER_DROPS); + uint index = base + place; + uint target = water_target[index]; + if ((target == place) || (target >= uint(WATER_DROPS)) || (water_target[base + target] != target)) + return; + water_drop_s drop = water_drops[index]; + water_drop_s into = water_drops[base + target]; + if ((drop.place.z <= 0.0) || (into.place.z <= 0.0)) + return; + // with what flowed into the drop itself in the step (the landings) + float landed = float(water_gather[index * 4u]) * 1e-4; + vec2 moment = vec2(float(water_gather[index * 4u + 1u]), float(water_gather[index * 4u + 2u])) * 1e-6; + float volume = drop.place.z + landed; + vec2 offset = drop.place.xy - into.place.xy; + atomicAdd(water_gather[(base + target) * 4u], int(volume * 1e4)); + atomicAdd(water_gather[(base + target) * 4u + 1u], int((offset.x * volume + moment.x) * 1e6)); + atomicAdd(water_gather[(base + target) * 4u + 2u], int((offset.y * volume + moment.y) * 1e6)); + // the blade carrying the drop carries what it flows into + uint carried = water_state[index] & 0x1eu; + if ((carried != 0u) && ((water_state[base + target] & 0x0eu) == 0u)) + atomicOr(water_state[base + target], carried); + water_remove(index); +} diff --git a/shaders/vk14/water_grid.comp b/shaders/vk14/water_grid.comp new file mode 100644 index 00000000..ca3b617d --- /dev/null +++ b/shaders/vk14/water_grid.comp @@ -0,0 +1,55 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): every drop put in the cells of the grid it reaches into, as far as the glasses +// draw it (a sliding one is drawn out behind itself) and as far as the drops it may flow together with are. a thread per drop, +// a row of groups per glass + +// none of the pass set: everything the kernel reads is pushed with its dispatch (set COMPUTE_SET, water_sim.glsl), so it runs +// before the passes of the frame set theirs up +#define LAYOUT_UI 1 +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +void main() +{ + uint slot = gl_WorkGroupID.y; + uint place = gl_GlobalInvocationID.x; + if ((water_slots[slot].z < 0.5) || (place >= uint(WATER_DROPS))) + return; + water_drop_s drop = water_drops[slot * uint(WATER_DROPS) + place]; + if (drop.place.z <= 0.0) + return; + water_frame_s frame = water_frame(slot, drop.place.xy); + float lu = length(frame.tu); + float lv = length(frame.tv); + if ((lu < 1e-6) || (lv < 1e-6)) + return; + float reach; + if (drop.motion.w < 1.0) + { + reach = 0.6 * water_flake(drop.place.z); + } + else + { + bool sliding = length(frame.tu * drop.motion.x + frame.tv * drop.motion.y) > 0.002; + reach = water_radius(drop.place.z) * (sliding ? 3.6 : 2.2); + } + vec2 extent = vec2(reach / lu, reach / lv); + ivec2 lower = clamp(ivec2(floor((drop.place.xy - extent) * float(WATER_GRID))), ivec2(0), ivec2(WATER_GRID - 1)); + ivec2 upper = clamp(ivec2(floor((drop.place.xy + extent) * float(WATER_GRID))), ivec2(0), ivec2(WATER_GRID - 1)); + // a drop can't be larger than this many cells, it would only flood the lists + upper = min(upper, lower + ivec2(6)); + uint base = water_grid_base(slot); + for (int y = lower.y; y <= upper.y; ++y) + { + for (int x = lower.x; x <= upper.x; ++x) + { + uint cell = uint(y * WATER_GRID + x); + uint count = atomicAdd(water_cells[base + cell], 1u); + if (count < uint(WATER_CELL_DROPS)) + water_cells[base + uint(WATER_GRID * WATER_GRID) + cell * uint(WATER_CELL_DROPS) + count] = place; + } + } +} diff --git a/shaders/vk14/water_merge.comp b/shaders/vk14/water_merge.comp new file mode 100644 index 00000000..b561808d --- /dev/null +++ b/shaders/vk14/water_merge.comp @@ -0,0 +1,66 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): the drops which touch flow together, the way surface tension pulls them. every +// drop finds the largest of the drops it touches which are larger than itself (by the cell of the grid its middle is in, which +// lists every drop reaching into it); the water goes over to that one in water_gather.comp. flakes of snow don't flow, but the +// ones a blade pushes pile up. a thread per drop, a row of groups per glass + +// none of the pass set: everything the kernel reads is pushed with its dispatch (set COMPUTE_SET, water_sim.glsl), so it runs +// before the passes of the frame set theirs up +#define LAYOUT_UI 1 +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +void main() +{ + uint slot = gl_WorkGroupID.y; + uint place = gl_GlobalInvocationID.x; + if ((water_slots[slot].z < 0.5) || (place >= uint(WATER_DROPS))) + return; + uint index = slot * uint(WATER_DROPS) + place; + water_drop_s drop = water_drops[index]; + water_target[index] = place; + if (drop.place.z <= 0.0) + return; + bool flake = drop.motion.w < 1.0; + bool carried = ((water_state[index] >> 1u) & 7u) != 0u; + if (flake && (false == carried)) + return; + water_frame_s frame = water_frame(slot, drop.place.xy); + float size = flake ? 0.4 * water_flake(drop.place.z) : water_radius(drop.place.z); + uint cell = water_cell(drop.place.xy); + uint base = water_grid_base(slot); + uint count = min(water_cells[base + cell], uint(WATER_CELL_DROPS)); + uint best = place; + float most = drop.place.z; + for (uint k = 0u; k < count; ++k) + { + uint other = water_cells[base + uint(WATER_GRID * WATER_GRID) + cell * uint(WATER_CELL_DROPS) + k]; + if (other == place) + continue; + uint otherindex = slot * uint(WATER_DROPS) + other; + water_drop_s neighbour = water_drops[otherindex]; + float volume = neighbour.place.z; + if ((volume < most) || ((volume == most) && (other > best)) || (volume <= 0.0)) + continue; + if ((volume == drop.place.z) && (other > place)) + continue; + bool otherflake = neighbour.motion.w < 1.0; + if (otherflake != flake) + continue; + // the snow a blade pushes piles up into lumps a few centimetres across, not more + if (otherflake && ((((water_state[otherindex] >> 1u) & 7u) == 0u) || (volume + drop.place.z > 20.0))) + continue; + vec2 step = neighbour.place.xy - drop.place.xy; + float apart = length(frame.tu * step.x + frame.tv * step.y); + float reach = size + (otherflake ? 0.4 * water_flake(volume) : water_radius(volume)); + if (apart < 0.92 * reach) + { + best = other; + most = volume; + } + } + water_target[index] = best; +} diff --git a/shaders/vk14/water_move.comp b/shaders/vk14/water_move.comp new file mode 100644 index 00000000..267bf6f3 --- /dev/null +++ b/shaders/vk14/water_move.comp @@ -0,0 +1,257 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): every drop through the step. a thread per drop, a row of groups per glass. +// a blade going over the drop takes it along in front of itself until it turns back at the end of its sweep or stops, or until +// the drop is pushed out of the sweep; a drop of its own slides when gravity and the air pull it harder than its line of +// contact holds it, wetting its path and leaving droplets behind; a flake melts into a drop + +// none of the pass set: everything the kernel reads is pushed with its dispatch (set COMPUTE_SET, water_sim.glsl), so it runs +// before the passes of the frame set theirs up +#define LAYOUT_UI 1 +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +// how fresh the wetted path is at a place of the glass +float water_fresh(uint slot, vec2 uv) +{ + return water_wetted_at(slot, uv, water_step.y, 8.0); +} + +// the path from one place to another marked wetted, Halfwidth (metres) to the sides: the texels around it get the time and how +// near they are to the middle of the path +void water_wet_path(uint slot, water_frame_s frame, vec2 from, vec2 to, float halfwidth) +{ + uint stamp = (uint(water_step.y * 2.0) + 1u) << 8u; + uint base = water_trail_base(slot); + float texels = float(WATER_TRAIL); + // the half width in texels, a texel and a half more for the filtering + float reach = halfwidth / max(min(length(frame.tu), length(frame.tv)), 1e-6) * texels + 1.5; + int spread = clamp(int(ceil(reach)), 1, 3); + vec2 a = from * texels - 0.5; + vec2 b = to * texels - 0.5; + int steps = clamp(int(ceil(length(b - a) * 2.0)), 1, 24); + for (int s = 1; s <= steps; ++s) + { + vec2 point = mix(a, b, float(s) / float(steps)); + ivec2 centre = ivec2(floor(point + 0.5)); + for (int y = -spread; y <= spread; ++y) + { + for (int x = -spread; x <= spread; ++x) + { + ivec2 texel = centre + ivec2(x, y); + if (any(lessThan(texel, ivec2(0))) || any(greaterThanEqual(texel, ivec2(WATER_TRAIL)))) + continue; + float near = 1.0 - length(vec2(texel) - point) / reach; + if (near <= 0.0) + continue; + atomicMax(water_cells[base + uint(texel.y * WATER_TRAIL + texel.x)], stamp | uint(near * 255.0)); + } + } + } +} + +void main() +{ + uint slot = gl_WorkGroupID.y; + uint place = gl_GlobalInvocationID.x; + if ((water_slots[slot].z < 0.5) || (place >= uint(WATER_DROPS))) + return; + uint index = slot * uint(WATER_DROPS) + place; + water_drop_s drop = water_drops[index]; + if (drop.place.z <= 0.0) + return; + float seconds = water_step.x; + uint state = water_state[index]; + vec4 extra = water_extra[index]; + vec2 uv = drop.place.xy; + water_frame_s frame = water_frame(slot, uv); + if (frame.valid < 0.25) + { + water_remove(index); + return; + } + uint seed = water_pcg4d(uvec4(water_counts.x, slot, place, 9u)).x; + + // a flake melts into a drop: the glass is warmed by the cab, the faster the warmer the air + bool flake = drop.motion.w < 1.0; + if (flake) + { + float warm = water_up.w; + float melt = (warm > 0.5) ? mix(3.0, 10.0, drop.motion.z) / (1.0 + 0.25 * warm) : mix(15.0, 45.0, drop.motion.z); + drop.motion.w = min(drop.motion.w + seconds / melt, 1.0); + flake = drop.motion.w < 1.0; + } + + // the blades: one going over the drop in the step takes it along + uint carried = (state >> 1u) & 7u; + float way = (((state >> 4u) & 1u) != 0u) ? 1.0 : -1.0; + vec4 mask = water_blades_at(slot, uv); + vec4 reached = water_blades_texel(slot, uv); + for (int k = 0; k < 4; ++k) + { + float before = water_blades_before[k]; + float now = water_blades_now[k]; + if ((before < 0.0) || (now < 0.0)) + { + if (carried == uint(k + 1)) + carried = 0u; + continue; + } + float moved = now - before; + if (carried == uint(k + 1)) + { + // the blade turned back at the end of its sweep or stopped: the water stays where it brought it + if ((moved * way <= 0.0) || (now >= 0.995) || (now <= 0.005)) + carried = 0u; + } + // a blade goes over a drop which was ahead of it (by a little more than nothing: the water it left where it turned back at + // the end of its sweep isn't taken back) and is behind it now + else if ((carried == 0u) && (moved != 0.0) && (reached[k] >= 0.001) && ((mask[k] - before) * sign(moved) > 0.003) && ((mask[k] - now) * sign(moved) <= 0.0)) + { + carried = uint(k + 1); + way = (moved > 0.0) ? 1.0 : -1.0; + // the blade doesn't take all of it: now and then a droplet stays behind + vec4 r = water_random(seed, 5u, place, slot); + if ((false == flake) && (drop.place.z > 1.0) && (r.x < 0.35)) + { + float left = mix(0.02, 0.08, r.y); + if (water_add(slot, seed ^ 0x9e3779b9u, uv, left, 1.0, r.z, false)) + drop.place.z -= left; + } + } + } + + vec3 velocity = frame.tu * drop.motion.x + frame.tv * drop.motion.y; + if (carried != 0u) + { + // in front of the blade by its own size and the lip of the bead: the place where the mask is as far ahead of the blade, + // found along the gradient of the mask (along the arc of the blade) + int k = int(carried) - 1; + float radius = flake ? 0.5 * water_flake(drop.place.z) : water_radius(drop.place.z); + float a = dot(frame.tu, frame.tu); + float b = dot(frame.tu, frame.tv); + float c = dot(frame.tv, frame.tv); + float det = max(a * c - b * b, 1e-20); + float h = 1.0 / float(WATER_MAP); + bool edge = false; + for (int step = 0; step < 4; ++step) + { + // at the edge of the sweep (the end of the blade) the water squeezes out and stays + if ((water_blades_texel(slot, uv + vec2(h, 0.0))[k] < 0.001) || (water_blades_texel(slot, uv - vec2(h, 0.0))[k] < 0.001) || + (water_blades_texel(slot, uv + vec2(0.0, h))[k] < 0.001) || (water_blades_texel(slot, uv - vec2(0.0, h))[k] < 0.001)) + { + edge = true; + break; + } + float m = water_blades_at(slot, uv)[k]; + vec2 gradient = vec2(water_blades_at(slot, uv + vec2(h, 0.0))[k] - water_blades_at(slot, uv - vec2(h, 0.0))[k], + water_blades_at(slot, uv + vec2(0.0, h))[k] - water_blades_at(slot, uv - vec2(0.0, h))[k]) / (2.0 * h); + float g2 = dot(gradient, gradient); + if (g2 < 1e-6) + break; + // how fast the mask changes along the glass, per metre + float permetre = sqrt(max((c * gradient.x * gradient.x - 2.0 * b * gradient.x * gradient.y + a * gradient.y * gradient.y) / det, 0.0)); + float target = water_blades_now[k] + way * (0.0015 + radius) * permetre; + vec2 delta = gradient * ((target - m) / g2); + float length_delta = length(delta); + if (length_delta > 0.02) + delta *= 0.02 / length_delta; + uv += delta; + if (length_delta < 1e-5) + break; + } + // pushed out of the sweep it stays at its edge + if (edge || (water_blades_texel(slot, uv)[k] < 0.001)) + carried = 0u; + velocity = vec3(0.0); + } + else if (false == flake) + { + float volume = drop.place.z; + float radius = water_radius(volume); + vec3 n = frame.normal; + vec3 up = water_up.xyz; + // gravity along the glass + float weight = 1000.0 * WATER_GRAVITY * volume * 1e-9; + vec3 force = -(up - n * dot(up, n)) * weight; + // the drag of the air going along the glass, with a share of the air meeting it head on turned up along it + vec3 air = water_air.xyz; + vec3 rise = up - n * dot(up, n); + rise = (length(rise) > 0.1) ? normalize(rise) : vec3(0.0); + vec3 flow = (air - n * dot(air, n) + rise * (max(-dot(air, n), 0.0) * WATER_HEAD_ON)) * WATER_BOUNDARY; + float area = 2.0 * radius * (0.364 * radius) * 0.8; + force += flow * (0.6 * area * length(flow)); + // the hold of the line of contact, uneven over the glass, much less on a path wetted lately + vec2 spread = uv * vec2(length(frame.tu), length(frame.tv)); + float uneven = mix(0.55, 1.45, water_noise(spread / 0.003, 3u + slot)); + float hold = WATER_TENSION * 2.0 * radius * WATER_HOLD * uneven * mix(1.0, 0.3, water_fresh(slot, uv)) * mix(0.85, 1.15, drop.motion.z); + float pull = length(force); + vec3 target = vec3(0.0); + if (pull > hold) + { + vec3 dir = force / pull; + // the way bent towards the side which holds the drop less: the wetter one (the water keeps to the wetted paths) and the + // one where the glass is cleaner, so it meanders + vec3 side = cross(n, dir); + float offset = max(radius, 0.0015); + vec2 leftuv = uv + water_to_uv(frame, side * offset); + vec2 rightuv = uv - water_to_uv(frame, side * offset); + float wetter = water_fresh(slot, leftuv) - water_fresh(slot, rightuv); + float cleaner = water_noise(rightuv * vec2(length(frame.tu), length(frame.tv)) / 0.003, 3u + slot) - water_noise(leftuv * vec2(length(frame.tu), length(frame.tv)) / 0.003, 3u + slot); + float wander = water_noise(spread / 0.02 + vec2(17.0 * drop.motion.z), 11u + slot) - 0.5; + dir = normalize(dir + side * (wetter * 0.6 + cleaner * 0.5 + wander * 0.35)); + target = dir * min((pull - hold) / (WATER_VISCOUS * radius), 1.5); + } + velocity += (target - velocity) * (1.0 - exp(-seconds / ((pull > hold) ? 0.05 : 0.015))); + velocity -= n * dot(velocity, n); + vec3 moved = velocity * seconds; + float travel = length(moved); + if (travel > 1e-7) + { + vec2 next = uv + water_to_uv(frame, moved); + water_wet_path(slot, frame, uv, next, 0.4 * radius); + // a sliding drop leaves droplets behind as it goes, and thins; a large one breaks up the faster (a lot of water runs as + // a stream of beads rather than as one big drop) + extra.x += travel; + if ((extra.x >= extra.y) && (volume > 1.5)) + { + vec4 r = water_random(seed, 6u, place, slot); + float left = clamp(volume * mix(0.01, 0.03, r.x) * (1.0 + volume / 40.0), 0.01, 0.25 * volume); + vec3 behind = -(velocity / max(length(velocity), 1e-6)) * ((radius + 1.2 * water_radius(left)) * 1.05); + if (water_add(slot, seed ^ 0x85ebca6bu, uv + water_to_uv(frame, behind), left, 1.0, r.y, false)) + drop.place.z -= left; + extra.x = 0.0; + extra.y = mix(0.002, 0.005, r.z) / (1.0 + volume / 100.0); + } + uv = next; + } + // without the rain the water dries, slowly + if (water_step.z <= 0.0) + drop.place.z -= seconds * 0.004 * radius * 1000.0; + } + else + { + velocity = vec3(0.0); + } + + if ((drop.place.z < WATER_LEAST) || any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0)))) + { + water_remove(index); + return; + } + water_frame_s there = water_frame(slot, uv); + if (there.valid < 0.25) + { + // off the glass, it falls off + water_remove(index); + return; + } + drop.place.xy = uv; + drop.place.w += seconds; + drop.motion.xy = water_to_uv(there, velocity); + water_drops[index] = drop; + water_extra[index] = extra; + water_state[index] = 1u | (carried << 1u) | ((way > 0.0) ? 16u : 0u); +} diff --git a/shaders/vk14/water_settle.comp b/shaders/vk14/water_settle.comp new file mode 100644 index 00000000..2f3278e2 --- /dev/null +++ b/shaders/vk14/water_settle.comp @@ -0,0 +1,39 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): the drops which gathered others take their water, move to the middle of it and +// slow down as much as the still water slows them; the header of the glass tells the glasses it's simulated. a thread per drop, +// a row of groups per glass + +// none of the pass set: everything the kernel reads is pushed with its dispatch (set COMPUTE_SET, water_sim.glsl), so it runs +// before the passes of the frame set theirs up +#define LAYOUT_UI 1 +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +void main() +{ + uint slot = gl_WorkGroupID.y; + uint place = gl_GlobalInvocationID.x; + if ((water_slots[slot].z < 0.5) || (place >= uint(WATER_DROPS))) + return; + if (place == 0u) + water_header[slot] = vec4(1.0, water_step.y, 0.0, 0.0); + uint index = slot * uint(WATER_DROPS) + place; + int gathered = water_gather[index * 4u]; + if (gathered == 0) + return; + vec2 moment = vec2(float(water_gather[index * 4u + 1u]), float(water_gather[index * 4u + 2u])) * 1e-6; + water_gather[index * 4u] = 0; + water_gather[index * 4u + 1u] = 0; + water_gather[index * 4u + 2u] = 0; + water_drop_s drop = water_drops[index]; + if (drop.place.z <= 0.0) + return; + float volume = drop.place.z + float(gathered) * 1e-4; + drop.place.xy += moment / volume; + drop.motion.xy *= drop.place.z / volume; + drop.place.z = volume; + water_drops[index] = drop; +} diff --git a/shaders/vk14/water_sim.glsl b/shaders/vk14/water_sim.glsl new file mode 100644 index 00000000..82710150 --- /dev/null +++ b/shaders/vk14/water_sim.glsl @@ -0,0 +1,279 @@ +// what the passes of the simulation of the water on the windscreens share (water_*.comp): the block of the step, the random +// numbers, the map of the glass, the forces on a drop +// +// the physics (the drops are caps of spheres flat on the glass, 40 degrees at their edge, untreated weathered glass): +// - a drop lies held by its line of contact: the hold is the surface tension times the width of the drop times the difference +// of the cosines of its angles at the front and the rear (WATER_HOLD), so it grows with the radius, while the weight grows +// with the cube of it. on a vertical glass a drop slides once its footprint is about 7 mm across (25 mm3), on a glass leaning +// back later. the hold is uneven over the glass (dirt, scratches: the drops stop and go) and much less where the water went +// lately (a wetted path: the next drops follow it and it becomes a rivulet) +// - the air going along the glass drags the drops (with the area they stand up into it, within the layer of slow air at the +// glass), and of the air meeting the glass head on a share turns up along it: at speed the water goes up the glass, the sooner +// the more it leans back +// - a sliding drop is held back by the viscosity in proportion to its speed and radius; it leaves droplets behind as it goes +// (it thins and stops in the end) and wets its path +// - drops which touch flow together into one (surface tension), the water and the momentum of the places kept +// - a raindrop hits the glass far above the speed at which it splashes: it spreads and throws droplets around, which stay +// - a blade going over a drop takes it along in front of itself; the water it gathers flows together into a bead along it, left +// at the ends of its sweep (where it runs down as streams) and pushed off where the glass ends + +#ifndef WATER_SIM_GLSL +#define WATER_SIM_GLSL + +#include "water.glsl" + +// gl::water_ubs, every vector in the space of the model of the cab +layout(std140, set = COMPUTE_SET, binding = 1) uniform water_ubo +{ + // x: seconds of the step, y: seconds the simulation runs, z: how heavy the rain or the snow is 0 .. 1, w: 1 for snow + vec4 water_step; + // xyz: up, w: the temperature of the air, degrees Celsius + vec4 water_up; + // xyz: the air against the cab, m/s; w: how fast the rain or the snow falls, m/s + vec4 water_air; + // the blades now and at the step before: 0 .. 1 across their sweep, below 0 where there's none + vec4 water_blades_now; + vec4 water_blades_before; + // x: the smallest diameter of a raindrop landing as a drop of its own, mm, y: 1 / the mean of the diameters above it, 1 / mm + vec4 water_rain; + // x: random seed of the step, y: the most landings tried on a glass, z: the glass a single glass pass works on + uvec4 water_counts; + // every glass: x the landings tried in the step, y 1 / the most metres squared a unit of its uv holds, z 1 when it's simulated, + // w the size of its drops (rain_scale of its material) + vec4 water_slots[WATER_SLOTS]; +}; + +const float WATER_GRAVITY = 9.81; +// the surface tension of water, N/m, and the share of it a drop is held by for the width of its footprint +const float WATER_TENSION = 0.072; +const float WATER_HOLD = 0.5; +// the viscous drag of a sliding drop for its radius and speed, N s / m2 +const float WATER_VISCOUS = 1.3; +// the share of the speed of the air the drops feel in the slow layer at the glass, and of the air meeting the glass head on +// which turns up along it +const float WATER_BOUNDARY = 0.3; +const float WATER_HEAD_ON = 0.35; +// the drops below this much water (mm3) dry at once; a raindrop or a droplet of a splash smaller than WATER_SEEN (0.5 mm across +// its footprint, hardly a pixel) landing where there's no drop is left to the mist of the glass +const float WATER_LEAST = 0.004; +const float WATER_SEEN = 0.075; + +uvec4 water_pcg4d(uvec4 v) +{ + v = v * 1664525u + 1013904223u; + v.x += v.y * v.w; + v.y += v.z * v.x; + v.z += v.x * v.y; + v.w += v.y * v.z; + v ^= v >> 16u; + v.x += v.y * v.w; + v.y += v.z * v.x; + v.z += v.x * v.y; + v.w += v.y * v.z; + return v; +} + +// four random numbers 0 .. 1 +vec4 water_random(uint a, uint b, uint c, uint d) +{ + return vec4(water_pcg4d(uvec4(a, b, c, d))) * (1.0 / 4294967296.0); +} + +// value noise 0 .. 1 +float water_noise(vec2 p, uint layer) +{ + vec2 i = floor(p); + vec2 f = p - i; + vec2 u = f * f * (3.0 - 2.0 * f); + uvec2 c = uvec2(ivec2(i)); + float a = water_random(c.x, c.y, layer, 0u).x; + float b = water_random(c.x + 1u, c.y, layer, 0u).x; + float d = water_random(c.x, c.y + 1u, layer, 0u).x; + float e = water_random(c.x + 1u, c.y + 1u, layer, 0u).x; + return mix(mix(a, b, u.x), mix(d, e, u.x), u.y); +} + +// the glass at a place of the uv: the steps along it for the steps of u and v (metres), the way out of the cab and whether the +// place is on the glass (1), next to it (0.5) or off it (0) +struct water_frame_s +{ + vec3 tu; + vec3 tv; + vec3 normal; + float valid; +}; + +water_frame_s water_frame(uint slot, vec2 uv) +{ + water_frame_s frame = water_frame_s(vec3(0.0), vec3(0.0), vec3(0.0, 0.0, 1.0), 0.0); + if (any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0)))) + return frame; + uint base = slot * uint(WATER_MAP * WATER_MAP); + vec2 p = uv * float(WATER_MAP) - 0.5; + ivec2 i = ivec2(floor(p)); + vec2 f = p - vec2(i); + float total = 0.0; + float side = 0.0; + for (int k = 0; k < 4; ++k) + { + ivec2 texel = clamp(i + ivec2(k & 1, k >> 1), ivec2(0), ivec2(WATER_MAP - 1)); + uint index = base + uint(texel.y * WATER_MAP + texel.x); + vec4 a = water_axes[index * 2u]; + vec4 b = water_axes[index * 2u + 1u]; + if (a.w <= 0.0) + continue; + float w = (((k & 1) != 0) ? f.x : 1.0 - f.x) * (((k >> 1) != 0) ? f.y : 1.0 - f.y) + 1e-4; + frame.tu += a.xyz * w; + frame.tv += b.xyz * w; + side += b.w * w; + total += w; + } + if (total <= 0.0) + return frame; + frame.tu /= total; + frame.tv /= total; + vec3 n = cross(frame.tu, frame.tv); + float length_n = length(n); + if (length_n < 1e-12) + return frame; + frame.normal = n * (((side < 0.0) ? -1.0 : 1.0) / length_n); + // on the glass by the texel the place is in + ivec2 nearest = clamp(ivec2(floor(uv * float(WATER_MAP))), ivec2(0), ivec2(WATER_MAP - 1)); + frame.valid = water_axes[(base + uint(nearest.y * WATER_MAP + nearest.x)) * 2u].w; + return frame; +} + +// a step along the glass (metres, the space of the cab) as a step of the uv +vec2 water_to_uv(water_frame_s frame, vec3 step) +{ + float a = dot(frame.tu, frame.tu); + float b = dot(frame.tu, frame.tv); + float c = dot(frame.tv, frame.tv); + float det = a * c - b * b; + if (abs(det) < 1e-20) + return vec2(0.0); + vec2 r = vec2(dot(frame.tu, step), dot(frame.tv, step)); + return vec2(c * r.x - b * r.y, a * r.y - b * r.x) / det; +} + +// the blades at a place of the uv, filtered between the texels of the map +vec4 water_blades_at(uint slot, vec2 uv) +{ + uint base = slot * uint(WATER_MAP * WATER_MAP); + vec2 p = clamp(uv, vec2(0.0), vec2(1.0)) * float(WATER_MAP) - 0.5; + ivec2 i = ivec2(floor(p)); + vec2 f = p - vec2(i); + vec4 result = vec4(0.0); + for (int k = 0; k < 4; ++k) + { + ivec2 texel = clamp(i + ivec2(k & 1, k >> 1), ivec2(0), ivec2(WATER_MAP - 1)); + float w = (((k & 1) != 0) ? f.x : 1.0 - f.x) * (((k >> 1) != 0) ? f.y : 1.0 - f.y); + result += water_blades[base + uint(texel.y * WATER_MAP + texel.x)] * w; + } + return result; +} + +// the blades at the texel of the map holding a place of the uv, unfiltered: where a blade reaches it's at least 0.001 (the +// filtered values fall off across a texel at the edge of the sweep) +vec4 water_blades_texel(uint slot, vec2 uv) +{ + ivec2 texel = clamp(ivec2(floor(uv * float(WATER_MAP))), ivec2(0), ivec2(WATER_MAP - 1)); + return water_blades[slot * uint(WATER_MAP * WATER_MAP) + uint(texel.y * WATER_MAP + texel.x)]; +} + +// a free place for a drop of the glass, taken; ~0u when none was found +uint water_claim(uint slot, uint seed) +{ + for (uint k = 0u; k < 8u; ++k) + { + uint place = water_pcg4d(uvec4(seed, k, slot, 77u)).x % uint(WATER_DROPS); + if (atomicCompSwap(water_state[slot * uint(WATER_DROPS) + place], 0u, 1u) == 0u) + return place; + } + return ~0u; +} + +// a new drop of the glass: Volume mm3 at a place, a flake of snow when Melted is below 1. false when the glass is full. the +// landing rain and snow get only nine tenths of the places, so there are always some for the droplets the moving drops leave +bool water_add(uint slot, uint seed, vec2 uv, float volume, float melted, float shape, bool landing) +{ + if ((volume < WATER_LEAST) || any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0)))) + return false; + if (landing && (water_counts_of[slot].x >= uint(WATER_DROPS) / 10u * 9u)) + return false; + uint place = water_claim(slot, seed); + if (place == ~0u) + return false; + atomicAdd(water_counts_of[slot].x, 1u); + uint index = slot * uint(WATER_DROPS) + place; + water_drops[index].place = vec4(uv, volume, 0.0); + water_drops[index].motion = vec4(0.0, 0.0, shape, melted); + water_extra[index] = vec4(0.0, 0.004 * (0.6 + 0.8 * shape), 0.0, 0.0); + water_target[index] = place; + for (uint k = 0u; k < 4u; ++k) + water_gather[index * 4u + k] = 0; + return true; +} + +// the slot of the drop on the glass freed, with what was flowing into it +void water_remove(uint index) +{ + water_drops[index].place.z = 0.0; + water_state[index] = 0u; + atomicAdd(water_counts_of[index / uint(WATER_DROPS)].x, ~0u); + for (uint k = 0u; k < 4u; ++k) + water_gather[index * 4u + k] = 0; +} + +// Volume mm3 of water flowing into a drop (index in the storage) from Offset (uv) away from its middle, taken by +// water_settle.comp +void water_pour(uint index, float volume, vec2 offset) +{ + atomicAdd(water_gather[index * 4u], int(volume * 1e4)); + atomicAdd(water_gather[index * 4u + 1u], int(offset.x * volume * 1e6)); + atomicAdd(water_gather[index * 4u + 2u], int(offset.y * volume * 1e6)); +} + +// the drop lying at a place of the glass which water of Radius (metres) landing there touches, the largest of them, by the grid +// of the step before (~0u: none) +uint water_landing_on(uint slot, water_frame_s frame, vec2 uv, float radius) +{ + uint cell = water_cell(uv); + uint base = water_grid_base(slot); + uint count = min(water_cells[base + cell], uint(WATER_CELL_DROPS)); + uint best = ~0u; + float most = 0.0; + for (uint k = 0u; k < count; ++k) + { + uint other = water_cells[base + uint(WATER_GRID * WATER_GRID) + cell * uint(WATER_CELL_DROPS) + k]; + water_drop_s drop = water_drops[slot * uint(WATER_DROPS) + other]; + if ((drop.place.z <= most) || (drop.motion.w < 1.0)) + continue; + vec2 step = drop.place.xy - uv; + if (length(frame.tu * step.x + frame.tv * step.y) < 0.9 * (radius + water_radius(drop.place.z))) + { + best = other; + most = drop.place.z; + } + } + return best; +} + +// water landing on the glass: it flows into a drop it touches, else it's a new drop where it's large enough to be seen +void water_land(uint slot, uint seed, water_frame_s frame, vec2 uv, float volume, float shape) +{ + if (any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0)))) + return; + uint onto = water_landing_on(slot, frame, uv, water_radius(volume)); + if (onto != ~0u) + { + uint index = slot * uint(WATER_DROPS) + onto; + water_pour(index, volume, uv - water_drops[index].place.xy); + } + else if (volume >= WATER_SEEN) + { + water_add(slot, seed, uv, volume, 1.0, shape, true); + } +} + +#endif diff --git a/shaders/vk14/water_spawn.comp b/shaders/vk14/water_spawn.comp new file mode 100644 index 00000000..578e6c0c --- /dev/null +++ b/shaders/vk14/water_spawn.comp @@ -0,0 +1,68 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): the rain or the snow landing on the glasses in the step. a thread per landing +// tried, a row of groups per glass: a place of the uv picked at random is taken as often as the glass there gets hit (the +// metres squared the place holds, how much it faces the rain coming at it), so the landings go evenly over the glass. a raindrop +// spreads and throws a splash of droplets around, along the way it came, which flow into the drops they land on (water_settle.comp +// takes the water); a flake sticks where it lands + +// none of the pass set: everything the kernel reads is pushed with its dispatch (set COMPUTE_SET, water_sim.glsl), so it runs +// before the passes of the frame set theirs up +#define LAYOUT_UI 1 +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +void main() +{ + uint slot = gl_WorkGroupID.y; + uint landing = gl_GlobalInvocationID.x; + if ((water_slots[slot].z < 0.5) || (float(landing) >= water_slots[slot].x)) + return; + vec4 r = water_random(water_counts.x, slot, landing, 0u); + vec2 uv = r.xy; + water_frame_s frame = water_frame(slot, uv); + if (frame.valid < 0.75) + return; + // the rain coming at the glass, against the cab + vec3 coming = water_air.xyz - water_up.xyz * water_air.w; + float speed = length(coming); + if (speed < 1e-3) + return; + float facing = max(-dot(coming, frame.normal), 0.0) / speed; + float density = length(cross(frame.tu, frame.tv)); + if (r.z >= density * water_slots[slot].y * facing) + return; + float scale = max(water_slots[slot].w, 0.05); + vec4 s = water_random(water_counts.x, slot, landing, 1u); + uint seed = water_pcg4d(uvec4(water_counts.x, slot, landing, 2u)).x; + if (water_step.w > 0.5) + { + // a flake, a few mm of snow holding a little water + water_add(slot, seed, uv, mix(0.3, 2.5, s.x) * scale * scale * scale, 0.0, s.y, true); + return; + } + // the diameter of the raindrop, mm: above the least landing as a drop of its own they go as Marshall-Palmer (exponential) + float diameter = min(water_rain.x - log(max(r.w, 1e-6)) / max(water_rain.y, 0.1), 5.0) * scale; + float drop = 0.5236 * diameter * diameter * diameter; + // most of it stays where it hit, the rest is thrown around as droplets, forward along the glass + float kept = mix(0.55, 0.85, s.x); + water_land(slot, seed, frame, uv, drop * kept, s.y); + vec3 along = coming - frame.normal * dot(coming, frame.normal); + float glancing = length(along) / speed; + vec3 forward = (glancing > 1e-3) ? normalize(along) : normalize(frame.tu); + vec3 across = cross(frame.normal, forward); + float radius = water_radius(drop * kept); + int splash = 2 + int(s.z * 4.0); + for (int k = 0; k < splash; ++k) + { + vec4 e = water_random(water_counts.x, slot, landing, 3u + uint(k)); + float angle = e.x * 6.2831853; + vec3 way = cos(angle) * forward + sin(angle) * across + forward * (1.5 * glancing); + way = (length(way) > 1e-3) ? normalize(way) : forward; + float distance = radius * (1.3 + 2.5 * e.y) * (1.0 + glancing); + float volume = drop * (1.0 - kept) / float(splash) * mix(0.4, 1.6, e.z); + water_land(slot, seed + 101u * uint(k + 1), frame, uv + water_to_uv(frame, way * distance), volume, e.w); + } +} diff --git a/shaders/vk14/water_trail.comp b/shaders/vk14/water_trail.comp new file mode 100644 index 00000000..6cd602dc --- /dev/null +++ b/shaders/vk14/water_trail.comp @@ -0,0 +1,37 @@ +#version 460 core +#define STAGE_COMPUTE 1 +// the water on the windscreens (water_sim.glsl): the blades going over the wetted paths in the step wipe them. a thread per texel +// of the map of the paths, a row of groups per glass + +// none of the pass set: everything the kernel reads is pushed with its dispatch (set COMPUTE_SET, water_sim.glsl), so it runs +// before the passes of the frame set theirs up +#define LAYOUT_UI 1 +#include "common.glsl" +#include "water_sim.glsl" + +layout(local_size_x = WATER_GROUP_SIZE) in; + +void main() +{ + uint slot = gl_WorkGroupID.y; + uint texel = gl_GlobalInvocationID.x; + if ((water_slots[slot].z < 0.5) || (texel >= uint(WATER_TRAIL * WATER_TRAIL))) + return; + uint index = water_trail_base(slot) + texel; + if (water_cells[index] == 0u) + return; + vec2 uv = (vec2(float(texel % uint(WATER_TRAIL)), float(texel / uint(WATER_TRAIL))) + 0.5) / float(WATER_TRAIL); + vec4 mask = water_blades_at(slot, uv); + for (int k = 0; k < 4; ++k) + { + float before = water_blades_before[k]; + float now = water_blades_now[k]; + if ((mask[k] < 0.001) || (before < 0.0) || (now < 0.0) || (before == now)) + continue; + if ((mask[k] > min(before, now)) && (mask[k] <= max(before, now))) + { + water_cells[index] = 0u; + return; + } + } +}