diff --git a/CMakeLists.txt b/CMakeLists.txt index 31cb327c..0258f915 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -393,6 +393,7 @@ if (WITH_OPENGL_MODERN) "rendering/opengl46/opengl46shadowfit.cpp" "rendering/opengl46/opengl46precipitation.cpp" "rendering/opengl46/opengl46particles.cpp" + "rendering/opengl46/opengl46gpuparticles.cpp" "rendering/opengl46/opengl46terrain.cpp" "rendering/opengl46/opengl46grass.cpp" "rendering/opengl46/opengl46renderer.cpp") diff --git a/docs/opengl46-renderer.md b/docs/opengl46-renderer.md index 35a97dbd..9a2b7ab5 100644 --- a/docs/opengl46-renderer.md +++ b/docs/opengl46-renderer.md @@ -259,6 +259,32 @@ are no longer loaded. | `gfx.moon.disc` | `1.0` | 0..16: brightness of the moon disc | | `gfx.moonlight` | `1.0` | 0..10: how much the moon lights the scenery at night (its direct light, the night light that comes with it and the moonlit sky); 0: no moonlight | +### Particles + +- **Smoke** of vehicles and scenery models is simulated and drawn on the graphics card. The sources (the `smokesource_` + points of the models and their `data/*.txt` templates, unchanged) work out on the processor only how many particles they + give off and what these start with, as before; the particles themselves live in a buffer of the graphics card and are + moved there once per step of the simulation, by the same rules as on the processor. The particles of a step are spread + along the way the source went in it, so the smoke of a fast train no longer comes out in clumps. Each view leaves out + what it doesn't see and lights the rest with the sun (with the shadows of the terrain, models and clouds), the sky and + the lamps and headlights, as the surfaces are lit: smoke in the shade is darker, smoke against the sun glows, smoke in + the beam of headlights at night is lit by it. +- **Rain and snow** are particles in three boxes around the camera (10, 24 and 60 metres), more of them the heavier the + rain or snow (the overcast above 1). The drops and flakes fall, drift with the wind, the flakes flutter, and each is + drawn as a streak of its motion against the camera: they slant when the train moves. They are lit like the smoke, so the + rain shows in the headlights and under the lamps, and fogged. The cab still hides them as before. They don't stop under + roofs or in tunnels yet (neither did the old cylinder). +- **Soft particles**: smoke, rain and snow fade where they near the scenery behind them instead of being cut by it + (the depth of the view drawn so far is copied for it once per view). + +| Entry | Default | Meaning | +|---|---|---| +| `gfx.smoke.gpu yes/no` | `yes` | the smoke on the graphics card, see above; `no`: simulated on the processor and drawn as before | +| `gfx.smoke.lighting yes/no` | `yes` | the smoke of the graphics card lit by the sun, the sky and the lamps; `no`: in the colour the processor gave it, as before | +| `gfx.particles.soft` | `1.0` | 0..4: how far in front of the scenery the particles fade out, as a share of the half size of a particle of smoke, and times 0.25 m for rain and snow; 0: cut, as before | +| `gfx.precipitation.gpu yes/no` | `yes` | rain and snow as particles, see above; `no`: the textured cylinder, as before | +| `gfx.precipitation.particles` | `65536` | 0..1048576: the particles of the heaviest rain or snow; lighter rain has fewer, down to 15% of it | + ### Models drawn from the GPU Many models of the scenery (trees, bushes, poles, fences: models without lights and animations) are drawn in batches, one @@ -447,6 +473,11 @@ editor goes through the first `dynamiclights` headlights, the reflections throug under `gpu steps:` are measured each from its own pair of marks, and the first mark of a pair can be put while the graphics card still works on what came before, so a short step right after a heavy one may show some of that work; the parts don't. Like the other timings, they are of a frame a few frames back. +- **Particles** window (next to the Weather window): the switches above. Shift+F12 shows the `smoke of the gpu:` row + (particles drawn, as counted on the graphics card a few frames before, the places for the particles and the sources they + are shared by) and, while it rains or snows, `rain of the gpu:` / `snow of the gpu:` (drawn of how many); the timings + `particles:` give the GPU time of the step of the smoke, of the smoke of the view (culling, light and drawing) and of the + rain or snow. - `gfx.gldebug yes` turns on the OpenGL debug output (`KHR_debug`); messages go to the log. ## Building diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index b58d48b7..16fcb3f3 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -29,6 +29,12 @@ void gl::glsl_common_setup() "#define HIZ_GROUP_SIZE " + std::to_string(HIZ_GROUP_SIZE) + "\n" + "#define SHADOW_FIT_GROUP_SIZE " + std::to_string(SHADOW_FIT_GROUP_SIZE) + "\n" + "#define SHADOW_FIT_SSBO_BINDING " + std::to_string(SHADOW_FIT_SSBO_BINDING) + "\n" + + "#define PARTICLE_GROUP_SIZE " + std::to_string(PARTICLE_GROUP_SIZE) + "\n" + + "#define PARTICLE_SSBO_BINDING " + std::to_string(PARTICLE_SSBO_BINDING) + "\n" + + "#define PARTICLE_SOURCE_SSBO_BINDING " + std::to_string(PARTICLE_SOURCE_SSBO_BINDING) + "\n" + + "#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 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 1b171cac..8d95caab 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -496,6 +496,50 @@ namespace gl const GLuint SHADOW_FIT_GROUP_SIZE = 16; const GLuint SHADOW_FIT_SSBO_BINDING = 9; + // the particles of the gpu (opengl46_gpu_particles, particles_*.comp, particle_common.glsl): work group of their passes and the + // binding points of their buffers. the points are ones of the instance culling, which binds its buffers before every use; the + // particles bind their own before their passes. the draws read the particles as attributes of instances, not from storage + const GLuint PARTICLE_GROUP_SIZE = 64; + const GLuint PARTICLE_SSBO_BINDING = 9; // the smoke particles + const GLuint PARTICLE_SOURCE_SSBO_BINDING = 10; // their sources, as the last update of the simulation left them + const GLuint PARTICLE_OWNER_SSBO_BINDING = 11; // the source of every place of the particles + const GLuint PARTICLE_OUTPUT_SSBO_BINDING = 12; // what the draw reads, a particle_draw per particle drawn + const GLuint PARTICLE_COMMAND_SSBO_BINDING = 13; // the indirect draw command, whose instances the passes count + + // layout std430: GLSL smoke_particle_s + struct particle_smoke + { + glm::vec4 position; + glm::vec4 velocity; + glm::vec4 state; + }; + static_assert(sizeof(particle_smoke) == 48, "bad size of particle_smoke"); + + // layout std430: GLSL smoke_source_s + struct particle_smoke_source + { + glm::uvec4 pool; + glm::vec4 location; + glm::vec4 previous; + glm::vec4 launch; + glm::vec4 initial; + glm::vec4 sizechange; + glm::vec4 opacitychange; + glm::vec4 motion; + glm::vec4 color; + glm::vec4 oldcolor; + }; + static_assert(sizeof(particle_smoke_source) == 160, "bad size of particle_smoke_source"); + + // layout std430: GLSL particle_draw_s, read by the draws as attributes of instances + struct particle_draw + { + glm::vec4 place; + glm::vec4 shape; + glm::vec4 color; + }; + static_assert(sizeof(particle_draw) == 48, "bad size of particle_draw"); + // layout std430: a bucket of a scene cell queued in the pass (GLSL: instance_cull_job) struct instance_cull_job { diff --git a/rendering/opengl46/opengl46gpuparticles.cpp b/rendering/opengl46/opengl46gpuparticles.cpp new file mode 100644 index 00000000..0e6e140b --- /dev/null +++ b/rendering/opengl46/opengl46gpuparticles.cpp @@ -0,0 +1,472 @@ +/* +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/opengl46gpuparticles.h" + +#include "rendering/particles.h" +#include "simulation/simulationenvironment.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/Timer.h" +#include "utilities/utilities.h" +#include "gl/vao.h" + +namespace +{ + +// places of the smoke at most, all sources together (48 bytes each, as many again for the draw); the sources past it get none +std::uint32_t constexpr MAX_SMOKE_PLACES{1u << 19}; +// the origin of the smoke moves by whole steps when the camera gets this far from it +double constexpr ORIGIN_STEP{256.0}; +double constexpr ORIGIN_REACH{1024.0}; +// how much the smoke scatters the light onwards (Henyey-Greenstein) +float constexpr SMOKE_FORWARD{0.3f}; + +// the boxes of the rain and snow around the camera: their sizes (metres) and the shares of the particles in each +std::array constexpr BOX_SIZE{10.f, 24.f, 60.f}; +std::array constexpr BOX_SHARE{0.2f, 0.35f, 0.45f}; + +// what the rain and the snow are like: the speed of the fall (m/s), how long a streak lasts (s, the eye's or the camera's +// exposure), opacity, how much a drop or a flake scatters the light onwards and how bright it is against a white surface +struct precipitation_kind +{ + float fall; + float exposure; + float opacity; + float forward; + float brightness; +}; +precipitation_kind constexpr RAIN{7.f, 0.025f, 0.5f, 0.6f, 0.6f}; +precipitation_kind constexpr SNOW{1.1f, 0.01f, 0.85f, 0.25f, 0.9f}; +// the share of the wind of the environment the drops and flakes drift with (as the old cylinder leant with it) +double constexpr WIND_SHARE{0.5}; +// the camera faster than this from a frame to the next was moved, not driven or flown +double constexpr CAMERA_JUMP{120.0}; + +std::unique_ptr make_program(std::vector const &Names) +{ + std::vector> shaders; + std::vector> references; + for (auto const &name : Names) + { + shaders.emplace_back(std::make_unique(name)); + references.emplace_back(*shaders.back()); + } + return std::make_unique(references); +} + +std::uint32_t groups(std::uint32_t const Count) +{ + return (Count + gl::PARTICLE_GROUP_SIZE - 1) / gl::PARTICLE_GROUP_SIZE; +} + +} // namespace + +opengl46_gpu_particles::~opengl46_gpu_particles() +{ + // the vertex array goes with the context; deleted here only if it's still there + if ((m_vao != 0) && (::glfwGetCurrentContext() != nullptr)) + ::glDeleteVertexArrays(1, &m_vao); +} + +bool opengl46_gpu_particles::init() +{ + if (m_state != 0) + return m_state > 0; + m_state = -1; + try + { + m_smokesimulate = make_program({"particles_smoke_simulate.comp"}); + m_smokeprepare = make_program({"particles_smoke_prepare.comp"}); + m_smokedraw = make_program({"particles_smoke.vert", "particles_smoke.frag"}); + m_precipitationprepare = make_program({"particles_precipitation.comp"}); + m_precipitationdraw = make_program({"particles_precipitation.vert", "particles_precipitation.frag"}); + } + catch (gl::shader_exception const &e) + { + ErrorLog("particles: can't use the programs of the particles of the gpu, the smoke stays on the cpu: " + std::string(e.what())); + return false; + } + + // the attributes of the instances: three vec4 of gl::particle_draw + ::glCreateVertexArrays(1, &m_vao); + for (GLuint attribute = 0; attribute < 3; ++attribute) + { + ::glEnableVertexArrayAttrib(m_vao, attribute); + ::glVertexArrayAttribFormat(m_vao, attribute, 4, GL_FLOAT, GL_FALSE, attribute * sizeof(glm::vec4)); + ::glVertexArrayAttribBinding(m_vao, attribute, 0); + } + ::glVertexArrayBindingDivisor(m_vao, 0, 1); + + m_smokecommand = std::make_unique(4 * sizeof(GLuint), gl::PARTICLE_COMMAND_SSBO_BINDING); + m_precipitationcommand = std::make_unique(4 * sizeof(GLuint), gl::PARTICLE_COMMAND_SSBO_BINDING); + m_smokereadback = std::make_unique(4 * sizeof(GLuint)); + m_precipitationreadback = std::make_unique(4 * sizeof(GLuint)); + m_state = 1; + return true; +} + +bool opengl46_gpu_particles::smoke_available() +{ + return init(); +} + +bool opengl46_gpu_particles::precipitation_available() +{ + return init(); +} + +void opengl46_gpu_particles::clear() +{ + m_pool.clear(); + m_requested.clear(); + m_poolfull = false; + m_sourcerecords.clear(); + m_places = 0; + m_particles.reset(); + m_sources.reset(); + m_owners.reset(); + m_smokeoutput.reset(); + m_precipitationoutput.reset(); + m_precipitationcapacity = 0; + m_originset = false; + m_lastupdate = 0; +} + +void opengl46_gpu_particles::layout(particle_manager &Particles) +{ + auto const &sources{Particles.sequence()}; + // the sources already laid out which asked for the same places since + std::size_t kept{0}; + while ((kept < m_requested.size()) && (kept < sources.size()) && (m_requested[kept] == sources[kept].max_particles())) + ++kept; + if ((kept == m_requested.size()) && (kept == sources.size()) && m_particles) + return; + if (kept < m_requested.size()) + { + // a source went away or changed: all anew + kept = 0; + m_pool.clear(); + m_requested.clear(); + m_places = 0; + m_poolfull = false; + } + auto const oldplaces{m_places}; + + auto first{m_places}; + m_pool.resize(sources.size()); + m_requested.resize(sources.size()); + for (std::size_t i = kept; i < sources.size(); ++i) + { + m_requested[i] = sources[i].max_particles(); + auto const size{static_cast(std::min(m_requested[i], MAX_SMOKE_PLACES))}; + m_pool[i] = pool_source{}; + if (first + size > MAX_SMOKE_PLACES) + { + if (false == m_poolfull) + WriteLog("particles: the smoke of some sources left out, over " + std::to_string(MAX_SMOKE_PLACES) + " places of particles"); + m_poolfull = true; + continue; + } + m_pool[i].first = first; + m_pool[i].size = size; + first += size; + } + m_places = first; + + std::vector owners(std::max(m_places, 1), 0); + for (std::size_t i = 0; i < m_pool.size(); ++i) + std::fill_n(owners.begin() + m_pool[i].first, m_pool[i].size, static_cast(i)); + + auto const places{static_cast(std::max(m_places, 1))}; + // the particles of the places kept go over to the new storage, the new places are dead + auto particles{std::make_unique(places * static_cast(sizeof(gl::particle_smoke)), gl::PARTICLE_SSBO_BINDING)}; + ::glClearNamedBufferData(*particles, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, nullptr); + if (m_particles && (oldplaces > 0) && (kept > 0)) + ::glCopyNamedBufferSubData(*m_particles, *particles, 0, 0, static_cast(oldplaces) * static_cast(sizeof(gl::particle_smoke))); + m_particles = std::move(particles); + + auto const ensure = [](std::unique_ptr &Buffer, GLsizeiptr const Size, GLuint const Binding) { + if (!Buffer) + Buffer = std::make_unique(Size, Binding); + else + Buffer->resize(Size); + }; + ensure(m_owners, places * static_cast(sizeof(std::uint32_t)), gl::PARTICLE_OWNER_SSBO_BINDING); + ensure(m_smokeoutput, places * static_cast(sizeof(gl::particle_draw)), gl::PARTICLE_OUTPUT_SSBO_BINDING); + ensure(m_sources, static_cast(std::max(sources.size(), 1) * sizeof(gl::particle_smoke_source)), gl::PARTICLE_SOURCE_SSBO_BINDING); + m_owners->update(owners.data(), 0, static_cast(owners.size() * sizeof(std::uint32_t))); + m_sourcerecords.resize(sources.size(), gl::particle_smoke_source{}); +} + +void opengl46_gpu_particles::simulate_smoke(particle_manager &Particles, glm::dvec3 const &Camera) +{ + if (false == init()) + return; + layout(Particles); + if (Particles.updates() == m_lastupdate) + return; + m_lastupdate = Particles.updates(); + if (m_places == 0) + return; + + // the origin of the positions near the camera; when it moves, the particles move by as much the other way + glm::vec3 shift{0.f}; + auto const snapped{glm::floor(Camera / ORIGIN_STEP) * ORIGIN_STEP}; + if (false == m_originset) + { + m_origin = snapped; + m_originset = true; + } + else if (glm::length(Camera - m_origin) > ORIGIN_REACH) + { + shift = glm::vec3{m_origin - snapped}; + m_origin = snapped; + } + + // the light of the smoke as the cpu works it out, for gfx.smoke.lighting off + auto const oldlight{(glm::vec3{Global.DayLight.ambient} + 0.35f * glm::vec3{Global.DayLight.diffuse}) * simulation::Environment.light_intensity()}; + auto const &sources{Particles.sequence()}; + m_smokealive = 0; + for (std::size_t i = 0; i < sources.size(); ++i) + { + auto const &state{sources[i].gpu()}; + auto &pool{m_pool[i]}; + auto &record{m_sourcerecords[i]}; + auto const spawns{(pool.size > 0) ? std::min(state.spawns, pool.size) : 0u}; + record.pool = glm::uvec4{pool.first, pool.size, pool.head, spawns}; + record.location = glm::vec4{glm::vec3{state.location - m_origin}, 0.f}; + record.previous = glm::vec4{glm::vec3{state.previous - m_origin}, 0.f}; + record.launch = glm::vec4{state.inclination[0], state.inclination[1], state.velocity[0], state.velocity[1]}; + record.initial = glm::vec4{state.size[0], state.size[1], state.opacity[0], state.opacity[1]}; + record.sizechange = glm::vec4{state.sizechange[0], state.sizechange[1], state.sizechange[2], 0.f}; + record.opacitychange = glm::vec4{state.opacitychange[0], state.opacitychange[1], state.opacitychange[2], 0.f}; + record.motion = glm::vec4{state.rise, state.damping, state.risemin, 0.f}; + record.color = glm::vec4{glm::clamp(state.color, glm::vec3{0.f}, glm::vec3{1.f}), 0.f}; + record.oldcolor = glm::vec4{glm::clamp(state.color * oldlight, glm::vec3{0.f}, glm::vec3{1.f}), 0.f}; + if (pool.size > 0) + { + pool.head = (pool.head + spawns) % pool.size; + if (spawns > 0) + m_smokealive += pool.size; + } + } + if (false == m_sourcerecords.empty()) + m_sources->update(m_sourcerecords.data(), 0, static_cast(m_sourcerecords.size() * sizeof(gl::particle_smoke_source))); + + m_simulatetimer.begin(); + m_particles->bind_base(); + m_sources->bind_base(); + m_owners->bind_base(); + auto const wind{glm::vec3{simulation::Environment.wind()}}; + auto &program{*m_smokesimulate}; + ::glProgramUniform1ui(program, 0, m_places); + ::glProgramUniform4f(program, 1, static_cast(Particles.timedelta()), shift.x, shift.y, shift.z); + ::glProgramUniform4f(program, 2, wind.x, wind.y, wind.z, 0.f); + ::glProgramUniform1ui(program, 3, static_cast(m_lastupdate)); + program.dispatch(groups(m_places)); + ::glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + m_simulatetimer.end(); +} + +std::array opengl46_gpu_particles::planes(view const &View) +{ + // the sides and the near side of the clip space, w >= 0 (the far plane is at infinity) + auto const matrix{glm::transpose(View.projection * View.modelview)}; + std::array result{matrix[3] + matrix[0], matrix[3] - matrix[0], matrix[3] + matrix[1], matrix[3] - matrix[1], matrix[3]}; + for (auto &plane : result) + { + auto const length{glm::length(glm::vec3{plane})}; + if (length > 0.f) + plane /= length; + } + return result; +} + +void opengl46_gpu_particles::collect(gl::readback &Readback, std::size_t &Drawn) +{ + GLuint command[4]; + if (Readback.pop(command)) + Drawn = command[1]; +} + +void opengl46_gpu_particles::draw(GLuint const Output, GLuint const Command, gl::program &Program) +{ + auto const culling{::glIsEnabled(GL_CULL_FACE) == GL_TRUE}; + if (culling) + ::glDisable(GL_CULL_FACE); + Program.bind(); + // the vertex array is bound behind the back of gl::vao, which is told nothing is bound afterwards + gl::vao::unbind(); + ::glVertexArrayVertexBuffer(m_vao, 0, Output, 0, sizeof(gl::particle_draw)); + ::glBindVertexArray(m_vao); + ::glBindBuffer(GL_DRAW_INDIRECT_BUFFER, Command); + ::glDrawArraysIndirect(GL_TRIANGLE_STRIP, nullptr); + ::glBindBuffer(GL_DRAW_INDIRECT_BUFFER, 0); + gl::vao::unbind(); + gl::program::unbind(); + if (culling) + ::glEnable(GL_CULL_FACE); +} + +std::size_t opengl46_gpu_particles::render_smoke(view const &View) +{ + if ((false == init()) || (m_places == 0)) + return 0; + collect(*m_smokereadback, m_smokedrawn); + + m_smoketimer.begin(); + GLuint const command[4]{4, 0, 0, 0}; + m_smokecommand->update(command, 0, sizeof(command)); + m_particles->bind_base(); + m_sources->bind_base(); + m_owners->bind_base(); + m_smokeoutput->bind_base(); + m_smokecommand->bind_base(); + auto &program{*m_smokeprepare}; + auto const origin{glm::vec3{m_origin - View.camera}}; + auto const viewplanes{planes(View)}; + ::glProgramUniform1ui(program, 0, m_places); + ::glProgramUniform3f(program, 1, origin.x, origin.y, origin.z); + ::glProgramUniformMatrix4fv(program, 2, 1, GL_FALSE, glm::value_ptr(View.modelview)); + ::glProgramUniform4f(program, 3, Global.gfx_smoke_lighting ? 1.f : 0.f, SMOKE_FORWARD, static_cast(m_places), 0.f); + ::glProgramUniform4fv(program, 4, 5, glm::value_ptr(viewplanes[0])); + program.dispatch(groups(m_places)); + ::glMemoryBarrier(GL_COMMAND_BARRIER_BIT | GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT); + + ::glProgramUniform4f(*m_smokedraw, 0, View.soft ? Global.gfx_particles_soft : 0.f, 0.f, 0.f, 0.f); + draw(*m_smokeoutput, *m_smokecommand, *m_smokedraw); + m_smoketimer.end(); + + m_smokereadback->push(*m_smokecommand); + return m_smokedrawn; +} + +std::size_t opengl46_gpu_particles::render_precipitation(view const &View, bool const Snow, float const Intensity, unsigned int const Frame) +{ + if (false == init()) + return 0; + collect(*m_precipitationreadback, m_precipitationdrawn); + + auto const &kind{Snow ? SNOW : RAIN}; + // once per frame: the time moves on, the particles drift with the wind and fall, the camera has its velocity + if ((false == m_framestarted) || (Frame != m_frame)) + { + m_framestarted = true; + m_frame = Frame; + auto const seconds{Timer::GetDeltaTime()}; + if (seconds > 0.0) + { + auto const wind{glm::dvec3{simulation::Environment.wind()} * WIND_SHARE}; + for (std::size_t box = 0; box < BOX_SIZE.size(); ++box) + { + auto const size{static_cast(BOX_SIZE[box])}; + m_drift[box] = glm::mod(m_drift[box] + wind * seconds, size); + m_fall[box] = std::fmod(m_fall[box] + kind.fall * seconds, 16.0 * size); + } + m_time = std::fmod(m_time + seconds, 3600.0); + if (m_camerastarted) + { + auto const velocity{(View.camera - m_lastcamera) / seconds}; + if (glm::length(velocity) < CAMERA_JUMP) + m_cameravelocity = glm::mix(m_cameravelocity, velocity, 1.0 - std::exp(-10.0 * seconds)); + } + } + m_lastcamera = View.camera; + m_camerastarted = true; + } + + // the particles, more of them in heavier rain or snow + auto const most{static_cast(std::max(Global.gfx_precipitation_particles, 0))}; + auto const count{static_cast(most * std::clamp(0.15f + 0.85f * Intensity, 0.f, 1.f))}; + m_precipitationcount = count; + m_precipitationsnow = Snow; + if (count == 0) + return 0; + if (m_precipitationcapacity < count) + { + m_precipitationcapacity = std::max(count, most); + auto const size{static_cast(m_precipitationcapacity) * static_cast(sizeof(gl::particle_draw))}; + if (!m_precipitationoutput) + m_precipitationoutput = std::make_unique(size, gl::PARTICLE_OUTPUT_SSBO_BINDING); + else + m_precipitationoutput->resize(size); + } + + m_precipitationtimer.begin(); + GLuint const command[4]{4, 0, 0, 0}; + m_precipitationcommand->update(command, 0, sizeof(command)); + m_precipitationoutput->bind_base(); + m_precipitationcommand->bind_base(); + auto &program{*m_precipitationprepare}; + auto const second{static_cast(count * BOX_SHARE[0])}; + auto const third{static_cast(count * (BOX_SHARE[0] + BOX_SHARE[1]))}; + ::glProgramUniform4ui(program, 0, second, third, count, Snow ? 1u : 0u); + std::array offsets; + for (std::size_t box = 0; box < BOX_SIZE.size(); ++box) + { + auto const size{static_cast(BOX_SIZE[box])}; + offsets[box] = glm::vec4{glm::vec3{glm::mod(m_drift[box] - glm::mod(View.camera, size), size)}, static_cast(m_fall[box])}; + } + ::glProgramUniform4fv(program, 1, 3, glm::value_ptr(offsets[0])); + ::glProgramUniform4f(program, 4, BOX_SIZE[0], BOX_SIZE[1], BOX_SIZE[2], 0.f); + auto const velocity{glm::vec3{glm::dvec3{simulation::Environment.wind()} * WIND_SHARE - m_cameravelocity}}; + ::glProgramUniform4f(program, 5, velocity.x, velocity.y, velocity.z, kind.fall); + ::glProgramUniform4f(program, 6, static_cast(m_time), kind.exposure, kind.opacity, static_cast(m_precipitationcapacity)); + ::glProgramUniformMatrix4fv(program, 7, 1, GL_FALSE, glm::value_ptr(View.modelview)); + ::glProgramUniform4f(program, 11, 1.f, kind.forward, kind.brightness, 0.f); + auto const viewplanes{planes(View)}; + ::glProgramUniform4fv(program, 12, 5, glm::value_ptr(viewplanes[0])); + program.dispatch(groups(count)); + ::glMemoryBarrier(GL_COMMAND_BARRIER_BIT | GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT); + + // the width of a pixel a metre in front of the camera, for the least width of a streak + auto const pixel{(View.height > 0) && (View.projection[1][1] != 0.f) ? 2.f / (std::abs(View.projection[1][1]) * static_cast(View.height)) : 0.f}; + ::glProgramUniform4f(*m_precipitationdraw, 0, View.soft ? 0.25f * Global.gfx_particles_soft : 0.f, 0.f, 0.f, 0.f); + ::glProgramUniform4f(*m_precipitationdraw, 1, pixel, 0.f, 0.f, 0.f); + draw(*m_precipitationoutput, *m_precipitationcommand, *m_precipitationdraw); + m_precipitationtimer.end(); + + m_precipitationreadback->push(*m_precipitationcommand); + return m_precipitationdrawn; +} + +std::string opengl46_gpu_particles::info() const +{ + if (m_state <= 0) + return {}; + std::string text; + if (m_places > 0) + text += " smoke of the gpu: " + std::to_string(m_smokedrawn) + " drawn, " + std::to_string(m_places) + " places of " + std::to_string(m_pool.size()) + " sources (" + + std::to_string(m_smokealive) + " of the ones which bore lately)" + (m_poolfull ? ", some sources left out" : "") + "\n"; + if (m_precipitationcount > 0) + text += std::string(" ") + (m_precipitationsnow ? "snow" : "rain") + " of the gpu: " + std::to_string(m_precipitationdrawn) + " of " + std::to_string(m_precipitationcount) + + " drawn\n"; + return text; +} + +std::string opengl46_gpu_particles::info_times() +{ + std::string text; + auto const add = [&text](gl::gpu_timer &Timer, char const *Name) { + if (auto const time = Timer.result()) + text += std::string(text.empty() ? " particles:" : ",") + " " + Name + " " + to_string(static_cast(*time) / 1000000.0, 3) + " ms"; + }; + if (m_places > 0) + { + add(m_simulatetimer, "smoke simulation"); + add(m_smoketimer, "smoke"); + } + if (m_precipitationcount > 0) + add(m_precipitationtimer, m_precipitationsnow ? "snow" : "rain"); + if (false == text.empty()) + text += "\n"; + return text; +} diff --git a/rendering/opengl46/opengl46gpuparticles.h b/rendering/opengl46/opengl46gpuparticles.h new file mode 100644 index 00000000..6b24e029 --- /dev/null +++ b/rendering/opengl46/opengl46gpuparticles.h @@ -0,0 +1,137 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include "gl/shader.h" +#include "gl/ssbo.h" +#include "gl/ubo.h" +#include "gl/query.h" +#include "gl/readback.h" + +class particle_manager; + +// particles of the gpu for the opengl 4.6 renderer: the smoke and the rain or snow. +// +// smoke: the sources (particle_manager in its gpu mode) work out on the cpu only how many particles they bear in an update of +// the simulation and what these start from; the particles are kept in a storage buffer, a ring of places for each source, and +// simulated by a compute pass once per update (particles_smoke_simulate.comp). in every colour pass which draws them another +// compute pass (particles_smoke_prepare.comp) leaves out the dead ones and the ones out of the view, lights the rest with the +// lights of the pass and adds them to an indirect draw of billboards (particles_smoke.vert/.frag). +// +// rain and snow: particles in three boxes around the camera, each repeated along the world axes, which keep no state: their +// place follows from their number, how far the wind took them and how far they fell (particles_precipitation.comp), drawn as +// streaks of their motion against the camera (particles_precipitation.vert/.frag). +// +// the positions of the smoke are kept from an origin near the camera, moved by whole steps as the camera goes, so they keep +// their precision far from the centre of the scenery +class opengl46_gpu_particles +{ + public: + opengl46_gpu_particles() = default; + ~opengl46_gpu_particles(); + opengl46_gpu_particles(opengl46_gpu_particles const &) = delete; + opengl46_gpu_particles &operator=(opengl46_gpu_particles const &) = delete; + + // what a colour pass draws with + struct view + { + glm::mat4 modelview; // world axes around the camera to the view space of the pass + glm::mat4 projection; + glm::dvec3 camera; + int height{0}; // of the target, pixels + bool soft{false}; // the resolved depth of the scene drawn so far is bound to gl::SCENE_DEPTH_TEX + }; + + // whether the programs could be made; without them the smoke stays on the cpu and the rain is the old cylinder + bool smoke_available(); + bool precipitation_available(); + // once per update of the particle manager (in its gpu mode): the births and the change of the smoke. Camera: of the view + void simulate_smoke(particle_manager &Particles, glm::dvec3 const &Camera); + // draws the smoke the view sees, with the texture bound to unit 0. returns: particles drawn, as read back a few frames later + std::size_t render_smoke(view const &View); + // draws the rain (Snow false) or the snow of the intensity 0-1. Frame: number of the frame, the time moves on once per frame. + // returns: particles drawn, read back + std::size_t render_precipitation(view const &View, bool Snow, float Intensity, unsigned int Frame); + // drops what is on the gpu, e.g. when the context goes + void clear(); + + std::string info() const; + std::string info_times(); + + private: + struct pool_source + { + std::uint32_t first{0}; + std::uint32_t size{0}; + std::uint32_t head{0}; + }; + + bool init(); + // the places of the sources in the storage: sources added after the others get theirs after the places already given, and + // the particles stay; otherwise all is laid out anew + void layout(particle_manager &Particles); + // reads back the instances counted by a pass; Drawn is left as it was when nothing came back yet + static void collect(gl::readback &Readback, std::size_t &Drawn); + // the planes of the view of the pass, for the culling of the compute passes + static std::array planes(view const &View); + void draw(GLuint Output, GLuint Command, gl::program &Program); + + int m_state{0}; // 0: not set up yet, 1: ready, -1: can't be used + std::unique_ptr m_smokesimulate; + std::unique_ptr m_smokeprepare; + std::unique_ptr m_smokedraw; + std::unique_ptr m_precipitationprepare; + std::unique_ptr m_precipitationdraw; + GLuint m_vao{0}; // the attributes of the instances, from the buffer of the draw bound to it before the draw + + // smoke + std::vector m_pool; + std::vector m_requested; // the sizes the sources asked for + std::vector m_sourcerecords; + std::uint32_t m_places{0}; + bool m_poolfull{false}; // some sources got no places + std::unique_ptr m_particles; + std::unique_ptr m_sources; + std::unique_ptr m_owners; + std::unique_ptr m_smokeoutput; + std::unique_ptr m_smokecommand; + std::unique_ptr m_smokereadback; + std::uint64_t m_lastupdate{0}; + glm::dvec3 m_origin{0.0}; + bool m_originset{false}; + std::size_t m_smokedrawn{0}; + std::size_t m_smokealive{0}; // places of the sources which bore particles lately + gl::gpu_timer m_simulatetimer; + gl::gpu_timer m_smoketimer; + + // rain and snow + std::unique_ptr m_precipitationoutput; + std::unique_ptr m_precipitationcommand; + std::unique_ptr m_precipitationreadback; + std::uint32_t m_precipitationcapacity{0}; + unsigned int m_frame{0}; + bool m_framestarted{false}; + std::array m_drift{}; // how far the wind took the particles of each box, within the box + std::array m_fall{}; // how far they fell, within 16 sizes of the box + double m_time{0.0}; // seconds, for the flutter of the flakes + glm::dvec3 m_lastcamera{0.0}; + glm::dvec3 m_cameravelocity{0.0}; + bool m_camerastarted{false}; + std::size_t m_precipitationdrawn{0}; + std::uint32_t m_precipitationcount{0}; + bool m_precipitationsnow{false}; + gl::gpu_timer m_precipitationtimer; +}; diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 6292ace8..51ea6633 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -865,6 +865,7 @@ void opengl46_renderer::SwapBuffers() m_debugtimestext += m_localshadows.info_times(); if( Global.gfx_shadow_sdsm ) m_debugtimestext += m_shadowfit.info_times(); + m_debugtimestext += m_gpuparticles.info_times(); m_debugtimestext += "uilayer: " + to_string( Timer::subsystem.gfx_gui.average(), 2 ) + " ms\n"; if( DebugModeFlag ) m_debugtimestext += m_textures.info(); @@ -964,6 +965,7 @@ 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_instancecull.info(); if( Global.gfx_gpucull_shadows ) { for( std::size_t cascade = 0; cascade < m_shadowcull.size(); ++cascade ) { @@ -1062,6 +1064,21 @@ void opengl46_renderer::draw_debug_ui() } ImGui::End(); + if (ImGui::Begin("Particles")) + { + ImGui::Checkbox("smoke simulated and drawn on the gpu (gfx.smoke.gpu)", &Global.gfx_smoke_gpu); + if (Global.gfx_smoke_gpu) + ImGui::Checkbox("smoke lit by the sun, the sky and the lamps (gfx.smoke.lighting)", &Global.gfx_smoke_lighting); + ImGui::Checkbox("rain and snow as particles of the gpu (gfx.precipitation.gpu)", &Global.gfx_precipitation_gpu); + if (Global.gfx_precipitation_gpu) + ImGui::SliderInt("particles of the heaviest rain or snow (gfx.precipitation.particles)", &Global.gfx_precipitation_particles, 0, 262144); + ImGui::SliderFloat("soft particles, share of their size, 0: off (gfx.particles.soft)", &Global.gfx_particles_soft, 0.f, 4.f, "%.2f"); + auto const info{m_gpuparticles.info()}; + if (!info.empty()) + ImGui::TextUnformatted(info.c_str() + 1); + } + ImGui::End(); + if (ImGui::Begin("Sky and exposure")) { m_sky.draw_debug_ui(); @@ -1464,6 +1481,12 @@ void opengl46_renderer::Render_pass(viewport_config &vp, rendermode const Mode) Render_Alpha(simulation::Region); Render_editor_overlay(); + // the depth drawn so far, which the particles of the gpu fade in front of + m_particlesoft = + ( Global.gfx_particles_soft > 0.f ) + && ( ( Global.Smoke && Global.gfx_smoke_gpu && ( false == simulation::Particles.sequence().empty() ) ) || ( Global.gfx_precipitation_gpu && ( Global.Overcast > 1.f ) ) ) + && Setup_Particles_Depth( vp ); + // particles Render_particles(); @@ -6897,10 +6920,59 @@ void opengl46_renderer::Render_particles() model_ubo->update(model_ubs); Bind_Texture(0, m_smoketexture); + // the smoke of the gpu, when its programs could be made; otherwise the particle manager simulates it on the cpu. a switch + // takes effect from the next update of the manager + auto const gpu{Global.gfx_smoke_gpu && m_gpuparticles.smoke_available()}; + simulation::Particles.gpu(gpu); + if (gpu) + { + if (Global.Smoke) + { + m_gpuparticles.simulate_smoke(simulation::Particles, m_renderpass.pass_camera.position()); + m_renderpass.draw_stats.particles = static_cast(m_gpuparticles.render_smoke(Particles_View())); + } + return; + } m_particlerenderer.update(m_renderpass.pass_camera); m_renderpass.draw_stats.particles = m_particlerenderer.render(); } +opengl46_gpu_particles::view opengl46_renderer::Particles_View() const +{ + GLint viewport[4]; + ::glGetIntegerv(GL_VIEWPORT, viewport); + opengl46_gpu_particles::view view; + view.modelview = OpenGLMatrices.data(GL_MODELVIEW); + view.projection = OpenGLMatrices.data(GL_PROJECTION); + view.camera = m_renderpass.pass_camera.position(); + view.height = viewport[3]; + view.soft = m_particlesoft; + return view; +} + +bool opengl46_renderer::Setup_Particles_Depth(viewport_config &Viewport) +{ + // the fading works out the distance from the depth as the contact shadows do (Contact_Shadows_Wanted()) + if (Global.gfx_skippipeline || Global.bWireFrame || (Viewport.msaa_fb == nullptr)) + return false; + auto const &projection{scene_ubs.projection}; + if ((projection[2][3] == 0.f) || (projection[2][2] != 0.f) || (projection[3][2] <= 0.f)) + return false; + if (false == Resolved_Depth_Target(Viewport)) + return false; + // making the target binds it + Viewport.msaa_fb->bind(); + // the scissor test would limit the blit + auto const scissor{::glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE}; + if (scissor) + ::glDisable(GL_SCISSOR_TEST); + ::glBlitNamedFramebuffer(*Viewport.msaa_fb, *Viewport.clusters_depth_fb, 0, 0, Viewport.width, Viewport.height, 0, 0, Viewport.width, Viewport.height, GL_DEPTH_BUFFER_BIT, GL_NEAREST); + if (scissor) + ::glEnable(GL_SCISSOR_TEST); + ::glBindTextureUnit(gl::SCENE_DEPTH_TEX, Viewport.clusters_depth_tex->id); + return true; +} + void opengl46_renderer::Render_vr_models() { if (!vr) @@ -6927,6 +6999,15 @@ void opengl46_renderer::Render_precipitation() return; } + if (Global.gfx_precipitation_gpu && m_gpuparticles.precipitation_available()) + { + // the particles of the gpu: the overcast above 1 is how heavy the rain or snow is + model_ubs.set_modelview(OpenGLMatrices.data(GL_MODELVIEW)); + model_ubo->update(model_ubs); + m_gpuparticles.render_precipitation(Particles_View(), Global.Weather == "snow:", std::clamp(Global.Overcast - 1.f, 0.f, 1.f), m_framestamp); + return; + } + ::glPushMatrix(); // tilt the precipitation cone against the velocity vector for crude motion blur // include current wind vector while at it diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index 2e74ee3b..d914c283 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/openglcamera.h" #include "rendering/opengl46/opengl46light.h" #include "rendering/opengl46/opengl46particles.h" +#include "rendering/opengl46/opengl46gpuparticles.h" #include "rendering/opengl46/opengl46skydome.h" #include "rendering/opengl46/opengl46precipitation.h" #include "simulation/simulationenvironment.h" @@ -447,6 +448,9 @@ class opengl46_renderer : public gfx_renderer { void Setup_Contact_Shadows( viewport_config &Viewport ); // makes the target of the resolved depth of the viewport if it isn't there yet; false if it can't be made bool Resolved_Depth_Target( viewport_config &Viewport ); + // resolves the depth drawn so far in the colour pass of the viewport for the soft particles (gfx.particles.soft) and binds it + // to gl::SCENE_DEPTH_TEX; false when the viewport has no such depth + bool Setup_Particles_Depth( viewport_config &Viewport ); // the direction the sun shadows are cast in (the sun, its height capped by gfx.shadow.angle.min), and the rotation from // the world to the space of the light of the cascades: x and y across the maps, z along the light glm::vec3 Shadow_Light_Vector() const; @@ -495,6 +499,8 @@ class opengl46_renderer : public gfx_renderer { void Render(TMemCell *Memcell); void Render_particles(); void Render_precipitation(); + // the view of the colour pass being drawn, for the particles of the gpu + opengl46_gpu_particles::view Particles_View() const; // the grass of the heightmap terrain around the camera of the main view; Future: the matrix of the motion vectors void Render_grass(glm::mat4 const &Future); void Render_vr_models(); @@ -588,6 +594,8 @@ class opengl46_renderer : public gfx_renderer { opengl46_skydome m_skydomerenderer; 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) + bool m_particlesoft{false}; // the depth for the soft particles is bound in the colour pass being drawn unsigned int m_framestamp; // id of currently rendered gfx frame float m_framerate; diff --git a/rendering/particles.cpp b/rendering/particles.cpp index 5f7158e0..a945d894 100644 --- a/rendering/particles.cpp +++ b/rendering/particles.cpp @@ -152,15 +152,9 @@ smoke_source::bind( TAnimModel const *Node ) { owner_type::none ); } -// updates state of owned particles +// adds the particles to spawn in an update of Timedelta to m_spawncount void -smoke_source::update( double const Timedelta, bool const Onlydespawn ) { - - // prepare bounding box for new pass - // TODO: include bounding box in the bounding_area class - bounding_box boundingbox { - glm::dvec3{ std::numeric_limits::max() }, - glm::dvec3{ std::numeric_limits::lowest() } }; +smoke_source::accumulate_spawns( double const Timedelta, bool const Onlydespawn ) { m_spawncount = ( ( ( false == Global.Smoke ) || ( true == Onlydespawn ) ) ? @@ -209,6 +203,19 @@ smoke_source::update( double const Timedelta, bool const Onlydespawn ) { } } } +} + +// updates state of owned particles +void +smoke_source::update( double const Timedelta, bool const Onlydespawn ) { + + // prepare bounding box for new pass + // TODO: include bounding box in the bounding_area class + bounding_box boundingbox { + glm::dvec3{ std::numeric_limits::max() }, + glm::dvec3{ std::numeric_limits::lowest() } }; + + accumulate_spawns( Timedelta, Onlydespawn ); // update spawned particles for( auto particleiterator { std::begin( m_particles ) }; particleiterator != std::end( m_particles ); ++particleiterator ) { @@ -329,23 +336,37 @@ smoke_source::initialize( smoke_particle &Particle ) { Particle.position = location(); if( m_ownertype == owner_type::vehicle ) { - Particle.opacity *= static_cast(m_owner.vehicle->MoverParameters->dizel_fill); + float velocityscale, opacityscale; + launch_scales( velocityscale, opacityscale ); + Particle.opacity *= opacityscale; + Particle.velocity *= velocityscale; + } +} + +// shares of the launch velocity and of the opacity of a particle born now, which the owner gives +void +smoke_source::launch_scales( float &Velocity, float &Opacity ) const { + + Velocity = 1.f; + Opacity = 1.f; + if( m_ownertype != owner_type::vehicle ) { return; } + + Opacity = static_cast(m_owner.vehicle->MoverParameters->dizel_fill); if (m_owner.vehicle->MoverParameters->EngineType == TEngineType::DieselElectric) { if (m_owner.vehicle->MoverParameters->dizel_spinup == true) { - Particle.velocity *= 0.38*(((m_owner.vehicle->MoverParameters->enrot)/2)*0.5); // / m_owner.vehicle->MoverParameters->dizel_fill *0.01)) ; + Velocity = static_cast(0.38*(((m_owner.vehicle->MoverParameters->enrot)/2)*0.5)); // / m_owner.vehicle->MoverParameters->dizel_fill *0.01)) ; } else { - Particle.velocity *= ((((m_owner.vehicle->MoverParameters->enrot)/60)*0.6) * (m_owner.vehicle->MoverParameters->EnginePower *0.03)); // / m_owner.vehicle->MoverParameters->dizel_fill *0.01)) ; + Velocity = static_cast((((m_owner.vehicle->MoverParameters->enrot)/60)*0.6) * (m_owner.vehicle->MoverParameters->EnginePower *0.03)); // / m_owner.vehicle->MoverParameters->dizel_fill *0.01)) ; //Particle.velocity *= m_owner.vehicle->GetVelocity(); // / m_owner.vehicle->MoverParameters->dizel_fill *0.01)) ; } } - } } // updates state of provided particle and bounding box. returns: true if particle is still alive afterwards, false otherwise @@ -385,6 +406,58 @@ smoke_source::update( smoke_particle &Particle, bounding_box &Boundingbox, doubl return true; } +// the same as update() for the particles simulated on the gpu: the births of the update and what the particles start from and +// change by, worked out as the cpu does it in update(), initialize() and update( Particle ). the particles of the cpu are dropped +void +smoke_source::update_gpu( double const Timedelta, bool const Onlydespawn ) { + + m_particles.clear(); + + accumulate_spawns( Timedelta, Onlydespawn ); + // the cpu fills the free places, then replaces the oldest; a full source can't take more than its size in an update and + // drops the rest of whole particles, as the cpu does + auto const spawns { std::min( std::floor( m_spawncount ), static_cast( m_max_particles ) ) }; + m_spawncount -= spawns; + m_spawncount -= std::floor( m_spawncount ); + m_gpu.spawns = static_cast( std::max( spawns, 0.f ) ); + + auto const here { location() }; + m_gpu.previous = ( m_gpu.started ? m_gpu.location : here ); + m_gpu.location = here; + m_gpu.started = true; + + float velocityscale, opacityscale; + launch_scales( velocityscale, opacityscale ); + for( int limit = value_limit::min; limit <= value_limit::max; ++limit ) { + m_gpu.inclination[ limit ] = glm::radians( m_emitter.inclination[ limit ] ); + m_gpu.velocity[ limit ] = m_emitter.velocity[ limit ] * velocityscale; + m_gpu.size[ limit ] = m_emitter.size[ limit ]; + m_gpu.opacity[ limit ] = m_emitter.opacity[ limit ] / Global.SmokeFidelity * opacityscale; + } + m_gpu.sizechange[ 0 ] = m_sizemodifier.value_change(); + m_gpu.sizechange[ 1 ] = m_sizemodifier.limit( value_limit::min ); + m_gpu.sizechange[ 2 ] = m_sizemodifier.limit( value_limit::max ); + m_gpu.opacitychange[ 0 ] = m_opacitymodifier.value_change(); + m_gpu.opacitychange[ 1 ] = m_opacitymodifier.limit( value_limit::min ); + m_gpu.opacitychange[ 2 ] = m_opacitymodifier.limit( value_limit::max ); + // crude smoke dispersion simulation, as in update( Particle ) + if( m_ownertype == owner_type::vehicle ) { + m_gpu.rise = static_cast( 0.025 * std::min( 0.f, Global.AirTemperature - 90 ) ); + m_gpu.damping = static_cast( 0.05 * pow( m_owner.vehicle->GetVelocity() * 1, 0.4 ) * Global.Overcast ); + m_gpu.risemin = std::numeric_limits::lowest(); + } + else { + m_gpu.rise = static_cast( 0.005 * std::min( 0.f, Global.AirTemperature - 10 ) ); + m_gpu.damping = static_cast( 0.050 * Global.Overcast ); + m_gpu.risemin = static_cast( 0.25 * ( 2.f - Global.Overcast ) ); + } + m_gpu.color = m_emitter.color; + + // the renderer culls the particles one by one; the area is where they come from, for the distance of the source + m_area.center = here; + m_area.radius = 0; +} + // adds a new particle source of specified type, placing it in specified world location @@ -437,13 +510,33 @@ particle_manager::update() { if( timedelta == 0.0 ) { return; } + ++m_updates; + m_timedelta = timedelta; + auto const distancethreshold { 2 * Global.BaseDrawRange * Global.fDistanceFactor }; // to reduce workload distant enough sources won't spawn new particles for( auto &source : m_sources ) { auto const viewerdistance { glm::length( source.area().center - glm::dvec3{ Global.pCamera.Pos } ) - source.area().radius }; - source.update( timedelta, viewerdistance > distancethreshold ); + if( m_gpu ) { + source.update_gpu( timedelta, viewerdistance > distancethreshold ); + } + else { + source.update( timedelta, viewerdistance > distancethreshold ); + } + } +} + +void +particle_manager::gpu( bool const Gpu ) { + + if( Gpu == m_gpu ) { return; } + m_gpu = Gpu; + for( auto &source : m_sources ) { + source.m_particles.clear(); + source.m_gpu.started = false; + source.m_gpu.spawns = 0; } } diff --git a/rendering/particles.h b/rendering/particles.h index df592014..54512675 100644 --- a/rendering/particles.h +++ b/rendering/particles.h @@ -57,6 +57,9 @@ public: m_valuechangemodifier == nullptr ? m_valuechange : m_valuechange / *m_valuechangemodifier ); } + Type_ + limit( value_limit const Limit ) const { + return m_valuelimits[ Limit ]; } private: //types @@ -81,6 +84,25 @@ class smoke_source { public: // types using particle_sequence = std::vector; + // what the particles simulated on the gpu (particle_manager::gpu(), opengl46_gpu_particles) take from the source in an + // update: how many are born, where and how they start, and how they change, all as the cpu update would do it + struct gpu_state { + std::uint32_t spawns { 0 }; // particles born in the last update, at most max_particles() + glm::dvec3 location { 0.0 }; // of the emitter at the last update... + glm::dvec3 previous { 0.0 }; // ...and at the one before; the particles of an update are born along the way between + bool started { false }; + float inclination[ 2 ] { 0.f, 0.f }; // radians + float velocity[ 2 ] { 0.f, 0.f }; // m/s, with the share the owner gives + float size[ 2 ] { 0.f, 0.f }; + float opacity[ 2 ] { 0.f, 0.f }; // with the smoke fidelity and the share the owner gives + float sizechange[ 3 ] { 0.f, 0.f, 0.f }; // per second, min, max + float opacitychange[ 3 ] { 0.f, 0.f, 0.f }; // per second (with the fidelity), min, max + // the vertical velocity: changes by (rise - damping) * itself per second, and doesn't fall below risemin + float rise { 0.f }; + float damping { 0.f }; + float risemin { std::numeric_limits::lowest() }; + glm::vec3 color { 0.f }; + }; // methods bool deserialize( cParser &Input ); @@ -93,6 +115,15 @@ public: // updates state of owned particles void update( double const Timedelta, bool const Onlydespawn ); + // the same for particles simulated on the gpu: only the births and the state of the source they start from + void + update_gpu( double const Timedelta, bool const Onlydespawn ); + gpu_state const & + gpu() const { + return m_gpu; } + std::size_t + max_particles() const { + return m_max_particles; } glm::vec3 const & color() const { return m_emitter.color; } @@ -131,6 +162,12 @@ private: // imports member data pair from the config file bool deserialize_mapping( cParser &Input ); + // adds the particles to spawn in an update of Timedelta to m_spawncount + void + accumulate_spawns( double const Timedelta, bool const Onlydespawn ); + // shares of the launch velocity and of the opacity of a particle born now, which the owner gives + void + launch_scales( float &Velocity, float &Opacity ) const; void initialize( smoke_particle &Particle ); // updates state of provided particle and bounding box. returns: true if particle is still alive afterwards, false otherwise @@ -158,6 +195,7 @@ private: particle_sequence m_particles; // collection of spawned particles std::size_t m_max_particles; // maximum number of particles existing scene::bounding_area m_area; // bounding sphere of owned particles + gpu_state m_gpu; // for the particles simulated on the gpu }; @@ -182,6 +220,20 @@ public: // updates state of all owned emitters void update(); + // whether the renderer simulates the particles on the gpu: the sources then work out only their births (smoke_source::gpu()); + // the renderer sets it before the next update. the particles simulated on the cpu are dropped when it's switched on + void + gpu( bool const Gpu ); + bool + gpu() const { + return m_gpu; } + // number of the updates in which the time went on, and the time of the last one: the gpu simulates once per update + std::uint64_t + updates() const { + return m_updates; } + double + timedelta() const { + return m_timedelta; } // data access source_sequence & sequence() { @@ -198,6 +250,9 @@ private: // members source_map m_sourcetemplates; // cached particle emitter configurations source_sequence m_sources; // all owned particle emitters + bool m_gpu { false }; + std::uint64_t m_updates { 0 }; + double m_timedelta { 0.0 }; }; diff --git a/shaders/particle_common.glsl b/shaders/particle_common.glsl new file mode 100644 index 00000000..3e40c018 --- /dev/null +++ b/shaders/particle_common.glsl @@ -0,0 +1,89 @@ +// storage of the particles of the gpu (opengl46_gpu_particles, gl::particle_* in gl/ubo.h) for the compute passes + +#ifndef PARTICLE_COMMON_GLSL +#define PARTICLE_COMMON_GLSL + +// a particle of smoke; it's dead (or its place was never used) when its opacity is 0 or less +struct smoke_particle_s +{ + vec4 position; // xyz: metres from the origin of the particles (world axes), w: rotation, radians + vec4 velocity; // xyz: m/s, w: size of the billboard, metres + vec4 state; // x: opacity, y: age, seconds +}; + +// a source of smoke as the last update of the simulation left it (smoke_source::gpu_state) +struct smoke_source_s +{ + uvec4 pool; // x: first place of the source in the storage, y: number of its places, z: place of the first particle born in the update, w: particles born + vec4 location; // xyz: emitter at the update, from the origin + vec4 previous; // xyz: emitter at the update before, from the origin + vec4 launch; // xy: inclination, radians, zw: speed, m/s + vec4 initial; // xy: size, zw: opacity + vec4 sizechange; // x: per second, y: min, z: max + vec4 opacitychange; // x: per second, y: min, z: max + vec4 motion; // x: rise, y: damping (per second, of the vertical velocity), z: least vertical velocity + vec4 color; // rgb: colour of the smoke, lit by the lights of the pass + vec4 oldcolor; // rgb: colour lit as the cpu does it, without the lights (gfx.smoke.lighting off) +}; + +// what a draw of a particle reads, as attributes of an instance +struct particle_draw_s +{ + vec4 place; // xyz: metres from the camera (world axes), w: size (smoke) or width (rain, snow) + vec4 shape; // x: rotation (smoke); xyz: from the head of the streak to its tail (rain, snow) + vec4 color; // rgb: lit colour, a: opacity +}; + +// the indirect draw command of the particles; the compute passes count the instances +layout(std430, binding = PARTICLE_COMMAND_SSBO_BINDING) buffer particle_command_ssbo +{ + uint particle_draw_vertices; + uint particle_draw_instances; + uint particle_draw_first; + uint particle_draw_base; +}; + +layout(std430, binding = PARTICLE_OUTPUT_SSBO_BINDING) writeonly buffer particle_output_ssbo +{ + particle_draw_s particle_draws[]; +}; + +// pcg hash +uint particle_hash(uint value) +{ + uint state = value * 747796405U + 2891336453U; + uint word = ((state >> ((state >> 28U) + 4U)) ^ state) * 277803737U; + return (word >> 22U) ^ word; +} + +// next of a sequence of random numbers 0-1 +float particle_random(inout uint state) +{ + state = particle_hash(state); + return float(state >> 8U) * (1.0 / 16777216.0); +} + +// whether a sphere (centre from the camera along the world axes) is within the planes of the view +bool particle_visible(vec4 planes[5], vec3 centre, float radius) +{ + for (int i = 0; i < 5; i++) + if (dot(planes[i].xyz, centre) + planes[i].w < -radius) + return false; + return true; +} + +// adds a particle to the draw; Capacity: the places of the storage of the draws +void particle_emit(vec3 place, float size, vec3 shape, vec3 color, float opacity, uint capacity) +{ + uint index = atomicAdd(particle_draw_instances, 1U); + if (index >= capacity) + { + atomicAdd(particle_draw_instances, 0xFFFFFFFFU); + return; + } + particle_draws[index].place = vec4(place, size); + particle_draws[index].shape = vec4(shape, 0.0); + particle_draws[index].color = vec4(color, opacity); +} + +#endif diff --git a/shaders/particle_light.glsl b/shaders/particle_light.glsl new file mode 100644 index 00000000..b647e9c9 --- /dev/null +++ b/shaders/particle_light.glsl @@ -0,0 +1,146 @@ +// light of the particles of the gpu (opengl46_gpu_particles: smoke, rain, snow) at a point of the view space of the pass, as a +// particle takes it as a whole, at its middle: the ambient light, the sun (the moon at night) held back by the shadow map and +// by the clouds, and the lamps and headlights of the light cluster the point is in (the first lights of lights[] without the +// clusters). each light is scattered towards the camera by the phase of the particle, Forward: 0 evenly, towards 1 mostly +// onwards. the scale is the one of the shading of the surfaces (light_common.glsl): a white particle takes as much light as +// a white face turned half way to it. for the compute passes; needs and + +#ifndef PARTICLE_LIGHT_GLSL +#define PARTICLE_LIGHT_GLSL + +#if SHADOWMAP_ENABLED +uniform sampler2DArrayShadow shadowmap; +#endif +uniform sampler2D headlightmap; +layout(binding = HEADLIGHT_SHADOW_TEX) uniform sampler2DArrayShadow headlight_shadowmap; + +// AMBIENT_SCALE and SUN_DIFFUSE_SCALE of light_common.glsl +const float PARTICLE_AMBIENT_SCALE = 0.65; +const float PARTICLE_SUN_SCALE = 1.5; + +// Henyey-Greenstein, 1 for every angle when G is 0 +float particle_phase(float cosangle, float g) +{ + float g2 = g * g; + return (1.0 - g2) / pow(max(1.0 + g2 - 2.0 * g * cosangle, 1e-4), 1.5); +} + +// share of the sunlight the shadow map holds back at a point of the view space, one comparison (as fog_inject.comp) +float particle_sun_shadow(vec3 position) +{ +#if SHADOWMAP_ENABLED + float span_distance = dot(position, position); + uint cascade; + for (cascade = 0U; cascade < MAX_CASCADES; cascade++) + if (span_distance <= cascade_end[cascade]) + break; + for (; cascade < MAX_CASCADES; cascade++) + { + vec4 lightpos = lightview[cascade] * vec4(position, 1.0); + vec3 coords = lightpos.xyz / lightpos.w; + if (all(greaterThanEqual(coords.xy, vec2(0.0))) && all(lessThanEqual(coords.xy, vec2(1.0)))) + { + if (coords.z < 0.0) + return 0.0; + return texture(shadowmap, vec4(coords.xy, float(cascade), coords.z + 0.0001 * float(cascade + 1U))); + } + // outside the map: lit, unless the cascades are fitted to the depth of the view (gfx.shadow.sdsm), whose maps cover only + // the area of the surfaces seen; then the first further cascade whose map holds the point + if (shadow_fit.x <= 0.0) + return 0.0; + } + return 0.0; +#else + return 0.0; +#endif +} + +// light of a lamp or of the beam of headlights reaching a point of the view space (as fog_inject.comp; the beams with their +// shadow maps, the lamps without) +vec3 particle_lamp(light_s light, vec3 position) +{ + if (light.type == LIGHT_HEADLIGHTS) + { + vec4 projected = light.headlight_projection * vec4(position, 1.0); + vec3 coords = projected.xyz / projected.w; + if (coords.z < 0.0 || coords.z > 1.0 || any(lessThan(coords.xy, vec2(0.0))) || any(greaterThan(coords.xy, vec2(1.0)))) + return vec3(0.0); + float span_distance = length(light.pos - position); + float atten = 1.0 / (1.0 + light.linear * span_distance + light.quadratic * span_distance * span_distance); + atten *= mix(1.0, 0.0, clamp((coords.z - 0.998) * 500.0, 0.0, 1.0)); + vec3 beam = textureLod(headlightmap, coords.xy, 1.0).rgb * light.headlight_weights.rgb; + float intensity = max(max(beam.r, beam.g), beam.b); + if ((light.headlight_weights.w > 0.5) && (intensity > 0.0)) + { + uint map = uint(light.headlight_weights.w - 0.5); + vec4 shadowpos = headlight_shadow[map] * vec4(position, 1.0); + if (shadowpos.w > 0.0) + { + vec2 shadowcoords = shadowpos.xy / shadowpos.w; + if (all(greaterThanEqual(shadowcoords, vec2(0.0))) && all(lessThanEqual(shadowcoords, vec2(1.0)))) + { + float texel = headlight_shadow_params[map].x * shadowpos.w; + float reference = shadowpos.z / max(shadowpos.w - texel * 1.5, 1e-3); + intensity *= 1.0 - texture(headlight_shadowmap, vec4(shadowcoords, float(map), reference)); + } + } + } + return light.color * (atten * intensity * light.intensity); + } + vec3 tolight = light.pos - position; + float distance2 = dot(tolight, tolight); + float edge = clamp(1.0 - distance2 * distance2 * (light.quadratic * light.quadratic), 0.0, 1.0); + float atten = edge * edge / max(distance2, 0.01); + if (light.type == LIGHT_SPOT) + atten *= smoothstep(light.out_cutoff, light.in_cutoff, dot(-tolight * inversesqrt(max(distance2, 1e-8)), normalize(light.dir))); + return light.color * (atten * light.intensity); +} + +// the light cluster of a point of the view space in front of the camera, as fragment_cluster() of light_common.glsl finds it for +// the fragment drawn there +uint particle_cluster(vec3 position) +{ + vec4 clip = cluster_projection * vec4(position, 1.0); + vec2 screen = clip.xy / clip.w * 0.5 + 0.5; + ivec2 tile = clamp(ivec2(screen * vec2(CLUSTER_GRID_X, CLUSTER_GRID_Y)), ivec2(0), ivec2(CLUSTER_GRID_X - 1, CLUSTER_GRID_Y - 1)); + int slice = clamp(int(floor(cluster_slice_bias + log(max(clip.w, 1e-4)) * cluster_slice_scale)), 0, CLUSTER_GRID_Z - 1); + return uint(tile.x + CLUSTER_GRID_X * (tile.y + CLUSTER_GRID_Y * slice)); +} + +// Position: of the particle in the view space; World: its offset from the camera along the world axes +vec3 particle_light(vec3 position, vec3 world, float forward) +{ + vec3 tocamera = -normalize(position); + vec3 light = ambient * PARTICLE_AMBIENT_SCALE; + if (lights_count > 0U) + { + float sunlit = cloud_shadow(world) * (1.0 - particle_sun_shadow(position)); + if (sunlit > 0.0) + light += lights[0].color * (0.5 * PARTICLE_SUN_SCALE * lights[0].intensity * sunlit * particle_phase(dot(normalize(lights[0].dir), tocamera), forward)); + } + vec3 lamps = vec3(0.0); + if (lights_clustered != 0U) + { + if ((cluster_valid != 0U) && (position.z < 0.0)) + { + cluster_range_s range = cluster_ranges[particle_cluster(position)]; + for (uint i = 0U; i < range.count; i++) + { + light_s lamp = lights[cluster_light_indices[range.offset + i]]; + vec3 fromlamp = position - lamp.pos; + lamps += particle_lamp(lamp, position) * particle_phase(dot(fromlamp, tocamera) * inversesqrt(max(dot(fromlamp, fromlamp), 1e-8)), forward); + } + } + } + else + { + for (uint i = 1U; i < lights_count; i++) + { + vec3 fromlamp = position - lights[i].pos; + lamps += particle_lamp(lights[i], position) * particle_phase(dot(fromlamp, tocamera) * inversesqrt(max(dot(fromlamp, fromlamp), 1e-8)), forward); + } + } + return light + lamps * 0.5; +} + +#endif diff --git a/shaders/particle_soft.glsl b/shaders/particle_soft.glsl new file mode 100644 index 00000000..3843e7a1 --- /dev/null +++ b/shaders/particle_soft.glsl @@ -0,0 +1,24 @@ +// soft particles (opengl46_gpu_particles): a particle fades where it nears what is behind it, instead of being cut by it. +// the resolved depth of the scene drawn so far (gl::SCENE_DEPTH_TEX), reversed with the far plane at infinity: the distance +// along the axis of the view is the near plane over the depth, and the sky, which writes no depth, stays at 0 + +#ifndef PARTICLE_SOFT_GLSL +#define PARTICLE_SOFT_GLSL + +layout(binding = SCENE_DEPTH_TEX) uniform sampler2D particle_scene_depth; + +// share of the particle left at the fragment, 0-1: Range (metres) is how far in front of the scene it fades out over, 0 for none. +// Fragdistance: of the fragment along the axis of the view +float particle_soft_fade(float range, float fragdistance) +{ + if (range <= 0.0) + return 1.0; + float depth = texelFetch(particle_scene_depth, ivec2(gl_FragCoord.xy), 0).r; + if (depth <= 0.0) + return 1.0; + float scene = projection[3][2] / depth; + float share = clamp((scene - fragdistance) / range, 0.0, 1.0); + return share * share * (3.0 - 2.0 * share); +} + +#endif diff --git a/shaders/particles_precipitation.comp b/shaders/particles_precipitation.comp new file mode 100644 index 00000000..a6439653 --- /dev/null +++ b/shaders/particles_precipitation.comp @@ -0,0 +1,75 @@ +// rain and snow of the gpu (opengl46_gpu_particles), in every colour pass which draws them: a thread per drop or flake. the +// particles fill three boxes around the camera, a small dense one and two larger ones, each repeated along the world axes: +// a particle keeps its place in the box, moved by how far the wind took them all and how far it fell, so nothing is kept +// from frame to frame. what the camera sees is lit (particle_light.glsl) and drawn as a streak from where it is to where +// it was a moment ago, seen from the moving camera (particles_precipitation.vert) + +#include +#include +#include +#include + +layout(local_size_x = PARTICLE_GROUP_SIZE) in; + +// x, y: first particles of the second and of the third box, z: particles, w: 1 for snow, 0 for rain +layout(location = 0) uniform uvec4 counts; +// xyz: how far the wind took the particles less where the camera is, in each box (metres, 0 to the size of the box); w: how +// far they fell in it, 0 to 16 sizes of the box +layout(location = 1) uniform vec4 offsets[3]; +// xyz: size of each box, metres; w unused +layout(location = 4) uniform vec4 sizes; +// xyz: velocity of the wind less the one of the camera, m/s; w: speed of the fall +layout(location = 5) uniform vec4 velocity; +// x: seconds (for the flutter of the flakes), y: how long a streak lasts, seconds, z: opacity, w: places for the draws +layout(location = 6) uniform vec4 timing; +// the world axes around the camera to the view space of the pass +layout(location = 7) uniform mat4 view; +// y: how much a particle scatters the light onwards, z: brightness +layout(location = 11) uniform vec4 options; +layout(location = 12) uniform vec4 planes[5]; + +void main() +{ + uint index = gl_GlobalInvocationID.x; + if (index >= counts.z) + return; + uint box = (index < counts.x) ? 0U : (index < counts.y) ? 1U : 2U; + float size = sizes[box]; + bool snow = counts.w != 0U; + + uint state = particle_hash(index * 3U + 0x9E3779B9U); + vec3 home = vec3(particle_random(state), particle_random(state), particle_random(state)) * size; + // the speed of the fall is (16 + k) / 16 of the common one, so a whole number of sizes of the box (16 of them) of the common + // fall is a whole number of them for every particle, and the fall can be kept within 16 sizes + float pace = (16.0 + floor(particle_random(state) * 7.0) - 3.0) / 16.0; + float width = snow ? mix(0.004, 0.012, particle_random(state)) : mix(0.0012, 0.0025, particle_random(state)); + vec3 move = vec3(velocity.xyz) + vec3(0.0, -velocity.w * pace, 0.0); + vec3 place = home + offsets[box].xyz - vec3(0.0, offsets[box].w * pace, 0.0); + if (snow) + { + // the flakes flutter about their way + float amplitude = mix(0.1, 0.35, particle_random(state)); + vec2 frequency = mix(vec2(1.5), vec2(5.0), vec2(particle_random(state), particle_random(state))); + vec2 phase = vec2(particle_random(state), particle_random(state)) * 6.2831853; + vec2 angle = frequency * timing.x + phase; + place.xz += amplitude * sin(angle); + move.xz += amplitude * frequency * cos(angle); + } + // within the box around the camera + vec3 world = mod(place, size) - 0.5 * size; + float opacity = timing.z; + // fading at the walls of the box, where the particles wrap around, and right at the camera + vec3 walls = abs(world) / (0.5 * size); + opacity *= 1.0 - smoothstep(0.7, 1.0, max(max(walls.x, walls.y), walls.z)); + opacity *= smoothstep(0.15, 0.6, length(world)); + if (opacity <= 0.002) + return; + vec3 tail = -move * timing.y; + float streak = length(tail); + if (streak > 3.0) + tail *= 3.0 / streak; + if (false == particle_visible(planes, world + 0.5 * tail, 0.5 * length(tail) + width)) + return; + vec3 color = particle_light((view * vec4(world, 1.0)).xyz, world, options.y); + particle_emit(world, width, tail, color * options.z, opacity, uint(timing.w)); +} diff --git a/shaders/particles_precipitation.frag b/shaders/particles_precipitation.frag new file mode 100644 index 00000000..a8c478a8 --- /dev/null +++ b/shaders/particles_precipitation.frag @@ -0,0 +1,46 @@ +// rain and snow of the gpu (opengl46_gpu_particles, particles_precipitation.vert): a streak rounded at its ends, fainter towards +// its tail, in the colour the compute pass lit it with, faded in front of the scene, fogged + +in vec4 f_pos; +in vec4 f_color; +in vec4 f_streak; +in float f_soft; + +in vec4 f_clip_pos; +in vec4 f_clip_future_pos; + +#include +#include +#include +#include + +layout(location = 0) out vec4 out_color; +#if MOTIONBLUR_ENABLED +layout(location = 1) out vec4 out_motion; +#endif + +void main() +{ + // distance from the axis of the streak, in its radii + float along = clamp(f_streak.y, 0.0, f_streak.w); + float offaxis = length(vec2(f_streak.x, f_streak.y - along)) / max(f_streak.z, 1e-6); + float alpha = f_color.a * (1.0 - smoothstep(0.35, 1.0, offaxis)); + alpha *= mix(1.0, 0.4, along / max(f_streak.w, 1e-6)); + alpha *= particle_soft_fade(f_soft, -f_pos.z); + if (alpha <= 0.0) + discard; + vec4 color = vec4(apply_fog(f_color.rgb), alpha); +#if POSTFX_ENABLED + out_color = color; +#else + out_color = tonemap(color); +#endif +#if MOTIONBLUR_ENABLED + { + vec2 a = (f_clip_future_pos.xy / f_clip_future_pos.w) * 0.5 + 0.5; + vec2 b = (f_clip_pos.xy / f_clip_pos.w) * 0.5 + 0.5; + + out_motion = vec4(a - b, 0.0f, alpha * alpha_mult); + } +#endif +} diff --git a/shaders/particles_precipitation.vert b/shaders/particles_precipitation.vert new file mode 100644 index 00000000..a4cbf144 --- /dev/null +++ b/shaders/particles_precipitation.vert @@ -0,0 +1,58 @@ +// rain and snow of the gpu (opengl46_gpu_particles): a streak per drop or flake the compute pass added to the draw, from where +// it is to where it was a moment ago as the moving camera saw it, turned to face the camera, rounded at its ends. a particle +// thinner than a pixel and a half is drawn that wide and as much fainter, so the distant ones don't flicker + +layout(location = 0) in vec4 v_place; // xyz: the head, metres from the camera (world axes), w: width +layout(location = 1) in vec4 v_shape; // xyz: from the head to the tail +layout(location = 2) in vec4 v_color; // rgb: lit colour, a: opacity + +out vec4 f_pos; +out vec4 f_color; +// x: across the streak, y: along it from the head, metres; z: its radius, w: its length +out vec4 f_streak; +out float f_soft; + +out vec4 f_clip_pos; +out vec4 f_clip_future_pos; + +#include + +// x: metres it fades out over in front of the scene, 0: none +layout(location = 0) uniform vec4 particle_soft; +// x: width of a pixel one metre in front of the camera, metres +layout(location = 1) uniform vec4 particle_pixel; + +void main() +{ + vec3 head = (modelview * vec4(v_place.xyz, 1.0)).xyz; + vec3 axis = mat3(modelview) * v_shape.xyz; + float span = length(axis); + vec3 direction = (span > 1e-5) ? axis / span : vec3(0.0, -1.0, 0.0); + vec3 middle = head + 0.5 * axis; + + float width = v_place.w; + float alpha = v_color.a; + float least = 1.5 * particle_pixel.x * max(-middle.z, 1e-3); + if (width < least) + { + alpha *= width / least; + width = least; + } + float radius = 0.5 * width; + vec3 side = cross(direction, normalize(-middle)); + float sidelength = length(side); + side = (sidelength > 1e-4) ? side / sidelength : vec3(1.0, 0.0, 0.0); + + // a triangle strip of four corners: across, along + vec2 corner = vec2(float(gl_VertexID & 1), float(gl_VertexID >> 1)); + float across = (corner.x * 2.0 - 1.0) * radius; + float along = mix(-radius, span + radius, corner.y); + f_pos = vec4(head + direction * along + side * across, 1.0); + f_clip_pos = projection * f_pos; + f_clip_future_pos = (projection * future) * f_pos; + + gl_Position = f_clip_pos; + f_streak = vec4(across, along, radius, span); + f_color = vec4(v_color.rgb, alpha); + f_soft = particle_soft.x; +} diff --git a/shaders/particles_smoke.frag b/shaders/particles_smoke.frag new file mode 100644 index 00000000..10a5eeef --- /dev/null +++ b/shaders/particles_smoke.frag @@ -0,0 +1,42 @@ +// the smoke of the gpu (opengl46_gpu_particles, particles_smoke.vert): the texture of the smoke in the colour the compute pass +// lit the particle with, faded in front of the scene, fogged + +in vec4 f_color; +in vec2 f_coord; +in vec4 f_pos; +in float f_soft; + +in vec4 f_clip_pos; +in vec4 f_clip_future_pos; + +#texture (tex1, 0, sRGB_A) +uniform sampler2D tex1; + +#include +#include +#include +#include + +layout(location = 0) out vec4 out_color; +#if MOTIONBLUR_ENABLED +layout(location = 1) out vec4 out_motion; +#endif + +void main() +{ + vec4 tex_color = texture(tex1, f_coord) * f_color; + tex_color.a *= particle_soft_fade(f_soft, -f_pos.z); +#if POSTFX_ENABLED + out_color = vec4(apply_fog(tex_color.rgb), tex_color.a); +#else + out_color = tonemap(vec4(apply_fog(tex_color.rgb), tex_color.a)); +#endif +#if MOTIONBLUR_ENABLED + { + vec2 a = (f_clip_future_pos.xy / f_clip_future_pos.w) * 0.5 + 0.5; + vec2 b = (f_clip_pos.xy / f_clip_pos.w) * 0.5 + 0.5; + + out_motion = vec4(a - b, 0.0f, tex_color.a * alpha_mult); + } +#endif +} diff --git a/shaders/particles_smoke.vert b/shaders/particles_smoke.vert new file mode 100644 index 00000000..77ec42e7 --- /dev/null +++ b/shaders/particles_smoke.vert @@ -0,0 +1,37 @@ +// the smoke of the gpu (opengl46_gpu_particles): a billboard per particle the compute pass added to the draw, turned about the +// axis of the view by the rotation of the particle (as opengl46_particles builds them on the cpu) + +layout(location = 0) in vec4 v_place; // xyz: metres from the camera (world axes), w: size +layout(location = 1) in vec4 v_shape; // x: rotation, radians +layout(location = 2) in vec4 v_color; // rgb: lit colour, a: opacity + +out vec4 f_pos; +out vec4 f_color; +out vec2 f_coord; +// metres in front of the scene the particle fades out over, see particle_soft.glsl +out float f_soft; + +out vec4 f_clip_pos; +out vec4 f_clip_future_pos; + +#include + +// x: share of the size of the particle it fades out over in front of the scene (gfx.particles.soft), 0: none +layout(location = 0) uniform vec4 particle_soft; + +void main() +{ + // a triangle strip of four corners + vec2 corner = vec2(float(gl_VertexID & 1), float(gl_VertexID >> 1)) - 0.5; + float c = cos(v_shape.x); + float s = sin(v_shape.x); + vec2 offset = vec2(c * corner.x - s * corner.y, s * corner.x + c * corner.y) * v_place.w; + f_pos = modelview * vec4(v_place.xyz, 1.0) + vec4(offset, 0.0, 0.0); + f_clip_pos = projection * f_pos; + f_clip_future_pos = (projection * future) * f_pos; + + gl_Position = f_clip_pos; + f_coord = corner + 0.5; + f_color = v_color; + f_soft = particle_soft.x * 0.5 * v_place.w; +} diff --git a/shaders/particles_smoke_prepare.comp b/shaders/particles_smoke_prepare.comp new file mode 100644 index 00000000..3a1b23df --- /dev/null +++ b/shaders/particles_smoke_prepare.comp @@ -0,0 +1,52 @@ +// the smoke of the gpu (opengl46_gpu_particles), in every colour pass which draws it: a thread per place of the storage. the +// living particles the view sees are lit (particle_light.glsl) and added to the draw (particles_smoke.vert) + +#include +#include +#include +#include + +layout(local_size_x = PARTICLE_GROUP_SIZE) in; + +layout(std430, binding = PARTICLE_SSBO_BINDING) readonly buffer particle_ssbo +{ + smoke_particle_s particles[]; +}; +layout(std430, binding = PARTICLE_SOURCE_SSBO_BINDING) readonly buffer particle_source_ssbo +{ + smoke_source_s sources[]; +}; +layout(std430, binding = PARTICLE_OWNER_SSBO_BINDING) readonly buffer particle_owner_ssbo +{ + uint owners[]; +}; + +layout(location = 0) uniform uint places; +// xyz: the origin of the particles less the camera, metres along the world axes +layout(location = 1) uniform vec3 origin; +// the world axes around the camera to the view space of the pass +layout(location = 2) uniform mat4 view; +// x: 1 when the smoke is lit by the lights of the pass, y: how much it scatters the light onwards, z: places for the draws +layout(location = 3) uniform vec4 options; +// planes of the view (world axes around the camera): a point is inside when dot(xyz, point) + w >= 0 +layout(location = 4) uniform vec4 planes[5]; + +void main() +{ + uint place = gl_GlobalInvocationID.x; + if (place >= places) + return; + smoke_particle_s particle = particles[place]; + if (particle.state.x <= 0.0) + return; + vec3 world = particle.position.xyz + origin; + float size = particle.velocity.w; + // the corners of the billboard turned anyhow + if (false == particle_visible(planes, world, size * 0.7072)) + return; + smoke_source_s source = sources[owners[place]]; + vec3 color = source.oldcolor.rgb; + if (options.x > 0.5) + color = source.color.rgb * particle_light((view * vec4(world, 1.0)).xyz, world, options.y); + particle_emit(world, size, vec3(particle.position.w, 0.0, 0.0), color, clamp(particle.state.x, 0.0, 1.0), uint(options.z)); +} diff --git a/shaders/particles_smoke_simulate.comp b/shaders/particles_smoke_simulate.comp new file mode 100644 index 00000000..590c3569 --- /dev/null +++ b/shaders/particles_smoke_simulate.comp @@ -0,0 +1,84 @@ +// the smoke of the gpu (opengl46_gpu_particles), once per update of the simulation: a thread per place of the storage. the +// particles the sources had born in the update take the places after the ones of the last update (each source keeps its +// places as a ring, the oldest give way), spread along the way the emitter went since the update before; the living ones +// change as smoke_source::update() changes them on the cpu + +#include +#include + +layout(local_size_x = PARTICLE_GROUP_SIZE) in; + +layout(std430, binding = PARTICLE_SSBO_BINDING) buffer particle_ssbo +{ + smoke_particle_s particles[]; +}; +layout(std430, binding = PARTICLE_SOURCE_SSBO_BINDING) readonly buffer particle_source_ssbo +{ + smoke_source_s sources[]; +}; +// the source of every place +layout(std430, binding = PARTICLE_OWNER_SSBO_BINDING) readonly buffer particle_owner_ssbo +{ + uint owners[]; +}; + +layout(location = 0) uniform uint places; +// x: seconds of the update, yzw: added to the positions, as the origin moved +layout(location = 1) uniform vec4 step; +// xyz: the wind, m/s; w unused +layout(location = 2) uniform vec4 wind; +// x: number of the update, for the random numbers +layout(location = 3) uniform uint seed; + +// smoke_source::update( Particle ); false when the particle is dead +bool advance(inout smoke_particle_s particle, smoke_source_s source, float seconds) +{ + particle.state.x = clamp(particle.state.x + source.opacitychange.x * seconds, source.opacitychange.y, source.opacitychange.z); + if (particle.state.x <= 0.0) + return false; + particle.velocity.w = clamp(particle.velocity.w + source.sizechange.x * seconds, source.sizechange.y, source.sizechange.z); + // crude smoke dispersion + particle.velocity.y += source.motion.x * particle.velocity.y * seconds; + particle.velocity.y -= source.motion.y * particle.velocity.y * seconds; + particle.velocity.y = max(source.motion.z, particle.velocity.y); + particle.position.xyz += particle.velocity.xyz * seconds; + particle.position.xyz += 0.1 * particle.state.y * wind.xyz * seconds; + particle.state.y += seconds; + return true; +} + +void main() +{ + uint place = gl_GlobalInvocationID.x; + if (place >= places) + return; + smoke_source_s source = sources[owners[place]]; + smoke_particle_s particle = particles[place]; + particle.position.xyz += step.yzw; + + uint size = source.pool.y; + uint order = (place - source.pool.x + size - source.pool.z) % size; + if (order < source.pool.w) + { + // born in the update: smoke_source::particle_emitter::initialize() + uint state = particle_hash(place ^ particle_hash(seed)); + float polar = mix(source.launch.x, source.launch.y, particle_random(state)); + float azimuth = radians(mix(-180.0, 180.0, particle_random(state))); + vec3 direction = vec3(-sin(polar) * sin(azimuth), cos(polar), sin(polar) * cos(azimuth)); + particle.velocity.xyz = direction * mix(source.launch.z, source.launch.w, particle_random(state)); + particle.position.w = radians(mix(0.0, 360.0, particle_random(state))); + particle.velocity.w = mix(source.initial.x, source.initial.y, particle_random(state)); + particle.state = vec4(mix(source.initial.z, source.initial.w, particle_random(state)), 0.0, 0.0, 0.0); + // the births spread over the update: where the emitter was then, and the time since + float share = (float(order) + 0.5) / float(source.pool.w); + particle.position.xyz = mix(source.previous.xyz, source.location.xyz, share); + if (false == advance(particle, source, step.x * (1.0 - share))) + particle.state.x = 0.0; + } + else if (particle.state.x > 0.0) + { + if (false == advance(particle, source, step.x)) + particle.state.x = 0.0; + } + particles[place] = particle; +} diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index bf06cd26..ee7ce467 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -1556,6 +1556,33 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token Parser >> gfx_grass_density; gfx_grass_density = std::clamp(gfx_grass_density, 0.f, 4.f); } + else if (Token == "gfx.smoke.gpu") + { + Parser.getTokens(1); + Parser >> gfx_smoke_gpu; + } + else if (Token == "gfx.smoke.lighting") + { + Parser.getTokens(1); + Parser >> gfx_smoke_lighting; + } + else if (Token == "gfx.particles.soft") + { + Parser.getTokens(1); + Parser >> gfx_particles_soft; + gfx_particles_soft = std::clamp(gfx_particles_soft, 0.f, 4.f); + } + else if (Token == "gfx.precipitation.gpu") + { + Parser.getTokens(1); + Parser >> gfx_precipitation_gpu; + } + else if (Token == "gfx.precipitation.particles") + { + Parser.getTokens(1); + Parser >> gfx_precipitation_particles; + gfx_precipitation_particles = std::clamp(gfx_precipitation_particles, 0, 1 << 20); + } else if (Token == "gfx.bufferstreaming") { Parser.getTokens(1); @@ -2074,6 +2101,11 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "gfx.grass.distance", gfx_grass_distance ); export_as_text( Output, "gfx.grass.fadedistance", gfx_grass_fadedistance ); export_as_text( Output, "gfx.grass.density", gfx_grass_density ); + export_as_text( Output, "gfx.smoke.gpu", gfx_smoke_gpu ); + export_as_text( Output, "gfx.smoke.lighting", gfx_smoke_lighting ); + export_as_text( Output, "gfx.particles.soft", gfx_particles_soft ); + export_as_text( Output, "gfx.precipitation.gpu", gfx_precipitation_gpu ); + export_as_text( Output, "gfx.precipitation.particles", gfx_precipitation_particles ); export_as_text( Output, "gfx.bufferstreaming", gfx_bufferstreaming ); export_as_text( Output, "gfx.postfx.motionblur.enabled", gfx_postfx_motionblur_enabled ); export_as_text( Output, "gfx.postfx.motionblur.shutter", gfx_postfx_motionblur_shutter ); diff --git a/utilities/Globals.h b/utilities/Globals.h index f07ef5f4..a205e0e5 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -332,6 +332,17 @@ struct global_settings { // (opengl 4.6) the grass thins out from gfx_grass_distance to here, where it ends float gfx_grass_fadedistance = 70.f; float gfx_grass_density = 1.f; + // (opengl 4.6) the smoke is simulated and drawn on the gpu (opengl46_gpu_particles), lit by the sun, the sky and the lamps + // when gfx_smoke_lighting; false: simulated on the cpu and drawn as before + bool gfx_smoke_gpu = true; + bool gfx_smoke_lighting = true; + // (opengl 4.6) the particles of the gpu fade where they near what is behind them, over this share of their size (smoke) or of + // a few decimetres (rain, snow), instead of being cut by it; 0: cut, as before + float gfx_particles_soft = 1.f; + // (opengl 4.6) rain and snow as particles around the camera, lit like the smoke (opengl46_gpu_particles), at most this many of + // them in the heaviest rain or snow; false: the textured cylinder, as before + bool gfx_precipitation_gpu = true; + int gfx_precipitation_particles = 65536; // (opengl 4.6) the model block of every draw and the matrices of instances are written to persistently mapped buffers, // one for each of the last frames, instead of with glBufferSubData. read at the start bool gfx_bufferstreaming = true;