17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-21 23:26:24 +02:00

Simulate and light the smoke on the GPU and make rain and snow of particles

This commit is contained in:
2026-10-20 16:44:27 +02:00
parent 6a109e984c
commit 867497b658
22 changed files with 1637 additions and 13 deletions

View File

@@ -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")

View File

@@ -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

View File

@@ -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" +

View File

@@ -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
{

View File

@@ -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<float, 3> constexpr BOX_SIZE{10.f, 24.f, 60.f};
std::array<float, 3> 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<gl::program> make_program(std::vector<std::string> const &Names)
{
std::vector<std::unique_ptr<gl::shader>> shaders;
std::vector<std::reference_wrapper<const gl::shader>> references;
for (auto const &name : Names)
{
shaders.emplace_back(std::make_unique<gl::shader>(name));
references.emplace_back(*shaders.back());
}
return std::make_unique<gl::program>(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<gl::ssbo>(4 * sizeof(GLuint), gl::PARTICLE_COMMAND_SSBO_BINDING);
m_precipitationcommand = std::make_unique<gl::ssbo>(4 * sizeof(GLuint), gl::PARTICLE_COMMAND_SSBO_BINDING);
m_smokereadback = std::make_unique<gl::readback>(4 * sizeof(GLuint));
m_precipitationreadback = std::make_unique<gl::readback>(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::uint32_t>(std::min<std::size_t>(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<std::uint32_t> owners(std::max<std::uint32_t>(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<std::uint32_t>(i));
auto const places{static_cast<GLsizeiptr>(std::max<std::uint32_t>(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<gl::ssbo>(places * static_cast<GLsizeiptr>(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<GLsizeiptr>(oldplaces) * static_cast<GLsizeiptr>(sizeof(gl::particle_smoke)));
m_particles = std::move(particles);
auto const ensure = [](std::unique_ptr<gl::ssbo> &Buffer, GLsizeiptr const Size, GLuint const Binding) {
if (!Buffer)
Buffer = std::make_unique<gl::ssbo>(Size, Binding);
else
Buffer->resize(Size);
};
ensure(m_owners, places * static_cast<GLsizeiptr>(sizeof(std::uint32_t)), gl::PARTICLE_OWNER_SSBO_BINDING);
ensure(m_smokeoutput, places * static_cast<GLsizeiptr>(sizeof(gl::particle_draw)), gl::PARTICLE_OUTPUT_SSBO_BINDING);
ensure(m_sources, static_cast<GLsizeiptr>(std::max<std::size_t>(sources.size(), 1) * sizeof(gl::particle_smoke_source)), gl::PARTICLE_SOURCE_SSBO_BINDING);
m_owners->update(owners.data(), 0, static_cast<GLsizeiptr>(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<GLsizeiptr>(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<float>(Particles.timedelta()), shift.x, shift.y, shift.z);
::glProgramUniform4f(program, 2, wind.x, wind.y, wind.z, 0.f);
::glProgramUniform1ui(program, 3, static_cast<GLuint>(m_lastupdate));
program.dispatch(groups(m_places));
::glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
m_simulatetimer.end();
}
std::array<glm::vec4, 5> 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<glm::vec4, 5> 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<float>(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<double>(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::uint32_t>(std::max(Global.gfx_precipitation_particles, 0))};
auto const count{static_cast<std::uint32_t>(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<GLsizeiptr>(m_precipitationcapacity) * static_cast<GLsizeiptr>(sizeof(gl::particle_draw))};
if (!m_precipitationoutput)
m_precipitationoutput = std::make_unique<gl::ssbo>(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<std::uint32_t>(count * BOX_SHARE[0])};
auto const third{static_cast<std::uint32_t>(count * (BOX_SHARE[0] + BOX_SHARE[1]))};
::glProgramUniform4ui(program, 0, second, third, count, Snow ? 1u : 0u);
std::array<glm::vec4, 3> offsets;
for (std::size_t box = 0; box < BOX_SIZE.size(); ++box)
{
auto const size{static_cast<double>(BOX_SIZE[box])};
offsets[box] = glm::vec4{glm::vec3{glm::mod(m_drift[box] - glm::mod(View.camera, size), size)}, static_cast<float>(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<float>(m_time), kind.exposure, kind.opacity, static_cast<float>(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<float>(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<double>(*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;
}

View File

@@ -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 <array>
#include <memory>
#include <string>
#include <vector>
#include <glm/glm.hpp>
#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<glm::vec4, 5> 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<gl::program> m_smokesimulate;
std::unique_ptr<gl::program> m_smokeprepare;
std::unique_ptr<gl::program> m_smokedraw;
std::unique_ptr<gl::program> m_precipitationprepare;
std::unique_ptr<gl::program> 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<pool_source> m_pool;
std::vector<std::size_t> m_requested; // the sizes the sources asked for
std::vector<gl::particle_smoke_source> m_sourcerecords;
std::uint32_t m_places{0};
bool m_poolfull{false}; // some sources got no places
std::unique_ptr<gl::ssbo> m_particles;
std::unique_ptr<gl::ssbo> m_sources;
std::unique_ptr<gl::ssbo> m_owners;
std::unique_ptr<gl::ssbo> m_smokeoutput;
std::unique_ptr<gl::ssbo> m_smokecommand;
std::unique_ptr<gl::readback> 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<gl::ssbo> m_precipitationoutput;
std::unique_ptr<gl::ssbo> m_precipitationcommand;
std::unique_ptr<gl::readback> m_precipitationreadback;
std::uint32_t m_precipitationcapacity{0};
unsigned int m_frame{0};
bool m_framestarted{false};
std::array<glm::dvec3, 3> m_drift{}; // how far the wind took the particles of each box, within the box
std::array<double, 3> 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;
};

View File

@@ -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<int>(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

View File

@@ -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;

View File

@@ -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<double>::max() },
glm::dvec3{ std::numeric_limits<double>::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<double>::max() },
glm::dvec3{ std::numeric_limits<double>::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<float>(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<float>(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<float>(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<float>((((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<float>( m_max_particles ) ) };
m_spawncount -= spawns;
m_spawncount -= std::floor( m_spawncount );
m_gpu.spawns = static_cast<std::uint32_t>( 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<float>( 0.025 * std::min( 0.f, Global.AirTemperature - 90 ) );
m_gpu.damping = static_cast<float>( 0.05 * pow( m_owner.vehicle->GetVelocity() * 1, 0.4 ) * Global.Overcast );
m_gpu.risemin = std::numeric_limits<float>::lowest();
}
else {
m_gpu.rise = static_cast<float>( 0.005 * std::min( 0.f, Global.AirTemperature - 10 ) );
m_gpu.damping = static_cast<float>( 0.050 * Global.Overcast );
m_gpu.risemin = static_cast<float>( 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;
}
}

View File

@@ -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<smoke_particle>;
// 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<float>::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 };
};

View File

@@ -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

146
shaders/particle_light.glsl Normal file
View File

@@ -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 <common> and <weather_common.glsl>
#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

View File

@@ -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

View File

@@ -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 <common>
#include <weather_common.glsl>
#include <particle_light.glsl>
#include <particle_common.glsl>
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));
}

View File

@@ -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 <common>
#include <apply_fog.glsl>
#include <tonemapping.glsl>
#include <particle_soft.glsl>
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
}

View File

@@ -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 <common>
// 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;
}

View File

@@ -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 <common>
#include <apply_fog.glsl>
#include <tonemapping.glsl>
#include <particle_soft.glsl>
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
}

View File

@@ -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 <common>
// 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;
}

View File

@@ -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 <common>
#include <weather_common.glsl>
#include <particle_light.glsl>
#include <particle_common.glsl>
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));
}

View File

@@ -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 <common>
#include <particle_common.glsl>
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;
}

View File

@@ -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 );

View File

@@ -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;