17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-22 02:56: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

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